Foundry Development for EVM Chains
This skill covers Foundry setup and development for EVM-compatible chains with emphasis on Celo.
When to Use
- Setting up a new Foundry project
- Writing and compiling Solidity smart contracts with forge
- Testing contracts with forge test
- Deploying contracts to Celo or other EVM chains
- Verifying contracts on block explorers
- Interacting with contracts using cast
Foundry Tools
| Tool | Purpose |
|---|---|
| forge | Build, test, debug, deploy, and verify smart contracts |
| cast | Interact with contracts and retrieve chain data from CLI |
| anvil | Run a local Ethereum development node with forking support |
| chisel | Fast Solidity REPL for interactive development |
Installation
# Install Foundryup (the Foundry installer)
curl -L https://foundry.paradigm.xyz | bash
# Install Foundry tools
foundryup
For Windows, use Git BASH or WSL (Foundryup doesn't support PowerShell or Command Prompt).
Quick Start
Initialize Project
forge init my-project
cd my-project
Project Structure
my-project/
├── foundry.toml # Configuration
├── src/ # Contract source files
│ └── Counter.sol
├── test/ # Test files
│ └── Counter.t.sol
├── script/ # Deployment scripts
│ └── Counter.s.sol
└── lib/ # Dependencies
Celo Network Information
| Network | Chain ID | RPC Endpoint |
|---|---|---|
| Celo Mainnet | 42220 | https://forno.celo.org |
| Celo Sepolia | 11142220 | https://forno.celo-sepolia.celo-testnet.org |
Configuration
foundry.toml for Celo
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.28"
optimizer = true
optimizer_runs = 200
[rpc_endpoints]
celo = "https://forno.celo.org"
celo_sepolia = "https://forno.celo-sepolia.celo-testnet.org"
localhost = "http://127.0.0.1:8545"
[etherscan]
celo = { key = "${CELOSCAN_API_KEY}", chain = 42220, url = "https://api.celoscan.io/api" }
celo_sepolia = { key = "${CELOSCAN_API_KEY}", chain = 11142220, url = "https://api.etherscan.io/v2/api" }
Environment Variables (.env)
PRIVATE_KEY=your_private_key_here
CELOSCAN_API_KEY=your_celoscan_api_key_here
Load environment variables:
source .env
Writing Contracts
Basic Contract
// src/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
contract MyContract {
string public name;
address public owner;
event NameChanged(string oldName, string newName);
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
constructor(string memory _name) {
name = _name;
owner = msg.sender;
}
function setName(string memory _newName) external onlyOwner {
string memory oldName = name;
name = _newName;
emit NameChanged(oldName, _newName);
}
}
Using OpenZeppelin
forge install OpenZeppelin/openzeppelin-contracts
Add to remappings.txt:
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
// src/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
Compilation
# Compile all contracts
forge build
# Clean and rebuild
forge clean && forge build
# Check contract sizes
forge build --sizes
Testing
Test File Structure
// test/MyContract.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Test, console} from "forge-std/Test.sol";
import {MyContract} from "../src/MyContract.sol";
contract MyContractTest is Test {
MyContract public myContract;
address public owner;
address public user;
function setUp() public {
owner = address(this);
user = makeAddr("user");
myContract = new MyContract("Initial Name");
}
function test_InitialName() public view {
assertEq(myContract.name(), "Initial Name");
}
function test_Owner() public view {
assertEq(myContract.owner(), owner);
}
function test_SetName() public {
myContract.setName("New Name");
assertEq(myContract.name(), "New Name");
}
function test_SetNameEmitsEvent() public {
vm.expectEmit(true, true, true, true);
emit MyContract.NameChanged("Initial Name", "New Name");
myContract.setName("New Name");
}
function test_RevertWhen_NonOwnerSetsName() public {
vm.prank(user);
vm.expectRevert("Not owner");
myContract.setName("Hacked");
}
function testFuzz_SetName(string memory newName) public {
myContract.setName(newName);
assertEq(myContract.name(), newName);
}
}
Running Tests
# Run all tests
forge test
# Run with verbosity (show logs)
forge test -vvv
# Run specific test
forge test --match-test test_SetName
# Run specific contract
forge test --match-contract MyContractTest
# Run with gas reporting
forge test --gas-report
# Run with coverage
forge coverage
# Fork testing against Celo mainnet
forge test --fork-url https://forno.celo.org
Deployment
Deployment Script
// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {Script, console} from "forge-std/Script.sol";
import {MyContract} from "../src/MyContract.sol";
contract DeployScript is Script {
function setUp() public {}
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
MyContract myContract = new MyContract("My Contract Name");
console.log("Contract deployed to:", address(myContract));
vm.stopBroadcast();
}
}
Deploy Commands
# Deploy to local Anvil
forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --broadcast
# Deploy to Celo Sepolia
forge script script/Deploy.s.sol --rpc-url https://forno.celo-sepolia.celo-testnet.org --broadcast --private-key $PRIVATE_KEY
# Deploy to Celo Mainnet
forge script script/Deploy.s.sol --rpc-url https://forno.celo.org --broadcast --private-key $PRIVATE_KEY
# Deploy with verification
forge script script/Deploy.s.sol --rpc-url https://forno.celo-sepolia.celo-testnet.org --broadcast --verify --private-key $PRIVATE_KEY
Quick Deploy with forge create
# Deploy to Celo Sepolia
forge create src/MyContract.sol:MyContract \
--rpc-url https://forno.celo-sepolia.celo-testnet.org \
--private-key $PRIVATE_KEY \
--constructor-args "My Contract Name"
# Deploy with verification
forge create src/MyContract.sol:MyContract \
--rpc-url https://forno.celo-sepolia.celo-testnet.org \
--private-key $PRIVATE_KEY \
--constructor-args "My Contract Name" \
--verify \
--etherscan-api-key $CELOSCAN_API_KEY
Verification
Verify Existing Contract
# Verify on Celo Sepolia
forge verify-contract \
--chain-id 11142220 \
<CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract \
--etherscan-api-key $CELOSCAN_API_KEY \
--watch
# Verify on Celo Mainnet
forge verify-contract \
--chain-id 42220 \
<CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract \
--etherscan-api-key $CELOSCAN_API_KEY \
--watch
# With constructor arguments
forge verify-contract \
--chain-id 11142220 \
<CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract \
--etherscan-api-key $CELOSCAN_API_KEY \
--constructor-args $(cast abi-encode "constructor(string)" "My Contract Name") \
--watch
Cast Commands
Reading Data
# Get balance
cast balance <ADDRESS> --rpc-url https://forno.celo.org
# Call view function
cast call <CONTRACT_ADDRESS> "name()(string)" --rpc-url https://forno.celo.org
# Get storage slot
cast storage <CONTRACT_ADDRESS> 0 --rpc-url https://forno.celo.org
# Get block number
cast block-number --rpc-url https://forno.celo.org
Writing Data
# Send transaction
cast send <CONTRACT_ADDRESS> "setName(string)" "New Name" \
--rpc-url https://forno.celo.org \
--private-key $PRIVATE_KEY
# Transfer CELO
cast send <TO_ADDRESS> --value 1ether \
--rpc-url https://forno.celo.org \
--private-key $PRIVATE_KEY
Utility Commands
# Encode function call
cast calldata "setName(string)" "New Name"
# Decode function call
cast 4byte-decode <CALLDATA>
# Convert units
cast to-wei 1 ether
cast from-wei 1000000000000000000
# Get ABI-encoded constructor args
cast abi-encode "constructor(string)" "My Contract Name"
Anvil (Local Node)
# Start local node
anvil
# Start with fork of Celo mainnet
anvil --fork-url https://forno.celo.org
# Start with specific block
anvil --fork-url https://forno.celo.org --fork-block-number 12345678
Block Explorers
- Celo Mainnet: https://celoscan.io
- Celo Sepolia: https://sepolia.celoscan.io
Additional Resources
- foundry-config.md - Detailed configuration options
- testing-patterns.md - Advanced testing patterns
- security-checklist.md - Security best practices