Last active
December 18, 2025 00:09
-
-
Save benwoody/812538a1b5b7338e7188a982fee9f8a6 to your computer and use it in GitHub Desktop.
Bootstrap a production-ready Foundry project with sensible defaults, testing patterns, and deploy scripts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Foundry Project Bootstrap Script | |
| # Usage: ./foundry-bootstrap.sh <project-name> [--with-chainlink] [--with-openzeppelin] | |
| set -e | |
| PROJECT_NAME=${1:-"my-contract"} | |
| WITH_CHAINLINK=false | |
| WITH_OPENZEPPELIN=false | |
| # Parse flags | |
| for arg in "$@"; do | |
| case $arg in | |
| --with-chainlink) | |
| WITH_CHAINLINK=true | |
| ;; | |
| --with-openzeppelin) | |
| WITH_OPENZEPPELIN=true | |
| ;; | |
| esac | |
| done | |
| echo "π¨ Creating Foundry project: $PROJECT_NAME" | |
| # Initialize project | |
| forge init "$PROJECT_NAME" | |
| cd "$PROJECT_NAME" | |
| # Remove default Counter files | |
| rm -f src/Counter.sol test/Counter.t.sol script/Counter.s.sol | |
| # Install dependencies | |
| if [ "$WITH_OPENZEPPELIN" = true ]; then | |
| echo "π¦ Installing OpenZeppelin contracts..." | |
| forge install OpenZeppelin/openzeppelin-contracts --no-commit | |
| echo '@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/' >> remappings.txt | |
| fi | |
| if [ "$WITH_CHAINLINK" = true ]; then | |
| echo "π¦ Installing Chainlink contracts..." | |
| forge install smartcontractkit/chainlink --no-commit | |
| echo '@chainlink/contracts/=lib/chainlink/contracts/' >> remappings.txt | |
| fi | |
| # Create foundry.toml | |
| cat > foundry.toml << 'EOF' | |
| [profile.default] | |
| src = "src" | |
| out = "out" | |
| libs = ["lib"] | |
| solc = "0.8.24" | |
| optimizer = true | |
| optimizer_runs = 200 | |
| via_ir = false | |
| # Gas reporting | |
| gas_reports = ["*"] | |
| [profile.default.fuzz] | |
| runs = 256 | |
| [rpc_endpoints] | |
| mainnet = "${MAINNET_RPC_URL}" | |
| sepolia = "${SEPOLIA_RPC_URL}" | |
| base = "${BASE_RPC_URL}" | |
| arbitrum = "${ARBITRUM_RPC_URL}" | |
| local = "http://localhost:8545" | |
| [etherscan] | |
| mainnet = { key = "${ETHERSCAN_API_KEY}" } | |
| sepolia = { key = "${ETHERSCAN_API_KEY}" } | |
| base = { key = "${BASESCAN_API_KEY}" } | |
| arbitrum = { key = "${ARBISCAN_API_KEY}" } | |
| [fmt] | |
| line_length = 100 | |
| tab_width = 4 | |
| bracket_spacing = false | |
| EOF | |
| # Create .env.example | |
| cat > .env.example << 'EOF' | |
| # RPC URLs | |
| MAINNET_RPC_URL=https://eth.llamarpc.com | |
| SEPOLIA_RPC_URL=https://rpc.sepolia.org | |
| BASE_RPC_URL=https://mainnet.base.org | |
| ARBITRUM_RPC_URL=https://arb1.arbitrum.io/rpc | |
| # API Keys for verification | |
| ETHERSCAN_API_KEY=your_etherscan_api_key | |
| BASESCAN_API_KEY=your_basescan_api_key | |
| ARBISCAN_API_KEY=your_arbiscan_api_key | |
| # Deployment | |
| PRIVATE_KEY=your_private_key_here | |
| EOF | |
| # Create .gitignore additions | |
| cat >> .gitignore << 'EOF' | |
| # Environment | |
| .env | |
| .env.local | |
| # Cache | |
| cache/ | |
| out/ | |
| # Coverage | |
| lcov.info | |
| coverage/ | |
| EOF | |
| # Create MockPriceFeed for Chainlink testing | |
| if [ "$WITH_CHAINLINK" = true ]; then | |
| cat > src/mocks/MockPriceFeed.sol << 'EOF' | |
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| contract MockPriceFeed { | |
| int256 private _price; | |
| uint8 private _decimals; | |
| constructor(int256 initialPrice, uint8 decimals_) { | |
| _price = initialPrice; | |
| _decimals = decimals_; | |
| } | |
| function latestRoundData() | |
| external | |
| view | |
| returns ( | |
| uint80 roundId, | |
| int256 answer, | |
| uint256 startedAt, | |
| uint256 updatedAt, | |
| uint80 answeredInRound | |
| ) | |
| { | |
| return (1, _price, block.timestamp, block.timestamp, 1); | |
| } | |
| function decimals() external view returns (uint8) { | |
| return _decimals; | |
| } | |
| function setPrice(int256 newPrice) external { | |
| _price = newPrice; | |
| } | |
| } | |
| EOF | |
| mkdir -p src/mocks | |
| fi | |
| # Create example contract | |
| cat > src/Example.sol << 'EOF' | |
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| contract Example { | |
| mapping(address => uint256) public values; | |
| event ValueSet(address indexed user, uint256 value); | |
| error ZeroValue(); | |
| function setValue(uint256 _value) external { | |
| if (_value == 0) revert ZeroValue(); | |
| values[msg.sender] = _value; | |
| emit ValueSet(msg.sender, _value); | |
| } | |
| function getValue(address _user) external view returns (uint256) { | |
| return values[_user]; | |
| } | |
| } | |
| EOF | |
| # Create test file | |
| cat > test/Example.t.sol << 'EOF' | |
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| import "forge-std/Test.sol"; | |
| import "../src/Example.sol"; | |
| contract ExampleTest is Test { | |
| Example public example; | |
| address public alice = makeAddr("alice"); | |
| address public bob = makeAddr("bob"); | |
| event ValueSet(address indexed user, uint256 value); | |
| function setUp() public { | |
| example = new Example(); | |
| } | |
| function test_SetValue() public { | |
| vm.prank(alice); | |
| example.setValue(100); | |
| assertEq(example.getValue(alice), 100); | |
| } | |
| function test_SetValue_EmitsEvent() public { | |
| vm.expectEmit(true, false, false, true); | |
| emit ValueSet(alice, 100); | |
| vm.prank(alice); | |
| example.setValue(100); | |
| } | |
| function test_RevertWhen_ZeroValue() public { | |
| vm.prank(alice); | |
| vm.expectRevert(Example.ZeroValue.selector); | |
| example.setValue(0); | |
| } | |
| function testFuzz_SetValue(uint256 value) public { | |
| vm.assume(value > 0); | |
| vm.prank(alice); | |
| example.setValue(value); | |
| assertEq(example.getValue(alice), value); | |
| } | |
| function test_MultipleUsers() public { | |
| vm.prank(alice); | |
| example.setValue(100); | |
| vm.prank(bob); | |
| example.setValue(200); | |
| assertEq(example.getValue(alice), 100); | |
| assertEq(example.getValue(bob), 200); | |
| } | |
| } | |
| EOF | |
| # Create deploy script | |
| cat > script/Deploy.s.sol << 'EOF' | |
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| import "forge-std/Script.sol"; | |
| import "../src/Example.sol"; | |
| contract DeployScript is Script { | |
| function run() external returns (Example) { | |
| uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); | |
| vm.startBroadcast(deployerPrivateKey); | |
| Example example = new Example(); | |
| vm.stopBroadcast(); | |
| console.log("Example deployed to:", address(example)); | |
| return example; | |
| } | |
| } | |
| EOF | |
| # Create local deploy script (uses Anvil default key) | |
| cat > script/DeployLocal.s.sol << 'EOF' | |
| // SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| import "forge-std/Script.sol"; | |
| import "../src/Example.sol"; | |
| contract DeployLocalScript is Script { | |
| // Anvil default account #0 private key | |
| uint256 constant ANVIL_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; | |
| function run() external returns (Example) { | |
| vm.startBroadcast(ANVIL_PRIVATE_KEY); | |
| Example example = new Example(); | |
| vm.stopBroadcast(); | |
| console.log("Example deployed to:", address(example)); | |
| return example; | |
| } | |
| } | |
| EOF | |
| # Create Makefile for common commands | |
| cat > Makefile << 'EOF' | |
| -include .env | |
| .PHONY: all test clean deploy anvil | |
| all: clean build | |
| # Build | |
| build: | |
| forge build | |
| clean: | |
| forge clean | |
| # Testing | |
| test: | |
| forge test | |
| test-v: | |
| forge test -vvv | |
| test-gas: | |
| forge test --gas-report | |
| coverage: | |
| forge coverage | |
| # Local development | |
| anvil: | |
| anvil | |
| deploy-local: | |
| forge script script/DeployLocal.s.sol --rpc-url http://localhost:8545 --broadcast | |
| # Testnet deployment | |
| deploy-sepolia: | |
| forge script script/Deploy.s.sol --rpc-url $(SEPOLIA_RPC_URL) --broadcast --verify | |
| deploy-base: | |
| forge script script/Deploy.s.sol --rpc-url $(BASE_RPC_URL) --broadcast --verify | |
| # Mainnet deployment | |
| deploy-mainnet: | |
| forge script script/Deploy.s.sol --rpc-url $(MAINNET_RPC_URL) --broadcast --verify | |
| # Formatting | |
| fmt: | |
| forge fmt | |
| fmt-check: | |
| forge fmt --check | |
| # Snapshots | |
| snapshot: | |
| forge snapshot | |
| snapshot-diff: | |
| forge snapshot --diff | |
| EOF | |
| echo "" | |
| echo "β Project '$PROJECT_NAME' created!" | |
| echo "" | |
| echo "π Structure:" | |
| echo " src/Example.sol - Example contract" | |
| echo " test/Example.t.sol - Test file with patterns" | |
| echo " script/Deploy.s.sol - Production deploy" | |
| echo " script/DeployLocal.s.sol - Local Anvil deploy" | |
| if [ "$WITH_CHAINLINK" = true ]; then | |
| echo " src/mocks/MockPriceFeed.sol - Chainlink mock" | |
| fi | |
| echo "" | |
| echo "π Quick start:" | |
| echo " cd $PROJECT_NAME" | |
| echo " cp .env.example .env" | |
| echo " make test # Run tests" | |
| echo " make anvil # Start local node (in another terminal)" | |
| echo " make deploy-local # Deploy to Anvil" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment