├── .czrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github ├── FUNDING.yml ├── scripts │ └── rename.sh └── workflows │ ├── ci.yml │ └── use-template.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE.md ├── README.md ├── bun.lockb ├── contracts └── Lock.sol ├── deploy └── deploy.ts ├── hardhat.config.ts ├── package.json ├── tasks ├── accounts.ts └── lock.ts ├── test ├── lock │ ├── Lock.fixture.ts │ └── Lock.ts └── types.ts └── tsconfig.json /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .coverage_artifacts 3 | .coverage_cache 4 | .coverage_contracts 5 | artifacts 6 | build 7 | cache 8 | coverage 9 | dist 10 | node_modules 11 | types 12 | 13 | # files 14 | *.env 15 | *.log 16 | .DS_Store 17 | .pnp.* 18 | bun.lockb 19 | coverage.json 20 | package-lock.json 21 | pnpm-lock.yaml 22 | yarn.lock 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: "https://3cities.xyz/#/pay?c=CAESFAKY9DMuOFdjE4Wzl2YyUFipPiSfIgICATICCAJaFURvbmF0aW9uIHRvIFBhdWwgQmVyZw" 2 | github: "PaulRBerg" 3 | -------------------------------------------------------------------------------- /.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/foundry-template} 8 | GITHUB_REPOSITORY_OWNER=${2?Error: Please pass username, e.g. prb} 9 | GITHUB_REPOSITORY_DESCRIPTION=${3:-""} # If null then replace with empty string 10 | 11 | echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY" 12 | echo "GITHUB_REPOSITORY_OWNER: $GITHUB_REPOSITORY_OWNER" 13 | echo "GITHUB_REPOSITORY_DESCRIPTION: $GITHUB_REPOSITORY_DESCRIPTION" 14 | 15 | # jq is like sed for JSON data 16 | JQ_OUTPUT=`jq \ 17 | --arg NAME "@$GITHUB_REPOSITORY" \ 18 | --arg AUTHOR_NAME "$GITHUB_REPOSITORY_OWNER" \ 19 | --arg URL "https://github.com/$GITHUB_REPOSITORY_OWNER" \ 20 | --arg DESCRIPTION "$GITHUB_REPOSITORY_DESCRIPTION" \ 21 | '.name = $NAME | .description = $DESCRIPTION | .author |= ( .name = $AUTHOR_NAME | .url = $URL )' \ 22 | package.json 23 | ` 24 | 25 | # Overwrite package.json 26 | echo "$JQ_OUTPUT" > package.json 27 | 28 | # Make sed command compatible in both Mac and Linux environments 29 | # Reference: https://stackoverflow.com/a/38595160/8696958 30 | sedi () { 31 | sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@" 32 | } 33 | 34 | # Rename instances of "PaulRBerg/foundry-template" to the new repo name in README.md for badges only 35 | sedi "/gitpod/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" 36 | sedi "/gitpod-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" 37 | sedi "/gha/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" 38 | sedi "/gha-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | env: 4 | HARDHAT_VAR_MNEMONIC: "test test test test test test test test test test test junk" 5 | HARDHAT_VAR_INFURA_API_KEY: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" 6 | # Uncomment the following lines to set your configuration variables using 7 | # GitHub secrets (https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) 8 | # 9 | # HARDHAT_VAR_MNEMONIC: ${{ secrets.Mnemonic }} 10 | # HARDHAT_VAR_INFURA_API_KEY: ${{ secrets.InfuraApiKey }} 11 | # HARDHAT_VAR_ARBISCAN_API_KEY: ${{ secrets.ArbiscanApiKey }} 12 | # HARDHAT_VAR_BSCSCAN_API_KEY: ${{ secrets.BscscanApiKey }} 13 | # HARDHAT_VAR_ETHERSCAN_API_KEY: ${{ secrets.EtherscanApiKey }} 14 | # HARDHAT_VAR_OPTIMISM_API_KEY: ${{ secrets.OptimismApiKey }} 15 | # HARDHAT_VAR_POLYGONSCAN_API_KEY: ${{ secrets.PolygonscanApiKey }} 16 | # HARDHAT_VAR_SNOWTRACE_API_KEY: ${{ secrets.SnowtraceApiKey }} 17 | 18 | on: 19 | workflow_dispatch: 20 | pull_request: 21 | push: 22 | branches: 23 | - main 24 | 25 | jobs: 26 | ci: 27 | runs-on: "ubuntu-latest" 28 | steps: 29 | - name: "Check out the repo" 30 | uses: "actions/checkout@v4" 31 | 32 | - name: "Install Bun" 33 | uses: "oven-sh/setup-bun@v1" 34 | 35 | - name: "Install the dependencies" 36 | run: "bun install" 37 | 38 | - name: "Lint the code" 39 | run: "bun run lint" 40 | 41 | - name: "Add lint summary" 42 | run: | 43 | echo "## Lint results" >> $GITHUB_STEP_SUMMARY 44 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 45 | 46 | - name: "Compile the contracts and generate the TypeChain bindings" 47 | run: "bun run typechain" 48 | 49 | - name: "Test the contracts and generate the coverage report" 50 | run: "bun run coverage" 51 | 52 | - name: "Add test summary" 53 | run: | 54 | echo "## Test results" >> $GITHUB_STEP_SUMMARY 55 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 56 | -------------------------------------------------------------------------------- /.github/workflows/use-template.yml: -------------------------------------------------------------------------------- 1 | name: "Create" 2 | 3 | # The workflow will run only when the "Use this template" button is used 4 | on: 5 | push: 6 | 7 | jobs: 8 | create: 9 | # We only run this action when the repository isn't the template repository. References: 10 | # - https://docs.github.com/en/actions/learn-github-actions/contexts 11 | # - https://docs.github.com/en/actions/learn-github-actions/expressions 12 | if: ${{ !github.event.repository.is_template }} 13 | permissions: "write-all" 14 | runs-on: "ubuntu-latest" 15 | steps: 16 | - name: "Check out the repo" 17 | uses: "actions/checkout@v4" 18 | 19 | - name: "Update package.json" 20 | env: 21 | GITHUB_REPOSITORY_DESCRIPTION: ${{ github.event.repository.description }} 22 | run: ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" 23 | 24 | - name: "Add rename summary" 25 | run: | 26 | echo "## Commit result" >> $GITHUB_STEP_SUMMARY 27 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 28 | 29 | - name: "Remove files not needed in the user's copy of the template" 30 | run: | 31 | rm -f "./.github/FUNDING.yml" 32 | rm -f "./.github/scripts/rename.sh" 33 | rm -f "./.github/workflows/create.yml" 34 | 35 | - name: "Add remove summary" 36 | run: | 37 | echo "## Remove result" >> $GITHUB_STEP_SUMMARY 38 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 39 | 40 | - name: "Update commit" 41 | uses: "stefanzweifel/git-auto-commit-action@v4" 42 | with: 43 | commit_message: "feat: initial commit" 44 | commit_options: "--amend" 45 | push_options: "--force" 46 | skip_fetch: true 47 | 48 | - name: "Add commit summary" 49 | run: | 50 | echo "## Commit result" >> $GITHUB_STEP_SUMMARY 51 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .coverage_artifacts 3 | .coverage_cache 4 | .coverage_contracts 5 | artifacts 6 | build 7 | cache 8 | coverage 9 | dist 10 | node_modules 11 | types 12 | deployments 13 | 14 | # files 15 | *.env 16 | *.log 17 | .DS_Store 18 | .pnp.* 19 | coverage.json 20 | package-lock.json 21 | pnpm-lock.yaml 22 | yarn.lock 23 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .coverage_artifacts 3 | .coverage_cache 4 | .coverage_contracts 5 | artifacts 6 | build 7 | cache 8 | coverage 9 | dist 10 | node_modules 11 | types 12 | 13 | # files 14 | *.env 15 | *.log 16 | .DS_Store 17 | .pnp.* 18 | bun.lockb 19 | coverage.json 20 | package-lock.json 21 | pnpm-lock.yaml 22 | yarn.lock 23 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - "@trivago/prettier-plugin-sort-imports" 3 | - "prettier-plugin-solidity" 4 | printWidth: 120 5 | trailingComma: "all" 6 | 7 | overrides: 8 | - files: "*.md" 9 | options: 10 | proseWrap: "always" 11 | - files: "*.sol" 12 | options: 13 | compiler: "0.8.17" 14 | parser: "solidity-parse" 15 | tabWidth: 4 16 | - files: "*.ts" 17 | options: 18 | importOrder: ["", "^[./]"] 19 | importOrderParserPlugins: ["typescript"] 20 | importOrderSeparation: true 21 | importOrderSortSpecifiers: true 22 | parser: "typescript" 23 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | istanbulReporter: ["html", "lcov"], 3 | skipFiles: ["test"], 4 | }; 5 | -------------------------------------------------------------------------------- /.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 | "named-parameters-mapping": "warn", 10 | "no-console": "off", 11 | "not-rely-on-time": "off", 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "endOfLine": "auto" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/artifacts 3 | **/node_modules 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "NomicFoundation.hardhat-solidity"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "prettier.documentSelectors": ["**/*.sol"], 5 | "solidity.formatter": "prettier", 6 | "typescript.tsdk": "node_modules/typescript/lib" 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hardhat Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Hardhat][hardhat-badge]][hardhat] [![License: MIT][license-badge]][license] 2 | 3 | [gitpod]: https://gitpod.io/#https://github.com/paulrberg/hardhat-template 4 | [gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod 5 | [gha]: https://github.com/paulrberg/hardhat-template/actions 6 | [gha-badge]: https://github.com/paulrberg/hardhat-template/actions/workflows/ci.yml/badge.svg 7 | [hardhat]: https://hardhat.org/ 8 | [hardhat-badge]: https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg 9 | [license]: https://opensource.org/licenses/MIT 10 | [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg 11 | 12 | A Hardhat-based template for developing Solidity smart contracts, with sensible defaults. 13 | 14 | - [Hardhat](https://github.com/nomiclabs/hardhat): compile, run and test smart contracts 15 | - [TypeChain](https://github.com/ethereum-ts/TypeChain): generate TypeScript bindings for smart contracts 16 | - [Ethers](https://github.com/ethers-io/ethers.js/): renowned Ethereum library and wallet implementation 17 | - [Solhint](https://github.com/protofire/solhint): code linter 18 | - [Solcover](https://github.com/sc-forks/solidity-coverage): code coverage 19 | - [Prettier Plugin Solidity](https://github.com/prettier-solidity/prettier-plugin-solidity): code formatter 20 | 21 | ## Getting Started 22 | 23 | Click the [`Use this template`](https://github.com/paulrberg/hardhat-template/generate) button at the top of the page to 24 | create a new repository with this repo as the initial state. 25 | 26 | ## Features 27 | 28 | This template builds upon the frameworks and libraries mentioned above, so for details about their specific features, 29 | please consult their respective documentations. 30 | 31 | For example, for Hardhat, you can refer to the [Hardhat Tutorial](https://hardhat.org/tutorial) and the 32 | [Hardhat Docs](https://hardhat.org/docs). You might be in particular interested in reading the 33 | [Testing Contracts](https://hardhat.org/tutorial/testing-contracts) section. 34 | 35 | ### Sensible Defaults 36 | 37 | This template comes with sensible default configurations in the following files: 38 | 39 | ```text 40 | ├── .editorconfig 41 | ├── .eslintignore 42 | ├── .eslintrc.yml 43 | ├── .gitignore 44 | ├── .prettierignore 45 | ├── .prettierrc.yml 46 | ├── .solcover.js 47 | ├── .solhint.json 48 | └── hardhat.config.ts 49 | ``` 50 | 51 | ### VSCode Integration 52 | 53 | This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic 54 | Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity). 55 | 56 | ### GitHub Actions 57 | 58 | This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull 59 | request made to the `main` branch. 60 | 61 | Note though that to make this work, you must use your `INFURA_API_KEY` and your `MNEMONIC` as GitHub secrets. 62 | 63 | For more information on how to set up GitHub secrets, check out the 64 | [docs](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions). 65 | 66 | You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). 67 | 68 | ## Usage 69 | 70 | ### Pre Requisites 71 | 72 | First, you need to install the dependencies: 73 | 74 | ```sh 75 | bun install 76 | ``` 77 | 78 | Then, you need to set up all the required 79 | [Hardhat Configuration Variables](https://hardhat.org/hardhat-runner/docs/guides/configuration-variables). You might 80 | also want to install some that are optional. 81 | 82 | To assist with the setup process, run `bunx hardhat vars setup`. To set a particular value, such as a BIP-39 mnemonic 83 | variable, execute this: 84 | 85 | ```sh 86 | bunx hardhat vars set MNEMONIC 87 | ? Enter value: ‣ here is where your twelve words mnemonic should be put my friend 88 | ``` 89 | 90 | If you do not already have a mnemonic, you can generate one using this [website](https://iancoleman.io/bip39/). 91 | 92 | ### Compile 93 | 94 | Compile the smart contracts with Hardhat: 95 | 96 | ```sh 97 | bun run compile 98 | ``` 99 | 100 | ### TypeChain 101 | 102 | Compile the smart contracts and generate TypeChain bindings: 103 | 104 | ```sh 105 | bun run typechain 106 | ``` 107 | 108 | ### Test 109 | 110 | Run the tests with Hardhat: 111 | 112 | ```sh 113 | bun run test 114 | ``` 115 | 116 | ### Lint Solidity 117 | 118 | Lint the Solidity code: 119 | 120 | ```sh 121 | bun run lint:sol 122 | ``` 123 | 124 | ### Lint TypeScript 125 | 126 | Lint the TypeScript code: 127 | 128 | ```sh 129 | bun run lint:ts 130 | ``` 131 | 132 | ### Coverage 133 | 134 | Generate the code coverage report: 135 | 136 | ```sh 137 | bun run coverage 138 | ``` 139 | 140 | ### Report Gas 141 | 142 | See the gas usage per unit test and average gas per method call: 143 | 144 | ```sh 145 | REPORT_GAS=true bun run test 146 | ``` 147 | 148 | ### Clean 149 | 150 | Delete the smart contract artifacts, the coverage reports and the Hardhat cache: 151 | 152 | ```sh 153 | bun run clean 154 | ``` 155 | 156 | ### Deploy 157 | 158 | Deploy the contracts to Hardhat Network: 159 | 160 | ```sh 161 | bun run deploy:contracts 162 | ``` 163 | 164 | ### Tasks 165 | 166 | #### Deploy Lock 167 | 168 | Deploy a new instance of the Lock contract via a task: 169 | 170 | ```sh 171 | bun run task:deployLock --unlock 100 --value 0.1 172 | ``` 173 | 174 | ### Syntax Highlighting 175 | 176 | If you use VSCode, you can get Solidity syntax highlighting with the 177 | [hardhat-solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) extension. 178 | 179 | ## Using GitPod 180 | 181 | [GitPod](https://www.gitpod.io/) is an open-source developer platform for remote development. 182 | 183 | To view the coverage report generated by `bun run coverage`, just click `Go Live` from the status bar to turn the server 184 | on/off. 185 | 186 | ## Local development with Ganache 187 | 188 | ### Install Ganache 189 | 190 | ```sh 191 | npm i -g ganache 192 | ``` 193 | 194 | ### Run a Development Blockchain 195 | 196 | ```sh 197 | ganache -s test 198 | ``` 199 | 200 | > The `-s test` passes a seed to the local chain and makes it deterministic 201 | 202 | Make sure to set the mnemonic in your `.env` file to that of the instance running with Ganache. 203 | 204 | ## License 205 | 206 | This project is licensed under MIT. 207 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PaulRBerg/hardhat-template/2942426516d22878d2a88d38daae9ab7522b4064/bun.lockb -------------------------------------------------------------------------------- /contracts/Lock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.9; 3 | 4 | error InvalidUnlockTime(uint256 unlockTime); 5 | error NotOwner(address owner); 6 | error UnlockTimeNotReached(uint256 unlockTime); 7 | 8 | contract Lock { 9 | uint256 public unlockTime; 10 | address payable public owner; 11 | 12 | event Withdrawal(uint256 amount, uint256 when); 13 | 14 | constructor(uint256 _unlockTime) payable { 15 | if (block.timestamp >= _unlockTime) { 16 | revert InvalidUnlockTime(_unlockTime); 17 | } 18 | 19 | unlockTime = _unlockTime; 20 | owner = payable(msg.sender); 21 | } 22 | 23 | function withdraw() public { 24 | if (block.timestamp < unlockTime) { 25 | revert UnlockTimeNotReached(unlockTime); 26 | } 27 | 28 | if (msg.sender != owner) { 29 | revert NotOwner(owner); 30 | } 31 | 32 | emit Withdrawal(address(this).balance, block.timestamp); 33 | 34 | owner.transfer(address(this).balance); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /deploy/deploy.ts: -------------------------------------------------------------------------------- 1 | import { DeployFunction } from "hardhat-deploy/types"; 2 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 3 | 4 | const DAY_IN_SECONDS = 60 * 60 * 24; 5 | const NOW_IN_SECONDS = Math.round(Date.now() / 1000); 6 | const UNLOCK_IN_X_DAYS = NOW_IN_SECONDS + DAY_IN_SECONDS * 1; // 1 DAY 7 | 8 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 9 | const { deployer } = await hre.getNamedAccounts(); 10 | const { deploy } = hre.deployments; 11 | const lockedAmount = hre.ethers.parseEther("0.01").toString(); 12 | 13 | const lock = await deploy("Lock", { 14 | from: deployer, 15 | args: [UNLOCK_IN_X_DAYS], 16 | log: true, 17 | value: lockedAmount, 18 | }); 19 | 20 | console.log(`Lock contract: `, lock.address); 21 | }; 22 | export default func; 23 | func.id = "deploy_lock"; // id required to prevent reexecution 24 | func.tags = ["Lock"]; 25 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@nomicfoundation/hardhat-toolbox"; 2 | import "hardhat-deploy"; 3 | import type { HardhatUserConfig } from "hardhat/config"; 4 | import { vars } from "hardhat/config"; 5 | import type { NetworkUserConfig } from "hardhat/types"; 6 | 7 | import "./tasks/accounts"; 8 | import "./tasks/lock"; 9 | 10 | // Run 'npx hardhat vars setup' to see the list of variables that need to be set 11 | 12 | const mnemonic: string = vars.get("MNEMONIC"); 13 | const infuraApiKey: string = vars.get("INFURA_API_KEY"); 14 | 15 | const chainIds = { 16 | "arbitrum-mainnet": 42161, 17 | avalanche: 43114, 18 | bsc: 56, 19 | ganache: 1337, 20 | hardhat: 31337, 21 | mainnet: 1, 22 | "optimism-mainnet": 10, 23 | "polygon-mainnet": 137, 24 | "polygon-mumbai": 80001, 25 | sepolia: 11155111, 26 | }; 27 | 28 | function getChainConfig(chain: keyof typeof chainIds): NetworkUserConfig { 29 | let jsonRpcUrl: string; 30 | switch (chain) { 31 | case "avalanche": 32 | jsonRpcUrl = "https://api.avax.network/ext/bc/C/rpc"; 33 | break; 34 | case "bsc": 35 | jsonRpcUrl = "https://bsc-dataseed1.binance.org"; 36 | break; 37 | default: 38 | jsonRpcUrl = "https://" + chain + ".infura.io/v3/" + infuraApiKey; 39 | } 40 | return { 41 | accounts: { 42 | count: 10, 43 | mnemonic, 44 | path: "m/44'/60'/0'/0", 45 | }, 46 | chainId: chainIds[chain], 47 | url: jsonRpcUrl, 48 | }; 49 | } 50 | 51 | const config: HardhatUserConfig = { 52 | defaultNetwork: "hardhat", 53 | namedAccounts: { 54 | deployer: 0, 55 | }, 56 | etherscan: { 57 | apiKey: { 58 | arbitrumOne: vars.get("ARBISCAN_API_KEY", ""), 59 | avalanche: vars.get("SNOWTRACE_API_KEY", ""), 60 | bsc: vars.get("BSCSCAN_API_KEY", ""), 61 | mainnet: vars.get("ETHERSCAN_API_KEY", ""), 62 | optimisticEthereum: vars.get("OPTIMISM_API_KEY", ""), 63 | polygon: vars.get("POLYGONSCAN_API_KEY", ""), 64 | polygonMumbai: vars.get("POLYGONSCAN_API_KEY", ""), 65 | sepolia: vars.get("ETHERSCAN_API_KEY", ""), 66 | }, 67 | }, 68 | gasReporter: { 69 | currency: "USD", 70 | enabled: process.env.REPORT_GAS ? true : false, 71 | excludeContracts: [], 72 | src: "./contracts", 73 | }, 74 | networks: { 75 | hardhat: { 76 | accounts: { 77 | mnemonic, 78 | }, 79 | chainId: chainIds.hardhat, 80 | }, 81 | ganache: { 82 | accounts: { 83 | mnemonic, 84 | }, 85 | chainId: chainIds.ganache, 86 | url: "http://localhost:8545", 87 | }, 88 | arbitrum: getChainConfig("arbitrum-mainnet"), 89 | avalanche: getChainConfig("avalanche"), 90 | bsc: getChainConfig("bsc"), 91 | mainnet: getChainConfig("mainnet"), 92 | optimism: getChainConfig("optimism-mainnet"), 93 | "polygon-mainnet": getChainConfig("polygon-mainnet"), 94 | "polygon-mumbai": getChainConfig("polygon-mumbai"), 95 | sepolia: getChainConfig("sepolia"), 96 | }, 97 | paths: { 98 | artifacts: "./artifacts", 99 | cache: "./cache", 100 | sources: "./contracts", 101 | tests: "./test", 102 | }, 103 | solidity: { 104 | version: "0.8.19", 105 | settings: { 106 | metadata: { 107 | // Not including the metadata hash 108 | // https://github.com/paulrberg/hardhat-template/issues/31 109 | bytecodeHash: "none", 110 | }, 111 | // Disable the optimizer when debugging 112 | // https://hardhat.org/hardhat-network/#solidity-optimizer-support 113 | optimizer: { 114 | enabled: true, 115 | runs: 800, 116 | }, 117 | }, 118 | }, 119 | typechain: { 120 | outDir: "types", 121 | target: "ethers-v6", 122 | }, 123 | }; 124 | 125 | export default config; 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prb/hardhat-template", 3 | "description": "Hardhat-based template for developing Solidity smart contracts", 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "Paul Razvan Berg", 7 | "url": "https://github.com/PaulRBerg" 8 | }, 9 | "devDependencies": { 10 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", 11 | "@nomicfoundation/hardhat-ethers": "^3.0.5", 12 | "@nomicfoundation/hardhat-network-helpers": "^1.0.10", 13 | "@nomicfoundation/hardhat-toolbox": "^4.0.0", 14 | "@nomicfoundation/hardhat-verify": "^2.0.2", 15 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 16 | "@typechain/ethers-v6": "^0.5.1", 17 | "@typechain/hardhat": "^9.1.0", 18 | "@types/chai": "^4.3.11", 19 | "@types/fs-extra": "^11.0.4", 20 | "@types/mocha": "^10.0.6", 21 | "@types/node": "^20.10.4", 22 | "@typescript-eslint/eslint-plugin": "^6.14.0", 23 | "@typescript-eslint/parser": "^6.14.0", 24 | "chai": "^4.3.10", 25 | "cross-env": "^7.0.3", 26 | "eslint": "^8.56.0", 27 | "eslint-config-prettier": "^9.1.0", 28 | "ethers": "^6.9.0", 29 | "fs-extra": "^11.2.0", 30 | "hardhat": "^2.19.2", 31 | "hardhat-deploy": "^0.12.1", 32 | "hardhat-gas-reporter": "^1.0.9", 33 | "lodash": "^4.17.21", 34 | "mocha": "^10.2.0", 35 | "prettier": "^3.1.1", 36 | "prettier-plugin-solidity": "^1.2.0", 37 | "rimraf": "^5.0.5", 38 | "solhint": "^4.0.0", 39 | "solhint-plugin-prettier": "^0.1.0", 40 | "solidity-coverage": "^0.8.5", 41 | "ts-generator": "^0.1.1", 42 | "ts-node": "^10.9.2", 43 | "typechain": "^8.3.2", 44 | "typescript": "^5.3.3" 45 | }, 46 | "files": [ 47 | "contracts" 48 | ], 49 | "keywords": [ 50 | "blockchain", 51 | "ethers", 52 | "ethereum", 53 | "hardhat", 54 | "smart-contracts", 55 | "solidity", 56 | "template", 57 | "typescript", 58 | "typechain" 59 | ], 60 | "publishConfig": { 61 | "access": "public" 62 | }, 63 | "scripts": { 64 | "clean": "rimraf ./artifacts ./cache ./coverage ./types ./coverage.json && bun run typechain", 65 | "compile": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat compile", 66 | "coverage": "hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"test/**/*.ts\" && bun run typechain", 67 | "deploy:contracts": "hardhat deploy", 68 | "lint": "bun run lint:sol && bun run lint:ts && bun run prettier:check", 69 | "lint:sol": "solhint --max-warnings 0 \"contracts/**/*.sol\"", 70 | "lint:ts": "eslint --ignore-path ./.eslintignore --ext .js,.ts .", 71 | "postcompile": "bun run typechain", 72 | "prettier:check": "prettier --check \"**/*.{js,json,md,sol,ts,yml}\"", 73 | "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", 74 | "task:deployLock": "hardhat task:deployLock", 75 | "task:withdraw": "hardhat task:withdraw", 76 | "test": "hardhat test", 77 | "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tasks/accounts.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => { 4 | const accounts = await hre.ethers.getSigners(); 5 | 6 | for (const account of accounts) { 7 | console.log(account.address); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /tasks/lock.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | import type { TaskArguments } from "hardhat/types"; 3 | 4 | function distance(past: number, future: number): string { 5 | // get total seconds between the times 6 | let delta = future - past; 7 | 8 | // calculate (and subtract) whole days 9 | const days = Math.floor(delta / 86400); 10 | delta -= days * 86400; 11 | 12 | // calculate (and subtract) whole hours 13 | const hours = Math.floor(delta / 3600) % 24; 14 | delta -= hours * 3600; 15 | 16 | // calculate (and subtract) whole minutes 17 | const minutes = Math.floor(delta / 60) % 60; 18 | delta -= minutes * 60; 19 | 20 | // what's left is seconds 21 | const seconds = delta % 60; // in theory the modulus is not required 22 | 23 | return `${days} day(s), ${hours} hour(s), ${minutes} minute(s) and ${seconds} second(s)`; 24 | } 25 | 26 | task("task:withdraw", "Calls the withdraw function of Lock Contract") 27 | .addOptionalParam("address", "Optionally specify the Lock address to withdraw") 28 | .addParam("account", "Specify which account [0, 9]") 29 | .setAction(async function (taskArguments: TaskArguments, hre) { 30 | const { ethers, deployments } = hre; 31 | 32 | const Lock = taskArguments.address ? { address: taskArguments.address } : await deployments.get("Lock"); 33 | 34 | const signers = await ethers.getSigners(); 35 | console.log(taskArguments.address); 36 | 37 | const lock = await ethers.getContractAt("Lock", Lock.address); 38 | 39 | const initialBalance = await ethers.provider.getBalance(Lock.address); 40 | await lock.connect(signers[taskArguments.account]).withdraw(); 41 | const finalBalance = await ethers.provider.getBalance(Lock.address); 42 | 43 | console.log("Contract balance before withdraw", ethers.formatEther(initialBalance)); 44 | console.log("Contract balance after withdraw", ethers.formatEther(finalBalance)); 45 | 46 | console.log("Lock Withdraw Success"); 47 | }); 48 | 49 | task("task:deployLock", "Deploys Lock Contract") 50 | .addParam("unlock", "When to unlock funds in seconds (number of seconds into the futrue)") 51 | .addParam("value", "How much ether you intend locking (in ether not wei, e.g., 0.1)") 52 | .setAction(async function (taskArguments: TaskArguments, { ethers }) { 53 | const NOW_IN_SECONDS = Math.round(Date.now() / 1000); 54 | 55 | const signers = await ethers.getSigners(); 56 | const lockedAmount = ethers.parseEther(taskArguments.value); 57 | const unlockTime = NOW_IN_SECONDS + parseInt(taskArguments.unlock); 58 | const lockFactory = await ethers.getContractFactory("Lock"); 59 | console.log(`Deploying Lock and locking ${taskArguments.value} ETH for ${distance(NOW_IN_SECONDS, unlockTime)}`); 60 | const lock = await lockFactory.connect(signers[0]).deploy(unlockTime, { value: lockedAmount }); 61 | await lock.waitForDeployment(); 62 | console.log("Lock deployed to: ", await lock.getAddress()); 63 | }); 64 | -------------------------------------------------------------------------------- /test/lock/Lock.fixture.ts: -------------------------------------------------------------------------------- 1 | import { time } from "@nomicfoundation/hardhat-network-helpers"; 2 | import { ethers } from "hardhat"; 3 | 4 | import type { Lock } from "../../types/Lock"; 5 | import type { Lock__factory } from "../../types/factories/Lock__factory"; 6 | 7 | export async function deployLockFixture() { 8 | const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; 9 | const ONE_GWEI = 1_000_000_000; 10 | 11 | const lockedAmount = ONE_GWEI; 12 | const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; 13 | 14 | // Contracts are deployed using the first signer/account by default 15 | const [owner, otherAccount] = await ethers.getSigners(); 16 | 17 | const Lock = (await ethers.getContractFactory("Lock")) as Lock__factory; 18 | const lock = (await Lock.deploy(unlockTime, { value: lockedAmount })) as Lock; 19 | const lock_address = await lock.getAddress(); 20 | 21 | return { lock, lock_address, unlockTime, lockedAmount, owner, otherAccount }; 22 | } 23 | -------------------------------------------------------------------------------- /test/lock/Lock.ts: -------------------------------------------------------------------------------- 1 | import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs"; 2 | import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; 3 | import { expect } from "chai"; 4 | import { ethers } from "hardhat"; 5 | 6 | import type { Signers } from "../types"; 7 | import { deployLockFixture } from "./Lock.fixture"; 8 | 9 | describe("Lock", function () { 10 | before(async function () { 11 | this.signers = {} as Signers; 12 | 13 | const signers = await ethers.getSigners(); 14 | this.signers.admin = signers[0]; 15 | 16 | this.loadFixture = loadFixture; 17 | }); 18 | 19 | describe("Deployment", function () { 20 | beforeEach(async function () { 21 | const { lock, lock_address, unlockTime, owner, lockedAmount } = await this.loadFixture(deployLockFixture); 22 | this.lock = lock; 23 | this.lock_address = lock_address; 24 | this.unlockTime = unlockTime; 25 | this.owner = owner; 26 | this.lockedAmount = lockedAmount; 27 | }); 28 | 29 | it("Should fail if the unlockTime is not in the future", async function () { 30 | // We don't use the fixture here because we want a different deployment 31 | const latestTime = await time.latest(); 32 | const Lock = await ethers.getContractFactory("Lock"); 33 | await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWithCustomError(Lock, "InvalidUnlockTime"); 34 | }); 35 | 36 | it("Should set the right unlockTime", async function () { 37 | expect(await this.lock.unlockTime()).to.equal(this.unlockTime); 38 | }); 39 | 40 | it("Should set the right owner", async function () { 41 | expect(await this.lock.owner()).to.equal(this.owner.address); 42 | }); 43 | 44 | it("Should receive and store the funds to lock", async function () { 45 | expect(await ethers.provider.getBalance(this.lock_address)).to.equal(this.lockedAmount); 46 | }); 47 | }); 48 | 49 | describe("Withdrawals", function () { 50 | beforeEach(async function () { 51 | const { lock, unlockTime, owner, lockedAmount, otherAccount } = await this.loadFixture(deployLockFixture); 52 | this.lock = lock; 53 | this.unlockTime = unlockTime; 54 | this.owner = owner; 55 | this.lockedAmount = lockedAmount; 56 | this.otherAccount = otherAccount; 57 | }); 58 | 59 | describe("Validations", function () { 60 | it("Should revert with the right error if called too soon", async function () { 61 | await expect(this.lock.withdraw()).to.be.revertedWithCustomError(this.lock, "UnlockTimeNotReached"); 62 | }); 63 | 64 | it("Should revert with the right error if called from another account", async function () { 65 | // We can increase the time in Hardhat Network 66 | await time.increaseTo(this.unlockTime); 67 | 68 | // We use lock.connect() to send a transaction from another account 69 | await expect(this.lock.connect(this.otherAccount).withdraw()).to.be.revertedWithCustomError( 70 | this.lock, 71 | "NotOwner", 72 | ); 73 | }); 74 | 75 | it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { 76 | // Transactions are sent using the first signer by default 77 | await time.increaseTo(this.unlockTime); 78 | 79 | await expect(this.lock.withdraw()).not.to.be.reverted; 80 | }); 81 | }); 82 | 83 | describe("Events", function () { 84 | it("Should emit an event on withdrawals", async function () { 85 | await time.increaseTo(this.unlockTime); 86 | 87 | await expect(this.lock.withdraw()).to.emit(this.lock, "Withdrawal").withArgs(this.lockedAmount, anyValue); // We accept any value as `when` arg 88 | }); 89 | }); 90 | 91 | describe("Transfers", function () { 92 | it("Should transfer the funds to the owner", async function () { 93 | await time.increaseTo(this.unlockTime); 94 | 95 | await expect(this.lock.withdraw()).to.changeEtherBalances( 96 | [this.owner, this.lock], 97 | [this.lockedAmount, -this.lockedAmount], 98 | ); 99 | }); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import type { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/dist/src/signer-with-address"; 2 | 3 | import type { Lock } from "../types/Lock"; 4 | 5 | type Fixture = () => Promise; 6 | 7 | declare module "mocha" { 8 | export interface Context { 9 | lock: Lock; 10 | loadFixture: (fixture: Fixture) => Promise; 11 | signers: Signers; 12 | } 13 | } 14 | 15 | export interface Signers { 16 | admin: SignerWithAddress; 17 | } 18 | -------------------------------------------------------------------------------- /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": ["es2020"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noImplicitAny": true, 13 | "removeComments": true, 14 | "resolveJsonModule": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "es2020" 18 | }, 19 | "exclude": ["node_modules"], 20 | "files": ["./hardhat.config.ts"], 21 | "include": ["src/**/*", "tasks/**/*", "test/**/*", "deploy/**/*", "types/"] 22 | } 23 | --------------------------------------------------------------------------------