Skip to content

Instantly share code, notes, and snippets.

@benwoody
Last active December 18, 2025 00:09
Show Gist options
  • Select an option

  • Save benwoody/812538a1b5b7338e7188a982fee9f8a6 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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