├── .editorconfig ├── .env.example ├── .gitattributes ├── .github ├── FUNDING.yml ├── scripts │ └── rename.sh └── workflows │ ├── ci.yml │ └── create.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc.yml ├── .solhint.json ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── foundry.toml ├── package.json ├── pnpm-lock.yaml ├── remappings.txt ├── script ├── Base.s.sol └── Deploy.s.sol ├── src ├── UniswapHooks.sol └── UniswapHooksFactory.sol └── test └── UniswapHooks.t.sol /.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 | 18 | [*.tree] 19 | indent_size = 1 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" 2 | export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN" 3 | export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN" 4 | export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" 5 | export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN" 6 | export API_KEY_INFURA="YOUR_API_KEY_INFURA" 7 | export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN" 8 | export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" 9 | export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE" 10 | export MNEMONIC="YOUR_MNEMONIC" 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | lib/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: "https://gitcoin.co/grants/1657/PaulRBerg-open-source-engineering" 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 | API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }} 5 | API_KEY_ETHERSCAN: ${{ secrets.API_KEY_ETHERSCAN }} 6 | API_KEY_INFURA: ${{ secrets.API_KEY_INFURA }} 7 | FOUNDRY_PROFILE: "ci" 8 | MNEMONIC: ${{ secrets.MNEMONIC }} 9 | 10 | on: 11 | workflow_dispatch: 12 | pull_request: 13 | push: 14 | branches: 15 | - "main" 16 | 17 | jobs: 18 | lint: 19 | runs-on: "ubuntu-latest" 20 | steps: 21 | - name: "Check out the repo" 22 | uses: "actions/checkout@v3" 23 | with: 24 | submodules: "recursive" 25 | 26 | - name: "Install Foundry" 27 | uses: "foundry-rs/foundry-toolchain@v1" 28 | 29 | - name: "Install Pnpm" 30 | uses: "pnpm/action-setup@v2" 31 | with: 32 | version: "8" 33 | 34 | - name: "Install Node.js" 35 | uses: "actions/setup-node@v3" 36 | with: 37 | cache: "pnpm" 38 | node-version: "lts/*" 39 | 40 | - name: "Install the Node.js dependencies" 41 | run: "pnpm install" 42 | 43 | - name: "Lint the contracts" 44 | run: "pnpm lint" 45 | 46 | - name: "Add lint summary" 47 | run: | 48 | echo "## Lint result" >> $GITHUB_STEP_SUMMARY 49 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 50 | 51 | build: 52 | runs-on: "ubuntu-latest" 53 | steps: 54 | - name: "Check out the repo" 55 | uses: "actions/checkout@v3" 56 | with: 57 | submodules: "recursive" 58 | 59 | - name: "Install Foundry" 60 | uses: "foundry-rs/foundry-toolchain@v1" 61 | 62 | - name: "Build the contracts and print their size" 63 | run: "forge build --sizes" 64 | 65 | - name: "Add build summary" 66 | run: | 67 | echo "## Build result" >> $GITHUB_STEP_SUMMARY 68 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 69 | 70 | test: 71 | needs: ["lint", "build"] 72 | runs-on: "ubuntu-latest" 73 | steps: 74 | - name: "Check out the repo" 75 | uses: "actions/checkout@v3" 76 | with: 77 | submodules: "recursive" 78 | 79 | - name: "Install Foundry" 80 | uses: "foundry-rs/foundry-toolchain@v1" 81 | 82 | - name: "Show the Foundry config" 83 | run: "forge config" 84 | 85 | - name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance" 86 | run: > 87 | echo "FOUNDRY_FUZZ_SEED=$( 88 | echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) 89 | )" >> $GITHUB_ENV 90 | 91 | - name: "Run the tests" 92 | run: "forge test" 93 | 94 | - name: "Add test summary" 95 | run: | 96 | echo "## Tests result" >> $GITHUB_STEP_SUMMARY 97 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 98 | -------------------------------------------------------------------------------- /.github/workflows/create.yml: -------------------------------------------------------------------------------- 1 | name: "Create" 2 | 3 | # The workflow will run only when the "Use this template" button is used 4 | on: 5 | create: 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@v3" 18 | 19 | - name: "Update package.json" 20 | env: 21 | GITHUB_REPOSITORY_DESCRIPTION: ${{ github.event.repository.description }} 22 | run: 23 | ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" 24 | 25 | - name: "Add rename summary" 26 | run: | 27 | echo "## Commit result" >> $GITHUB_STEP_SUMMARY 28 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 29 | 30 | - name: "Remove files not needed in the user's copy of the template" 31 | run: | 32 | rm -f "./.github/FUNDING.yml" 33 | rm -f "./.github/scripts/rename.sh" 34 | rm -f "./.github/workflows/create.yml" 35 | 36 | - name: "Add remove summary" 37 | run: | 38 | echo "## Remove result" >> $GITHUB_STEP_SUMMARY 39 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 40 | 41 | - name: "Update commit" 42 | uses: "stefanzweifel/git-auto-commit-action@v4" 43 | with: 44 | commit_message: "feat: initial commit" 45 | commit_options: "--amend" 46 | push_options: "--force" 47 | skip_fetch: true 48 | 49 | - name: "Add commit summary" 50 | run: | 51 | echo "## Commit result" >> $GITHUB_STEP_SUMMARY 52 | echo "✅ Passed" >> $GITHUB_STEP_SUMMARY 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | cache 3 | node_modules 4 | out 5 | 6 | # files 7 | *.env 8 | *.log 9 | .DS_Store 10 | .pnp.* 11 | lcov.info 12 | yarn.lock 13 | 14 | # broadcasts 15 | !broadcast 16 | broadcast/* 17 | broadcast/*/31337/ 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | branch = "v1" 3 | path = "lib/forge-std" 4 | url = "https://github.com/foundry-rs/forge-std" 5 | [submodule "lib/prb-test"] 6 | branch = "release-v0" 7 | path = "lib/prb-test" 8 | url = "https://github.com/PaulRBerg/prb-test" 9 | [submodule "lib/v4-periphery"] 10 | path = lib/v4-periphery 11 | url = https://github.com/Uniswap/v4-periphery 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | broadcast 3 | cache 4 | lib 5 | node_modules 6 | out 7 | 8 | # files 9 | *.env 10 | *.log 11 | .DS_Store 12 | .pnp.* 13 | lcov.info 14 | package-lock.json 15 | pnpm-lock.yaml 16 | yarn.lock 17 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | bracketSpacing: true 2 | printWidth: 120 3 | proseWrap: "always" 4 | singleQuote: false 5 | tabWidth: 2 6 | trailingComma: "all" 7 | useTabs: false 8 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "code-complexity": ["error", 8], 5 | "compiler-version": ["error", ">=0.8.19"], 6 | "func-name-mixedcase": "off", 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 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[solidity]": { 3 | "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" 4 | }, 5 | "[toml]": { 6 | "editor.defaultFormatter": "tamasfe.even-better-toml" 7 | }, 8 | "npm.exclude": "**/lib/**", 9 | "solidity.formatter": "forge" 10 | } 11 | -------------------------------------------------------------------------------- /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 | # Foundry Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license] 2 | 3 | [gitpod]: https://gitpod.io/#https://github.com/soliditylabs/uniswap-v4-custom-pool 4 | [gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod 5 | [gha]: https://github.com/soliditylabs/uniswap-v4-custom-pool/actions 6 | [gha-badge]: https://github.com/soliditylabs/uniswap-v4-custom-pool/actions/workflows/ci.yml/badge.svg 7 | [foundry]: https://getfoundry.sh/ 8 | [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg 9 | [license]: https://opensource.org/licenses/MIT 10 | [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg 11 | 12 | Uniswap v4 custom pool template for Foundry based on [foundry-template](https://github.com/PaulRBerg/foundry-template). 13 | 14 | Full blog post with more details about Uniswap 4 and hooks available at 15 | [https://soliditydeveloper.com/uniswap4](https://soliditydeveloper.com/uniswap4). 16 | 17 | An example for the flow of hooks is shown below: 18 | 19 | image 20 | 21 | The flags for which hooks will be executed depends the leading bits on the Hooks address: 22 | 23 | ![UniHookBits](https://github.com/soliditylabs/uniswap-v4-custom-pool/assets/659390/33ecc26a-67da-4d4c-accc-0255ea7e41df) 24 | 25 | ## What's Inside 26 | 27 | - [Uniswap v4](https://github.com/Uniswap/v4-core): Examples showing how to create custom hooks for Uniswap v4 28 | - [Foundry-Template](https://github.com/PaulRBerg/foundry-template): Foundry-based template for developing Solidity 29 | smart contracts by Paul Berg 30 | - [Forge](https://github.com/foundry-rs/foundry/blob/master/forge): compile, test, fuzz, format, and deploy smart 31 | contracts 32 | - [Forge Std](https://github.com/foundry-rs/forge-std): collection of helpful contracts and cheat codes for testing 33 | - [PRBTest](https://github.com/PaulRBerg/prb-test): modern collection of testing assertions and logging utilities 34 | - [Prettier](https://github.com/prettier/prettier): code formatter for non-Solidity files 35 | - [Solhint Community](https://github.com/solhint-community/solhint-community): linter for Solidity code 36 | 37 | ## Getting Started 38 | 39 | Click the [`Use this template`](https://github.com/soliditylabs/uniswap-v4-custom-pool/generate) button at the top of 40 | the page to create a new repository with this repo as the initial state. 41 | 42 | Or, if you prefer to install the template manually: 43 | 44 | ```sh 45 | forge init my-project --template https://github.com/soliditylabs/uniswap-v4-custom-pool 46 | cd my-project 47 | pnpm install # install Solhint, Prettier, and other Node.js deps 48 | ``` 49 | 50 | If this is your first time with Foundry, check out the 51 | [installation](https://github.com/foundry-rs/foundry#installation) instructions. 52 | 53 | ## Features 54 | 55 | This template builds upon the frameworks and libraries mentioned above, so for details about their specific features, 56 | please consult their respective documentation. 57 | 58 | For example, if you're interested in exploring Foundry in more detail, you should look at the 59 | [Foundry Book](https://book.getfoundry.sh/). In particular, you may be interested in reading the 60 | [Writing Tests](https://book.getfoundry.sh/forge/writing-tests.html) tutorial. 61 | 62 | ### Sensible Defaults 63 | 64 | This template comes with a set of sensible default configurations for you to use. These defaults can be found in the 65 | following files: 66 | 67 | ```text 68 | ├── .editorconfig 69 | ├── .gitignore 70 | ├── .prettierignore 71 | ├── .prettierrc.yml 72 | ├── .solhint.json 73 | ├── foundry.toml 74 | └── remappings.txt 75 | ``` 76 | 77 | ### VSCode Integration 78 | 79 | This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic 80 | Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity). 81 | 82 | For guidance on how to integrate a Foundry project in VSCode, please refer to this 83 | [guide](https://book.getfoundry.sh/config/vscode). 84 | 85 | ### GitHub Actions 86 | 87 | This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull 88 | request made to the `main` branch. 89 | 90 | You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). 91 | 92 | ## Writing Tests 93 | 94 | To write a new test contract, you start by importing [PRBTest](https://github.com/PaulRBerg/prb-test) and inherit from 95 | it in your test contract. PRBTest comes with a pre-instantiated [cheatcodes](https://book.getfoundry.sh/cheatcodes/) 96 | environment accessible via the `vm` property. If you would like to view the logs in the terminal output you can add the 97 | `-vvv` flag and use [console.log](https://book.getfoundry.sh/faq?highlight=console.log#how-do-i-use-consolelog). 98 | 99 | This template comes with an example test contract [Foo.t.sol](./test/Foo.t.sol) 100 | 101 | ## Usage 102 | 103 | This is a list of the most frequently needed commands. 104 | 105 | ### Build 106 | 107 | Build the contracts: 108 | 109 | ```sh 110 | $ forge build 111 | ``` 112 | 113 | ### Clean 114 | 115 | Delete the build artifacts and cache directories: 116 | 117 | ```sh 118 | $ forge clean 119 | ``` 120 | 121 | ### Compile 122 | 123 | Compile the contracts: 124 | 125 | ```sh 126 | $ forge build 127 | ``` 128 | 129 | ### Coverage 130 | 131 | Get a test coverage report: 132 | 133 | ```sh 134 | $ forge coverage 135 | ``` 136 | 137 | ### Deploy 138 | 139 | Deploy to Anvil: 140 | 141 | ```sh 142 | $ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545 143 | ``` 144 | 145 | For this script to work, you need to have a `MNEMONIC` environment variable set to a valid 146 | [BIP39 mnemonic](https://iancoleman.io/bip39/). 147 | 148 | For instructions on how to deploy to a testnet or mainnet, check out the 149 | [Solidity Scripting](https://book.getfoundry.sh/tutorials/solidity-scripting.html) tutorial. 150 | 151 | ### Format 152 | 153 | Format the contracts: 154 | 155 | ```sh 156 | $ forge fmt 157 | ``` 158 | 159 | ### Gas Usage 160 | 161 | Get a gas report: 162 | 163 | ```sh 164 | $ forge test --gas-report 165 | ``` 166 | 167 | ### Lint 168 | 169 | Lint the contracts: 170 | 171 | ```sh 172 | $ pnpm lint 173 | ``` 174 | 175 | ### Test 176 | 177 | Run the tests: 178 | 179 | ```sh 180 | $ forge test 181 | ``` 182 | 183 | ## Notes 184 | 185 | 1. Foundry uses [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) to manage dependencies. For 186 | detailed instructions on working with dependencies, please refer to the 187 | [guide](https://book.getfoundry.sh/projects/dependencies.html) in the book 188 | 2. You don't have to create a `.env` file, but filling in the environment variables may be useful when debugging and 189 | testing against a fork. 190 | 191 | ## Related Efforts 192 | 193 | - [abigger87/femplate](https://github.com/abigger87/femplate) 194 | - [cleanunicorn/ethereum-smartcontract-template](https://github.com/cleanunicorn/ethereum-smartcontract-template) 195 | - [foundry-rs/forge-template](https://github.com/foundry-rs/forge-template) 196 | - [FrankieIsLost/forge-template](https://github.com/FrankieIsLost/forge-template) 197 | 198 | ## License 199 | 200 | This project is licensed under MIT. 201 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # Full reference https://github.com/foundry-rs/foundry/tree/master/config 2 | 3 | [profile.default] 4 | auto_detect_solc = false 5 | block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT 6 | bytecode_hash = "none" 7 | cbor_metadata = false 8 | evm_version = "paris" 9 | fuzz = { runs = 1_000 } 10 | gas_reports = ["*"] 11 | libs = ["lib"] 12 | optimizer = true 13 | optimizer_runs = 10_000 14 | out = "out" 15 | script = "script" 16 | solc = "0.8.19" 17 | src = "src" 18 | test = "test" 19 | 20 | [profile.ci] 21 | fuzz = { runs = 10_000 } 22 | verbosity = 4 23 | 24 | [etherscan] 25 | arbitrum_one = { key = "${API_KEY_ARBISCAN}" } 26 | avalanche = { key = "${API_KEY_SNOWTRACE}" } 27 | bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } 28 | gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } 29 | goerli = { key = "${API_KEY_ETHERSCAN}" } 30 | mainnet = { key = "${API_KEY_ETHERSCAN}" } 31 | optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } 32 | polygon = { key = "${API_KEY_POLYGONSCAN}" } 33 | sepolia = { key = "${API_KEY_ETHERSCAN}" } 34 | 35 | [fmt] 36 | bracket_spacing = true 37 | int_types = "long" 38 | line_length = 120 39 | multiline_func_header = "all" 40 | number_underscore = "thousands" 41 | quote_style = "double" 42 | tab_width = 4 43 | wrap_comments = true 44 | 45 | [rpc_endpoints] 46 | arbitrum_one = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" 47 | avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" 48 | bnb_smart_chain = "https://bsc-dataseed.binance.org" 49 | gnosis_chain = "https://rpc.gnosischain.com" 50 | goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" 51 | localhost = "http://localhost:8545" 52 | mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" 53 | optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" 54 | polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" 55 | sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prb/foundry-template", 3 | "description": "Foundry-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 | "prettier": "^2.8.7", 11 | "solhint-community": "^3.5.0" 12 | }, 13 | "keywords": [ 14 | "blockchain", 15 | "ethereum", 16 | "forge", 17 | "foundry", 18 | "smart-contracts", 19 | "solidity", 20 | "template" 21 | ], 22 | "private": true, 23 | "scripts": { 24 | "clean": "rm -rf cache out", 25 | "lint": "pnpm lint:sol && pnpm prettier:check", 26 | "lint:sol": "forge fmt --check && pnpm solhint \"{script,src,test}/**/*.sol\"", 27 | "prettier:check": "prettier --check \"**/*.{json,md,yml}\"", 28 | "prettier:write": "prettier --write \"**/*.{json,md,yml}\"" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.1' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | devDependencies: 8 | prettier: 9 | specifier: ^2.8.7 10 | version: 2.8.7 11 | solhint-community: 12 | specifier: ^3.5.0 13 | version: 3.5.0 14 | 15 | packages: 16 | 17 | /@babel/code-frame@7.18.6: 18 | resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} 19 | engines: {node: '>=6.9.0'} 20 | dependencies: 21 | '@babel/highlight': 7.18.6 22 | dev: true 23 | 24 | /@babel/helper-validator-identifier@7.19.1: 25 | resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} 26 | engines: {node: '>=6.9.0'} 27 | dev: true 28 | 29 | /@babel/highlight@7.18.6: 30 | resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} 31 | engines: {node: '>=6.9.0'} 32 | dependencies: 33 | '@babel/helper-validator-identifier': 7.19.1 34 | chalk: 2.4.2 35 | js-tokens: 4.0.0 36 | dev: true 37 | 38 | /@solidity-parser/parser@0.16.0: 39 | resolution: {integrity: sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==} 40 | dependencies: 41 | antlr4ts: 0.5.0-alpha.4 42 | dev: true 43 | 44 | /ajv@6.12.6: 45 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 46 | dependencies: 47 | fast-deep-equal: 3.1.3 48 | fast-json-stable-stringify: 2.1.0 49 | json-schema-traverse: 0.4.1 50 | uri-js: 4.4.1 51 | dev: true 52 | 53 | /ajv@8.12.0: 54 | resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} 55 | dependencies: 56 | fast-deep-equal: 3.1.3 57 | json-schema-traverse: 1.0.0 58 | require-from-string: 2.0.2 59 | uri-js: 4.4.1 60 | dev: true 61 | 62 | /ansi-regex@5.0.1: 63 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 64 | engines: {node: '>=8'} 65 | dev: true 66 | 67 | /ansi-styles@3.2.1: 68 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 69 | engines: {node: '>=4'} 70 | dependencies: 71 | color-convert: 1.9.3 72 | dev: true 73 | 74 | /ansi-styles@4.3.0: 75 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 76 | engines: {node: '>=8'} 77 | dependencies: 78 | color-convert: 2.0.1 79 | dev: true 80 | 81 | /antlr4@4.12.0: 82 | resolution: {integrity: sha512-23iB5IzXJZRZeK9TigzUyrNc9pSmNqAerJRBcNq1ETrmttMWRgaYZzC561IgEO3ygKsDJTYDTozABXa4b/fTQQ==} 83 | engines: {node: '>=16'} 84 | dev: true 85 | 86 | /antlr4ts@0.5.0-alpha.4: 87 | resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} 88 | dev: true 89 | 90 | /argparse@2.0.1: 91 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 92 | dev: true 93 | 94 | /ast-parents@0.0.1: 95 | resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} 96 | dev: true 97 | 98 | /astral-regex@2.0.0: 99 | resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} 100 | engines: {node: '>=8'} 101 | dev: true 102 | 103 | /balanced-match@1.0.2: 104 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 105 | dev: true 106 | 107 | /brace-expansion@2.0.1: 108 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 109 | dependencies: 110 | balanced-match: 1.0.2 111 | dev: true 112 | 113 | /callsites@3.1.0: 114 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 115 | engines: {node: '>=6'} 116 | dev: true 117 | 118 | /chalk@2.4.2: 119 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 120 | engines: {node: '>=4'} 121 | dependencies: 122 | ansi-styles: 3.2.1 123 | escape-string-regexp: 1.0.5 124 | supports-color: 5.5.0 125 | dev: true 126 | 127 | /chalk@4.1.2: 128 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 129 | engines: {node: '>=10'} 130 | dependencies: 131 | ansi-styles: 4.3.0 132 | supports-color: 7.2.0 133 | dev: true 134 | 135 | /color-convert@1.9.3: 136 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 137 | dependencies: 138 | color-name: 1.1.3 139 | dev: true 140 | 141 | /color-convert@2.0.1: 142 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 143 | engines: {node: '>=7.0.0'} 144 | dependencies: 145 | color-name: 1.1.4 146 | dev: true 147 | 148 | /color-name@1.1.3: 149 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 150 | dev: true 151 | 152 | /color-name@1.1.4: 153 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 154 | dev: true 155 | 156 | /commander@10.0.0: 157 | resolution: {integrity: sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==} 158 | engines: {node: '>=14'} 159 | dev: true 160 | 161 | /cosmiconfig@8.1.0: 162 | resolution: {integrity: sha512-0tLZ9URlPGU7JsKq0DQOQ3FoRsYX8xDZ7xMiATQfaiGMz7EHowNkbU9u1coAOmnh9p/1ySpm0RB3JNWRXM5GCg==} 163 | engines: {node: '>=14'} 164 | dependencies: 165 | import-fresh: 3.3.0 166 | js-yaml: 4.1.0 167 | parse-json: 5.2.0 168 | path-type: 4.0.0 169 | dev: true 170 | 171 | /emoji-regex@8.0.0: 172 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 173 | dev: true 174 | 175 | /error-ex@1.3.2: 176 | resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} 177 | dependencies: 178 | is-arrayish: 0.2.1 179 | dev: true 180 | 181 | /escape-string-regexp@1.0.5: 182 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 183 | engines: {node: '>=0.8.0'} 184 | dev: true 185 | 186 | /fast-deep-equal@3.1.3: 187 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 188 | dev: true 189 | 190 | /fast-diff@1.2.0: 191 | resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} 192 | dev: true 193 | 194 | /fast-json-stable-stringify@2.1.0: 195 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 196 | dev: true 197 | 198 | /fs.realpath@1.0.0: 199 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 200 | dev: true 201 | 202 | /glob@8.1.0: 203 | resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} 204 | engines: {node: '>=12'} 205 | dependencies: 206 | fs.realpath: 1.0.0 207 | inflight: 1.0.6 208 | inherits: 2.0.4 209 | minimatch: 5.1.6 210 | once: 1.4.0 211 | dev: true 212 | 213 | /has-flag@3.0.0: 214 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 215 | engines: {node: '>=4'} 216 | dev: true 217 | 218 | /has-flag@4.0.0: 219 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 220 | engines: {node: '>=8'} 221 | dev: true 222 | 223 | /ignore@5.2.4: 224 | resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} 225 | engines: {node: '>= 4'} 226 | dev: true 227 | 228 | /import-fresh@3.3.0: 229 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} 230 | engines: {node: '>=6'} 231 | dependencies: 232 | parent-module: 1.0.1 233 | resolve-from: 4.0.0 234 | dev: true 235 | 236 | /inflight@1.0.6: 237 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 238 | dependencies: 239 | once: 1.4.0 240 | wrappy: 1.0.2 241 | dev: true 242 | 243 | /inherits@2.0.4: 244 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 245 | dev: true 246 | 247 | /is-arrayish@0.2.1: 248 | resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 249 | dev: true 250 | 251 | /is-fullwidth-code-point@3.0.0: 252 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 253 | engines: {node: '>=8'} 254 | dev: true 255 | 256 | /js-tokens@4.0.0: 257 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 258 | dev: true 259 | 260 | /js-yaml@4.1.0: 261 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 262 | hasBin: true 263 | dependencies: 264 | argparse: 2.0.1 265 | dev: true 266 | 267 | /json-parse-even-better-errors@2.3.1: 268 | resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} 269 | dev: true 270 | 271 | /json-schema-traverse@0.4.1: 272 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 273 | dev: true 274 | 275 | /json-schema-traverse@1.0.0: 276 | resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 277 | dev: true 278 | 279 | /lines-and-columns@1.2.4: 280 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 281 | dev: true 282 | 283 | /lodash.truncate@4.4.2: 284 | resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} 285 | dev: true 286 | 287 | /lodash@4.17.21: 288 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 289 | dev: true 290 | 291 | /minimatch@5.1.6: 292 | resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} 293 | engines: {node: '>=10'} 294 | dependencies: 295 | brace-expansion: 2.0.1 296 | dev: true 297 | 298 | /once@1.4.0: 299 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 300 | dependencies: 301 | wrappy: 1.0.2 302 | dev: true 303 | 304 | /parent-module@1.0.1: 305 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 306 | engines: {node: '>=6'} 307 | dependencies: 308 | callsites: 3.1.0 309 | dev: true 310 | 311 | /parse-json@5.2.0: 312 | resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} 313 | engines: {node: '>=8'} 314 | dependencies: 315 | '@babel/code-frame': 7.18.6 316 | error-ex: 1.3.2 317 | json-parse-even-better-errors: 2.3.1 318 | lines-and-columns: 1.2.4 319 | dev: true 320 | 321 | /path-type@4.0.0: 322 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 323 | engines: {node: '>=8'} 324 | dev: true 325 | 326 | /pluralize@8.0.0: 327 | resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} 328 | engines: {node: '>=4'} 329 | dev: true 330 | 331 | /prettier@2.8.7: 332 | resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} 333 | engines: {node: '>=10.13.0'} 334 | hasBin: true 335 | dev: true 336 | 337 | /punycode@2.3.0: 338 | resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} 339 | engines: {node: '>=6'} 340 | dev: true 341 | 342 | /require-from-string@2.0.2: 343 | resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 344 | engines: {node: '>=0.10.0'} 345 | dev: true 346 | 347 | /resolve-from@4.0.0: 348 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 349 | engines: {node: '>=4'} 350 | dev: true 351 | 352 | /semver@6.3.0: 353 | resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} 354 | hasBin: true 355 | dev: true 356 | 357 | /slice-ansi@4.0.0: 358 | resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} 359 | engines: {node: '>=10'} 360 | dependencies: 361 | ansi-styles: 4.3.0 362 | astral-regex: 2.0.0 363 | is-fullwidth-code-point: 3.0.0 364 | dev: true 365 | 366 | /solhint-community@3.5.0: 367 | resolution: {integrity: sha512-kSntBbwcPRVqIQuKMOmbuzIyDv/liLy0mjPaA5Gpw2ov3IZ5PlFXiG9ZoYhhhJQm/1iFCHmAjQcgOGmR18CkgA==} 368 | hasBin: true 369 | dependencies: 370 | '@solidity-parser/parser': 0.16.0 371 | ajv: 6.12.6 372 | antlr4: 4.12.0 373 | ast-parents: 0.0.1 374 | chalk: 4.1.2 375 | commander: 10.0.0 376 | cosmiconfig: 8.1.0 377 | fast-diff: 1.2.0 378 | glob: 8.1.0 379 | ignore: 5.2.4 380 | js-yaml: 4.1.0 381 | lodash: 4.17.21 382 | pluralize: 8.0.0 383 | semver: 6.3.0 384 | strip-ansi: 6.0.1 385 | table: 6.8.1 386 | text-table: 0.2.0 387 | optionalDependencies: 388 | prettier: 2.8.7 389 | dev: true 390 | 391 | /string-width@4.2.3: 392 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 393 | engines: {node: '>=8'} 394 | dependencies: 395 | emoji-regex: 8.0.0 396 | is-fullwidth-code-point: 3.0.0 397 | strip-ansi: 6.0.1 398 | dev: true 399 | 400 | /strip-ansi@6.0.1: 401 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 402 | engines: {node: '>=8'} 403 | dependencies: 404 | ansi-regex: 5.0.1 405 | dev: true 406 | 407 | /supports-color@5.5.0: 408 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 409 | engines: {node: '>=4'} 410 | dependencies: 411 | has-flag: 3.0.0 412 | dev: true 413 | 414 | /supports-color@7.2.0: 415 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 416 | engines: {node: '>=8'} 417 | dependencies: 418 | has-flag: 4.0.0 419 | dev: true 420 | 421 | /table@6.8.1: 422 | resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} 423 | engines: {node: '>=10.0.0'} 424 | dependencies: 425 | ajv: 8.12.0 426 | lodash.truncate: 4.4.2 427 | slice-ansi: 4.0.0 428 | string-width: 4.2.3 429 | strip-ansi: 6.0.1 430 | dev: true 431 | 432 | /text-table@0.2.0: 433 | resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 434 | dev: true 435 | 436 | /uri-js@4.4.1: 437 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 438 | dependencies: 439 | punycode: 2.3.0 440 | dev: true 441 | 442 | /wrappy@1.0.2: 443 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 444 | dev: true 445 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @prb/test/=lib/prb-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | @uniswap/v4-core/=lib/v4-periphery/lib/v4-core/ 4 | @uniswap-periphery/v4-periphery=lib/v4-periphery/ 5 | src/=src/ 6 | -------------------------------------------------------------------------------- /script/Base.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.19; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | 6 | abstract contract BaseScript is Script { 7 | /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. 8 | string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; 9 | 10 | /// @dev Needed for the deterministic deployments. 11 | bytes32 internal constant ZERO_SALT = bytes32(0); 12 | 13 | /// @dev The address of the contract deployer. 14 | address internal deployer; 15 | 16 | /// @dev Used to derive the deployer's address. 17 | string internal mnemonic; 18 | 19 | constructor() { 20 | mnemonic = vm.envOr("MNEMONIC", TEST_MNEMONIC); 21 | (deployer,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 }); 22 | } 23 | 24 | modifier broadcaster() { 25 | vm.startBroadcast(deployer); 26 | _; 27 | vm.stopBroadcast(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /script/Deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity >=0.8.19; 3 | 4 | import { UniswapHooksFactory } from "../src/UniswapHooksFactory.sol"; 5 | import { BaseScript } from "./Base.s.sol"; 6 | 7 | contract Deploy is BaseScript { 8 | function run() public broadcaster returns (UniswapHooksFactory uniswapHooksFactory) { 9 | uniswapHooksFactory = new UniswapHooksFactory(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/UniswapHooks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.19; 3 | 4 | import { 5 | IPoolManager, Hooks, IHooks, BaseHook, BalanceDelta 6 | } from "@uniswap-periphery/v4-periphery/contracts/BaseHook.sol"; 7 | import { IHookFeeManager } from "@uniswap/v4-core/contracts/interfaces/IHookFeeManager.sol"; 8 | import { IDynamicFeeManager } from "@uniswap/v4-core/contracts/interfaces/IDynamicFeeManager.sol"; 9 | import { console2 } from "forge-std/console2.sol"; 10 | 11 | contract UniswapHooks is BaseHook, IHookFeeManager, IDynamicFeeManager { 12 | address public owner; 13 | 14 | constructor(address _owner, IPoolManager _poolManager) BaseHook(_poolManager) { 15 | owner = _owner; 16 | } 17 | 18 | function getHooksCalls() public pure override returns (Hooks.Calls memory) { 19 | return Hooks.Calls({ 20 | beforeInitialize: true, 21 | afterInitialize: true, 22 | beforeModifyPosition: true, 23 | afterModifyPosition: true, 24 | beforeSwap: true, 25 | afterSwap: true, 26 | beforeDonate: true, 27 | afterDonate: true 28 | }); 29 | } 30 | 31 | /// @inheritdoc IHooks 32 | function beforeInitialize( 33 | address, // sender 34 | IPoolManager.PoolKey calldata, // key 35 | uint160 // sqrtPriceX96 36 | ) 37 | external 38 | pure 39 | override 40 | returns (bytes4) 41 | { 42 | console2.log("beforeInitialize"); 43 | return IHooks.beforeInitialize.selector; 44 | } 45 | 46 | /// @inheritdoc IHooks 47 | function afterInitialize( 48 | address, // sender 49 | IPoolManager.PoolKey calldata, // key 50 | uint160, // sqrtPriceX96 51 | int24 // tick 52 | ) 53 | external 54 | pure 55 | override 56 | returns (bytes4) 57 | { 58 | console2.log("afterInitialize"); 59 | return IHooks.afterInitialize.selector; 60 | } 61 | 62 | /// @inheritdoc IHooks 63 | function beforeModifyPosition( 64 | address, // sender 65 | IPoolManager.PoolKey calldata, // key 66 | IPoolManager.ModifyPositionParams calldata // params 67 | ) 68 | external 69 | pure 70 | override 71 | returns (bytes4) 72 | { 73 | console2.log("beforeModifyPosition"); 74 | return IHooks.beforeModifyPosition.selector; 75 | } 76 | 77 | /// @inheritdoc IHooks 78 | function afterModifyPosition( 79 | address, // sender 80 | IPoolManager.PoolKey calldata, // key 81 | IPoolManager.ModifyPositionParams calldata, // params 82 | BalanceDelta // delta 83 | ) 84 | external 85 | pure 86 | override 87 | returns (bytes4) 88 | { 89 | console2.log("afterModifyPosition"); 90 | return IHooks.afterModifyPosition.selector; 91 | } 92 | 93 | /// @inheritdoc IHooks 94 | function beforeSwap( 95 | address, // sender 96 | IPoolManager.PoolKey calldata, // key 97 | IPoolManager.SwapParams calldata // params 98 | ) 99 | external 100 | pure 101 | override 102 | returns (bytes4) 103 | { 104 | console2.log("beforeSwap"); 105 | return IHooks.beforeSwap.selector; 106 | } 107 | 108 | /// @inheritdoc IHooks 109 | function afterSwap( 110 | address, // sender 111 | IPoolManager.PoolKey calldata, // key 112 | IPoolManager.SwapParams calldata, // params 113 | BalanceDelta // delta 114 | ) 115 | external 116 | pure 117 | override 118 | returns (bytes4) 119 | { 120 | console2.log("afterSwap"); 121 | return IHooks.afterSwap.selector; 122 | } 123 | 124 | /// @inheritdoc IHooks 125 | function beforeDonate( 126 | address, // sender 127 | IPoolManager.PoolKey calldata, // key 128 | uint256, // amount0 129 | uint256 // amount1 130 | ) 131 | external 132 | pure 133 | override 134 | returns (bytes4) 135 | { 136 | console2.log("beforeDonate"); 137 | return IHooks.beforeDonate.selector; 138 | } 139 | 140 | /// @inheritdoc IHooks 141 | function afterDonate( 142 | address, // sender 143 | IPoolManager.PoolKey calldata, // key 144 | uint256, // amount0 145 | uint256 // amount1 146 | ) 147 | external 148 | pure 149 | override 150 | returns (bytes4) 151 | { 152 | console2.log("afterDonate"); 153 | return IHooks.afterDonate.selector; 154 | } 155 | 156 | /// @inheritdoc IHookFeeManager 157 | function getHookSwapFee(IPoolManager.PoolKey calldata) external pure returns (uint8) { 158 | console2.log("getHookSwapFee"); 159 | return 100; 160 | } 161 | 162 | /// @inheritdoc IHookFeeManager 163 | function getHookWithdrawFee(IPoolManager.PoolKey calldata) external pure returns (uint8) { 164 | console2.log("getHookWithdrawFee"); 165 | return 100; 166 | } 167 | 168 | /// @inheritdoc IDynamicFeeManager 169 | function getFee(IPoolManager.PoolKey calldata) external pure returns (uint24) { 170 | console2.log("getFee"); 171 | return 10_000; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/UniswapHooksFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.19; 3 | 4 | import { IPoolManager, UniswapHooks } from "./UniswapHooks.sol"; 5 | 6 | contract UniswapHooksFactory { 7 | function deploy(address owner, IPoolManager poolManager, bytes32 salt) external returns (address) { 8 | return address(new UniswapHooks{salt: salt}(owner, poolManager)); 9 | } 10 | 11 | function getPrecomputedHookAddress( 12 | address owner, 13 | IPoolManager poolManager, 14 | bytes32 salt 15 | ) 16 | external 17 | view 18 | returns (address) 19 | { 20 | bytes32 bytecodeHash = 21 | keccak256(abi.encodePacked(type(UniswapHooks).creationCode, abi.encode(owner, poolManager))); 22 | bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)); 23 | return address(uint160(uint256(hash))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/UniswapHooks.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.19; 3 | 4 | import { PRBTest } from "@prb/test/PRBTest.sol"; 5 | import { console2 } from "forge-std/console2.sol"; 6 | import { StdCheats } from "forge-std/StdCheats.sol"; 7 | 8 | import { PoolManager, Currency } from "@uniswap/v4-core/contracts/PoolManager.sol"; 9 | import { TickMath } from "@uniswap/v4-core/contracts/libraries/TickMath.sol"; 10 | import { Fees } from "@uniswap/v4-core/contracts/libraries/Fees.sol"; 11 | import { CurrencyLibrary } from "@uniswap/v4-core/contracts/libraries/CurrencyLibrary.sol"; 12 | import { TestERC20 } from "@uniswap/v4-core/contracts/test/TestERC20.sol"; 13 | import { 14 | IPoolManager, Hooks, IHooks, BaseHook, BalanceDelta 15 | } from "@uniswap-periphery/v4-periphery/contracts/BaseHook.sol"; 16 | 17 | import { UniswapHooksFactory } from "../src/UniswapHooksFactory.sol"; 18 | 19 | using CurrencyLibrary for Currency; 20 | 21 | contract UniswapHooksTest is PRBTest, StdCheats { 22 | UniswapHooksFactory internal uniswapHooksFactory; 23 | TestERC20 internal token1; 24 | TestERC20 internal token2; 25 | IHooks internal deployedHooks; 26 | IPoolManager internal poolManager; 27 | 28 | function setUp() public virtual { 29 | uniswapHooksFactory = new UniswapHooksFactory(); 30 | } 31 | 32 | function test_Example() external { 33 | address owner = 0x388C818CA8B9251b393131C08a736A67ccB19297; 34 | poolManager = IPoolManager(address(new PoolManager(type(uint256).max))); 35 | 36 | for (uint256 i = 0; i < 1500; i++) { 37 | bytes32 salt = bytes32(i); 38 | address expectedAddress = uniswapHooksFactory.getPrecomputedHookAddress(owner, poolManager, salt); 39 | 40 | // 0xff = 11111111 = all hooks enabled 41 | if (_doesAddressStartWith(expectedAddress, 0xff)) { 42 | console2.log("Found hook address", expectedAddress, "with salt of", i); 43 | 44 | deployedHooks = IHooks(uniswapHooksFactory.deploy(owner, poolManager, salt)); 45 | assertEq(address(deployedHooks), expectedAddress, "address is not as expected"); 46 | 47 | // Let's test all the hooks 48 | 49 | // First we need two tokens 50 | token1 = new TestERC20(3000); 51 | token2 = new TestERC20(3000); 52 | token1.approve(address(poolManager), type(uint256).max); 53 | token2.approve(address(poolManager), type(uint256).max); 54 | 55 | // sqrt(2) = 79_228_162_514_264_337_593_543_950_336 as Q64.96 56 | poolManager.initialize(_getPoolKey(), 79_228_162_514_264_337_593_543_950_336); 57 | poolManager.lock(new bytes(0)); 58 | 59 | return; 60 | } 61 | } 62 | 63 | revert("No salt found"); 64 | } 65 | 66 | function lockAcquired(uint256, bytes calldata) external returns (bytes memory) { 67 | IPoolManager.PoolKey memory key = _getPoolKey(); 68 | 69 | // lets execute all remaining hooks 70 | poolManager.modifyPosition(key, IPoolManager.ModifyPositionParams(TickMath.MIN_TICK, TickMath.MAX_TICK, 1000)); 71 | poolManager.donate(key, 100, 100); 72 | 73 | // opposite action: poolManager.swap(key, IPoolManager.SwapParams(true, 100, TickMath.MIN_SQRT_RATIO * 1000)); 74 | poolManager.swap(key, IPoolManager.SwapParams(false, 100, TickMath.MAX_SQRT_RATIO / 1000)); 75 | 76 | _settleTokenBalance(Currency.wrap(address(token1))); 77 | _settleTokenBalance(Currency.wrap(address(token2))); 78 | 79 | return new bytes(0); 80 | } 81 | 82 | function _settleTokenBalance(Currency token) private { 83 | int256 unsettledTokenBalance = poolManager.getCurrencyDelta(0, token); 84 | 85 | if (unsettledTokenBalance == 0) { 86 | return; 87 | } 88 | 89 | if (unsettledTokenBalance < 0) { 90 | poolManager.take(token, msg.sender, uint256(-unsettledTokenBalance)); 91 | return; 92 | } 93 | 94 | token.transfer(address(poolManager), uint256(unsettledTokenBalance)); 95 | poolManager.settle(token); 96 | } 97 | 98 | function _getPoolKey() private view returns (IPoolManager.PoolKey memory) { 99 | return IPoolManager.PoolKey({ 100 | currency0: Currency.wrap(address(token1)), 101 | currency1: Currency.wrap(address(token2)), 102 | fee: Fees.DYNAMIC_FEE_FLAG + Fees.HOOK_SWAP_FEE_FLAG + Fees.HOOK_WITHDRAW_FEE_FLAG, // 0xE00000 = 111 103 | tickSpacing: 1, 104 | hooks: IHooks(deployedHooks) 105 | }); 106 | } 107 | 108 | function _doesAddressStartWith(address _address, uint160 _prefix) private pure returns (bool) { 109 | return uint160(_address) / (2 ** (8 * (19))) == _prefix; 110 | } 111 | } 112 | --------------------------------------------------------------------------------