Foundry
Our contract toolchain for new Solidity work. (Two older suites are Hardhat — see
../ekx-hardhat/SKILL.md.)
Authoritative: the foundry-docs MCP server.
Web: https://book.getfoundry.sh
Our foundry.toml
All three repos agree on the core:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
optimizer = true
optimizer_runs = 200
via_ir = true
[rpc_endpoints]
base_sepolia = "${BASE_SEPOLIA_RPC_URL}"
base = "${BASE_RPC_URL}"
[etherscan]
base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" }
base = { key = "${BASESCAN_API_KEY}", url = "https://api.basescan.org/api" }
via_ir = true is required, not optional. Our contracts hit stack-too-deep
without it. It roughly triples compile time — that is the accepted cost. If a build
suddenly takes 3 minutes, this is why; do not "fix" it by turning IR off.
optimizer_runs = 200 favours deploy cost over call cost. Correct for contracts
deployed per-property (an AuctionManager) or per-challenge; raise it for a
singleton called constantly.
Pin your own solc. The OZ copies vendored in lib/ declare 0.8.27/prague
in most suites and 0.8.31/osaka in the newest. Inheriting whatever
lib/ says produces version drift between repos.
Daily commands
forge build
forge test -vvv # -vvv shows console.log + revert reasons
forge test --match-test testBid -vvvv
forge test --gas-report
forge coverage
forge fmt
forge snapshot # gas baseline; commit .gas-snapshot
Fork tests — the right way to test against real USDC:
forge test --fork-url $BASE_SEPOLIA_RPC_URL
Testing patterns
import {Test, console} from "forge-std/Test.sol";
contract AuctionTest is Test {
address alice = makeAddr("alice");
function setUp() public {
auction = new AuctionManager(...);
deal(USDC, alice, 1_000e6); // 6 decimals — USDC is NOT 18
}
function test_RevertWhen_BidTooLow() public {
vm.prank(alice);
vm.expectRevert(AuctionManager.BidTooLow.selector);
auction.bid(1);
}
function testFuzz_BidAccepted(uint96 amount) public {
amount = uint96(bound(amount, 1e6, 1_000e6));
vm.prank(alice);
auction.bid(amount);
}
}
Cheats we lean on: vm.prank / vm.startPrank, vm.warp, vm.roll, deal,
vm.expectRevert(Error.selector), vm.expectEmit, makeAddr, bound (never
vm.assume for ranges — it throws away runs).
Deploying
// script/Deploy.s.sol
contract Deploy is Script {
function run() external {
vm.startBroadcast(vm.envUint("DEPLOYER_PRIVATE_KEY"));
new AuctionFactory(USDC_BASE_SEPOLIA);
vm.stopBroadcast();
}
}
forge script script/Deploy.s.sol:Deploy \
--rpc-url base_sepolia --broadcast --verify -vvvv
Verify on Blockscout too (we read there):
forge verify-contract <ADDR> src/AuctionFactory.sol:AuctionFactory \
--verifier blockscout \
--verifier-url https://base-sepolia.blockscout.com/api
cast — the tool people forget
cast call $AUCTION "highestBid()(uint256)" --rpc-url base_sepolia
cast balance $ADDR --rpc-url base_sepolia
cast send $USDC "approve(address,uint256)" $SPENDER 1000000 \
--rpc-url base_sepolia --private-key $DEPLOYER_PRIVATE_KEY
cast 4byte-decode 0xa9059cbb... # what function is this calldata?
cast sig "transfer(address,uint256)"
cast --to-unit 1000000 6 # 1.0 (USDC sanity check)
Dependencies
forge install OpenZeppelin/openzeppelin-contracts@v5.0.2 # always pin a tag
forge update
Map them in remappings.txt:
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
forge-std/=lib/forge-std/src/
lib/ is a git submodule tree. After cloning any of our contract repos:
git submodule update --init --recursive — otherwise nothing compiles.
Gotchas
via_irmakes builds slow. Expected. Don't disable it.- USDC is 6 decimals.
1e6is one dollar.1e18is a million million dollars and will revert on balance. forge coveragefails withvia_iron some versions — run coverage with--ir-minimum.- Submodules. Missing
lib/is the most common "it doesn't build" report. - Broadcast artifacts in
broadcast/contain deployed addresses. Commit them; they are the deployment record. - Verify on both Basescan and Blockscout — our MCP reads use Blockscout.
Pre-deploy checklist
Short form: testnet first → correct USDC address for the network → verify on both explorers → record the address in the project README → transfer ownership off the deployer EOA.
Related: ../ekx-openzeppelin-upgrades/SKILL.md for
proxies, ../ekx-blockscout/SKILL.md for reads.