From 6ef0a4e8617643738cbd2cdb98d424893c936fcc Mon Sep 17 00:00:00 2001 From: Nico Vergauwen Date: Wed, 1 May 2024 12:14:23 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 19 ++++ .env.example | 11 ++ .github/FUNDING.yml | 2 + .github/scripts/rename.sh | 38 +++++++ .github/workflows/ci.yml | 92 ++++++++++++++++ .github/workflows/create.yml | 52 +++++++++ .gitignore | 20 ++++ .gitpod.yml | 14 +++ .prettierignore | 17 +++ .prettierrc.yml | 7 ++ .solhint.json | 14 +++ .vscode/settings.json | 9 ++ LICENSE.md | 16 +++ README.md | 205 +++++++++++++++++++++++++++++++++++ bun.lockb | Bin 0 -> 28441 bytes foundry.toml | 53 +++++++++ package.json | 38 +++++++ remappings.txt | 2 + script/Base.s.sol | 41 +++++++ script/Deploy.s.sol | 13 +++ src/Foo.sol | 8 ++ test/Foo.t.sol | 56 ++++++++++ 22 files changed, 727 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .github/FUNDING.yml create mode 100755 .github/scripts/rename.sh create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/create.yml create mode 100644 .gitignore create mode 100644 .gitpod.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.yml create mode 100644 .solhint.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 foundry.toml create mode 100644 package.json create mode 100644 remappings.txt create mode 100644 script/Base.s.sol create mode 100644 script/Deploy.s.sol create mode 100644 src/Foo.sol create mode 100644 test/Foo.t.sol diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..746ae31 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.sol] +indent_size = 4 + +[*.tree] +indent_size = 1 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..98c1028 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" +export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN" +export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN" +export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" +export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN" +export API_KEY_INFURA="YOUR_API_KEY_INFURA" +export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN" +export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" +export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE" +export MNEMONIC="YOUR_MNEMONIC" +export FOUNDRY_PROFILE="default" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b763d0f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +custom: "https://omo.so/prberg" +github: "PaulRBerg" diff --git a/.github/scripts/rename.sh b/.github/scripts/rename.sh new file mode 100755 index 0000000..62e37dd --- /dev/null +++ b/.github/scripts/rename.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# https://gist.github.com/vncsna/64825d5609c146e80de8b1fd623011ca +set -euo pipefail + +# Define the input vars +GITHUB_REPOSITORY=${1?Error: Please pass username/repo, e.g. prb/foundry-template} +GITHUB_REPOSITORY_OWNER=${2?Error: Please pass username, e.g. prb} +GITHUB_REPOSITORY_DESCRIPTION=${3:-""} # If null then replace with empty string + +echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY" +echo "GITHUB_REPOSITORY_OWNER: $GITHUB_REPOSITORY_OWNER" +echo "GITHUB_REPOSITORY_DESCRIPTION: $GITHUB_REPOSITORY_DESCRIPTION" + +# jq is like sed for JSON data +JQ_OUTPUT=`jq \ + --arg NAME "@$GITHUB_REPOSITORY" \ + --arg AUTHOR_NAME "$GITHUB_REPOSITORY_OWNER" \ + --arg URL "https://github.com/$GITHUB_REPOSITORY_OWNER" \ + --arg DESCRIPTION "$GITHUB_REPOSITORY_DESCRIPTION" \ + '.name = $NAME | .description = $DESCRIPTION | .author |= ( .name = $AUTHOR_NAME | .url = $URL )' \ + package.json +` + +# Overwrite package.json +echo "$JQ_OUTPUT" > package.json + +# Make sed command compatible in both Mac and Linux environments +# Reference: https://stackoverflow.com/a/38595160/8696958 +sedi () { + sed --version >/dev/null 2>&1 && sed -i -- "$@" || sed -i "" "$@" +} + +# Rename instances of "PaulRBerg/foundry-template" to the new repo name in README.md for badges only +sedi "/gitpod/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gitpod-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gha/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" +sedi "/gha-badge/ s|PaulRBerg/foundry-template|"${GITHUB_REPOSITORY}"|;" "README.md" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7550749 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,92 @@ +name: "CI" + +env: + API_KEY_ALCHEMY: ${{ secrets.API_KEY_ALCHEMY }} + FOUNDRY_PROFILE: "ci" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - "main" + +jobs: + lint: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Lint the code" + run: "bun run lint" + + - name: "Add lint summary" + run: | + echo "## Lint result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + build: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Build the contracts and print their size" + run: "forge build --sizes" + + - name: "Add build summary" + run: | + echo "## Build result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + test: + needs: ["lint", "build"] + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Install Foundry" + uses: "foundry-rs/foundry-toolchain@v1" + + - name: "Install Bun" + uses: "oven-sh/setup-bun@v1" + + - name: "Install the Node.js dependencies" + run: "bun install" + + - name: "Show the Foundry config" + run: "forge config" + + - name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance" + run: > + echo "FOUNDRY_FUZZ_SEED=$( + echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800)) + )" >> $GITHUB_ENV + + - name: "Run the tests" + run: "forge test" + + - name: "Add test summary" + run: | + echo "## Tests result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/create.yml b/.github/workflows/create.yml new file mode 100644 index 0000000..e0e9369 --- /dev/null +++ b/.github/workflows/create.yml @@ -0,0 +1,52 @@ +name: "Create" + +# The workflow will run only when the "Use this template" button is used +on: + create: + +jobs: + create: + # We only run this action when the repository isn't the template repository. References: + # - https://docs.github.com/en/actions/learn-github-actions/contexts + # - https://docs.github.com/en/actions/learn-github-actions/expressions + if: ${{ !github.event.repository.is_template }} + permissions: "write-all" + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v4" + + - name: "Update package.json" + env: + GITHUB_REPOSITORY_DESCRIPTION: ${{ github.event.repository.description }} + run: + ./.github/scripts/rename.sh "$GITHUB_REPOSITORY" "$GITHUB_REPOSITORY_OWNER" "$GITHUB_REPOSITORY_DESCRIPTION" + + - name: "Add rename summary" + run: | + echo "## Commit result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Remove files not needed in the user's copy of the template" + run: | + rm -f "./.github/FUNDING.yml" + rm -f "./.github/scripts/rename.sh" + rm -f "./.github/workflows/create.yml" + + - name: "Add remove summary" + run: | + echo "## Remove result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Update commit" + uses: "stefanzweifel/git-auto-commit-action@v4" + with: + commit_message: "feat: initial commit" + commit_options: "--amend" + push_options: "--force" + skip_fetch: true + + - name: "Add commit summary" + run: | + echo "## Commit result" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e108b40 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# directories +cache +coverage +node_modules +out + +# files +*.env +*.log +.DS_Store +.pnp.* +lcov.info +package-lock.json +pnpm-lock.yaml +yarn.lock + +# broadcasts +!broadcast +broadcast/* +broadcast/*/31337/ diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..b9646d8 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,14 @@ +image: "gitpod/workspace-bun" + +tasks: + - name: "Install dependencies" + before: | + curl -L https://foundry.paradigm.xyz | bash + source ~/.bashrc + foundryup + init: "bun install" + +vscode: + extensions: + - "esbenp.prettier-vscode" + - "NomicFoundation.hardhat-solidity" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3996d20 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,17 @@ +# directories +broadcast +cache +coverage +node_modules +out + +# files +*.env +*.log +.DS_Store +.pnp.* +bun.lockb +lcov.info +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..a1ecdbb --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,7 @@ +bracketSpacing: true +printWidth: 120 +proseWrap: "always" +singleQuote: false +tabWidth: 2 +trailingComma: "all" +useTabs: false diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 0000000..14f780e --- /dev/null +++ b/.solhint.json @@ -0,0 +1,14 @@ +{ + "extends": "solhint:recommended", + "rules": { + "code-complexity": ["error", 8], + "compiler-version": ["error", ">=0.8.25"], + "func-name-mixedcase": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 120], + "named-parameters-mapping": "warn", + "no-console": "off", + "not-rely-on-time": "off", + "one-contract-per-file": "off" + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..241108b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "[solidity]": { + "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" + }, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + }, + "solidity.formatter": "forge" +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..0424fa0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2024 Paul Razvan Berg + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f63c082 --- /dev/null +++ b/README.md @@ -0,0 +1,205 @@ +# Foundry Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license] + +[gitpod]: https://gitpod.io/#https://github.com/PaulRBerg/foundry-template +[gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod +[gha]: https://github.com/PaulRBerg/foundry-template/actions +[gha-badge]: https://github.com/PaulRBerg/foundry-template/actions/workflows/ci.yml/badge.svg +[foundry]: https://getfoundry.sh/ +[foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg +[license]: https://opensource.org/licenses/MIT +[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg + +A Foundry-based template for developing Solidity smart contracts, with sensible defaults. + +## What's Inside + +- [Forge](https://github.com/foundry-rs/foundry/blob/master/forge): compile, test, fuzz, format, and deploy smart + contracts +- [Forge Std](https://github.com/foundry-rs/forge-std): collection of helpful contracts and utilities for testing +- [Prettier](https://github.com/prettier/prettier): code formatter for non-Solidity files +- [Solhint](https://github.com/protofire/solhint): linter for Solidity code + +## Getting Started + +Click the [`Use this template`](https://github.com/PaulRBerg/foundry-template/generate) button at the top of the page to +create a new repository with this repo as the initial state. + +Or, if you prefer to install the template manually: + +```sh +$ mkdir my-project +$ cd my-project +$ forge init --template PaulRBerg/foundry-template +$ bun install # install Solhint, Prettier, and other Node.js deps +``` + +If this is your first time with Foundry, check out the +[installation](https://github.com/foundry-rs/foundry#installation) instructions. + +## Features + +This template builds upon the frameworks and libraries mentioned above, so please consult their respective documentation +for details about their specific features. + +For example, if you're interested in exploring Foundry in more detail, you should look at the +[Foundry Book](https://book.getfoundry.sh/). In particular, you may be interested in reading the +[Writing Tests](https://book.getfoundry.sh/forge/writing-tests.html) tutorial. + +### Sensible Defaults + +This template comes with a set of sensible default configurations for you to use. These defaults can be found in the +following files: + +```text +├── .editorconfig +├── .gitignore +├── .prettierignore +├── .prettierrc.yml +├── .solhint.json +├── foundry.toml +└── remappings.txt +``` + +### VSCode Integration + +This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic +Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity). + +For guidance on how to integrate a Foundry project in VSCode, please refer to this +[guide](https://book.getfoundry.sh/config/vscode). + +### GitHub Actions + +This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull +request made to the `main` branch. + +You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). + +## Installing Dependencies + +Foundry typically uses git submodules to manage dependencies, but this template uses Node.js packages because +[submodules don't scale](https://twitter.com/PaulRBerg/status/1736695487057531328). + +This is how to install dependencies: + +1. Install the dependency using your preferred package manager, e.g. `bun install dependency-name` + - Use this syntax to install from GitHub: `bun install github:username/repo-name` +2. Add a remapping for the dependency in [remappings.txt](./remappings.txt), e.g. + `dependency-name=node_modules/dependency-name` + +Note that OpenZeppelin Contracts is pre-installed, so you can follow that as an example. + +## Writing Tests + +To write a new test contract, you start by importing `Test` from `forge-std`, and then you inherit it in your test +contract. Forge Std comes with a pre-instantiated [cheatcodes](https://book.getfoundry.sh/cheatcodes/) environment +accessible via the `vm` property. If you would like to view the logs in the terminal output, you can add the `-vvv` flag +and use [console.log](https://book.getfoundry.sh/faq?highlight=console.log#how-do-i-use-consolelog). + +This template comes with an example test contract [Foo.t.sol](./test/Foo.t.sol) + +## Usage + +This is a list of the most frequently needed commands. + +### Build + +Build the contracts: + +```sh +$ forge build +``` + +### Clean + +Delete the build artifacts and cache directories: + +```sh +$ forge clean +``` + +### Compile + +Compile the contracts: + +```sh +$ forge build +``` + +### Coverage + +Get a test coverage report: + +```sh +$ forge coverage +``` + +### Deploy + +Deploy to Anvil: + +```sh +$ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545 +``` + +For this script to work, you need to have a `MNEMONIC` environment variable set to a valid +[BIP39 mnemonic](https://iancoleman.io/bip39/). + +For instructions on how to deploy to a testnet or mainnet, check out the +[Solidity Scripting](https://book.getfoundry.sh/tutorials/solidity-scripting.html) tutorial. + +### Format + +Format the contracts: + +```sh +$ forge fmt +``` + +### Gas Usage + +Get a gas report: + +```sh +$ forge test --gas-report +``` + +### Lint + +Lint the contracts: + +```sh +$ bun run lint +``` + +### Test + +Run the tests: + +```sh +$ forge test +``` + +Generate test coverage and output result to the terminal: + +```sh +$ bun run test:coverage +``` + +Generate test coverage with lcov report (you'll have to open the `./coverage/index.html` file in your browser, to do so +simply copy paste the path): + +```sh +$ bun run test:coverage:report +``` + +## Related Efforts + +- [abigger87/femplate](https://github.com/abigger87/femplate) +- [cleanunicorn/ethereum-smartcontract-template](https://github.com/cleanunicorn/ethereum-smartcontract-template) +- [foundry-rs/forge-template](https://github.com/foundry-rs/forge-template) +- [FrankieIsLost/forge-template](https://github.com/FrankieIsLost/forge-template) + +## License + +This project is licensed under MIT. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..a72ec4f3be8031e2be0095e51db5dc98250c5a3a GIT binary patch literal 28441 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 be*1*KCnq`hWQ2rRu{j);=0.8.25 <0.9.0; + +import { Script } from "forge-std/src/Script.sol"; + +abstract contract BaseScript is Script { + /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. + string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk"; + + /// @dev Needed for the deterministic deployments. + bytes32 internal constant ZERO_SALT = bytes32(0); + + /// @dev The address of the transaction broadcaster. + address internal broadcaster; + + /// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined. + string internal mnemonic; + + /// @dev Initializes the transaction broadcaster like this: + /// + /// - If $ETH_FROM is defined, use it. + /// - Otherwise, derive the broadcaster address from $MNEMONIC. + /// - If $MNEMONIC is not defined, default to a test mnemonic. + /// + /// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line. + constructor() { + address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) }); + if (from != address(0)) { + broadcaster = from; + } else { + mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC }); + (broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 }); + } + } + + modifier broadcast() { + vm.startBroadcast(broadcaster); + _; + vm.stopBroadcast(); + } +} diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..498db52 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.25 <0.9.0; + +import { Foo } from "../src/Foo.sol"; + +import { BaseScript } from "./Base.s.sol"; + +/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting +contract Deploy is BaseScript { + function run() public broadcast returns (Foo foo) { + foo = new Foo(); + } +} diff --git a/src/Foo.sol b/src/Foo.sol new file mode 100644 index 0000000..7483070 --- /dev/null +++ b/src/Foo.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.25; + +contract Foo { + function id(uint256 value) external pure returns (uint256) { + return value; + } +} diff --git a/test/Foo.t.sol b/test/Foo.t.sol new file mode 100644 index 0000000..727337a --- /dev/null +++ b/test/Foo.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.25 <0.9.0; + +import { Test } from "forge-std/src/Test.sol"; +import { console2 } from "forge-std/src/console2.sol"; + +import { Foo } from "../src/Foo.sol"; + +interface IERC20 { + function balanceOf(address account) external view returns (uint256); +} + +/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: +/// https://book.getfoundry.sh/forge/writing-tests +contract FooTest is Test { + Foo internal foo; + + /// @dev A function invoked before each test case is run. + function setUp() public virtual { + // Instantiate the contract-under-test. + foo = new Foo(); + } + + /// @dev Basic test. Run it with `forge test -vvv` to see the console log. + function test_Example() external view { + console2.log("Hello World"); + uint256 x = 42; + assertEq(foo.id(x), x, "value mismatch"); + } + + /// @dev Fuzz test that provides random values for an unsigned integer, but which rejects zero as an input. + /// If you need more sophisticated input validation, you should use the `bound` utility instead. + /// See https://twitter.com/PaulRBerg/status/1622558791685242880 + function testFuzz_Example(uint256 x) external view { + vm.assume(x != 0); // or x = bound(x, 1, 100) + assertEq(foo.id(x), x, "value mismatch"); + } + + /// @dev Fork test that runs against an Ethereum Mainnet fork. For this to work, you need to set `API_KEY_ALCHEMY` + /// in your environment You can get an API key for free at https://alchemy.com. + function testFork_Example() external { + // Silently pass this test if there is no API key. + string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); + if (bytes(alchemyApiKey).length == 0) { + return; + } + + // Otherwise, run the test against the mainnet fork. + vm.createSelectFork({ urlOrAlias: "mainnet", blockNumber: 16_428_000 }); + address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address holder = 0x7713974908Be4BEd47172370115e8b1219F4A5f0; + uint256 actualBalance = IERC20(usdc).balanceOf(holder); + uint256 expectedBalance = 196_307_713.810457e6; + assertEq(actualBalance, expectedBalance); + } +}