| name | evm-foundry |
| description | Foundry development for EVM chains including Celo. Use when working with forge, cast, anvil, writing Solidity contracts, testing, deploying, or verifying contracts with Foundry. |
| license | Apache-2.0 |
| metadata | {"author":"celo-org","version":"1.0.0"} |
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
curl -L https://foundry.paradigm.xyz | bash
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
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
forge build
forge clean && forge build
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
forge test
forge test -vvv
forge test --match-test test_SetName
forge test --match-contract MyContractTest
forge test --gas-report
forge coverage
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
forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --broadcast
forge script script/Deploy.s.sol --rpc-url https://forno.celo-sepolia.celo-testnet.org --broadcast --private-key $PRIVATE_KEY
forge script script/Deploy.s.sol --rpc-url https://forno.celo.org --broadcast --private-key $PRIVATE_KEY
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
forge create src/MyContract.sol:MyContract \
--rpc-url https://forno.celo-sepolia.celo-testnet.org \
--private-key $PRIVATE_KEY \
--constructor-args "My Contract Name"
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
forge verify-contract \
--chain-id 11142220 \
<CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract \
--etherscan-api-key $CELOSCAN_API_KEY \
--watch
forge verify-contract \
--chain-id 42220 \
<CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract \
--etherscan-api-key $CELOSCAN_API_KEY \
--watch
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
cast balance <ADDRESS> --rpc-url https://forno.celo.org
cast call <CONTRACT_ADDRESS> "name()(string)" --rpc-url https://forno.celo.org
cast storage <CONTRACT_ADDRESS> 0 --rpc-url https://forno.celo.org
cast block-number --rpc-url https://forno.celo.org
Writing Data
cast send <CONTRACT_ADDRESS> "setName(string)" "New Name" \
--rpc-url https://forno.celo.org \
--private-key $PRIVATE_KEY
cast send <TO_ADDRESS> --value 1ether \
--rpc-url https://forno.celo.org \
--private-key $PRIVATE_KEY
Utility Commands
cast calldata "setName(string)" "New Name"
cast 4byte-decode <CALLDATA>
cast to-wei 1 ether
cast from-wei 1000000000000000000
cast abi-encode "constructor(string)" "My Contract Name"
Anvil (Local Node)
anvil
anvil --fork-url https://forno.celo.org
anvil --fork-url https://forno.celo.org --fork-block-number 12345678
Block Explorers
Additional Resources