Foundry Testing & Script Skill
Rules and patterns for Foundry tests. Find examples in the actual codebase.
Bundled References
| Reference | Content | When to Read |
|---|---|---|
./references/test-infrastructure.md | Constants, defaults, mocks | When setting up tests |
./references/cheat-codes.md | Common cheatcode patterns | When using vm cheatcodes |
./references/invariant-patterns.md | Handlers, stores, invariants | When writing invariant tests |
./references/formal-verification.md | Halmos, Certora, symbolic exec | When proving correctness |
./references/deployment-scripts.md | Script patterns, verification | When writing deploy scripts |
./references/deployment-checklist.md | Pre-mainnet deployment steps | Before deploying to production |
./references/gas-benchmarking.md | Snapshot, profiling, CI | When measuring gas performance |
./references/sablier-conventions.md | Sablier-specific patterns | When working in Sablier repos |
Test Types
| Type | Directory | Naming | Purpose |
|---|---|---|---|
| Integration | tests/integration/concrete/ | *.t.sol | BTT-based concrete tests |
| Fuzz | tests/integration/fuzz/ | *.t.sol | Property-based testing |
| Fork | tests/fork/ | *.t.sol | Mainnet state testing |
| Invariant | tests/invariant/ | Invariant*.t.sol | Stateful protocol properties |
| Scripts | scripts/solidity/ | *.s.sol | Deployment/initialization |
1. Integration Tests (Concrete)
Naming Convention
| Pattern | Usage |
|---|---|
test_RevertWhen_{Condition} | Revert on input |
test_RevertGiven_{State} | Revert on state |
test_When_{Condition} | Success path |
Rules
- •Stack modifiers to document BTT path (modifiers are often empty - just document the path)
- •Expect events BEFORE action -
vm.expectEmit()then call function - •Assert state AFTER action - Check state changes after function executes
- •Use revert helpers for common patterns (
expectRevert_DelegateCall,expectRevert_Null) - •Named parameters in assertions -
assertEq(actual, expected, "description")
Mock Rules
- •Place all mocks in
tests/mocks/ - •One mock per scenario (not one mega-mock)
- •Naming:
*Good,*Reverting,*InvalidSelector,*Reentrant
2. Fuzz Tests
Naming Convention
testFuzz_{FunctionName}_{Scenario}
Rules
- •Bound before assume -
_bound()is more efficient thanvm.assume() - •Bound in dependency order - Independent params first, then dependent
- •Never hardcode params with validation constraints
- •Document fuzzed scenarios in NatSpec
Bounding Pattern
solidity
// 1. Bound independent params first cliffDuration = boundUint40(cliffDuration, 0, MAX - 1); // 2. Bound dependent params based on constraints totalDuration = boundUint40(totalDuration, cliffDuration + 1, MAX);
3. Fork Tests
Rules
- •Create fork with
vm.createSelectFork("ethereum") - •Use
deal()to give tokens to test users - •Use
assumeNoBlacklisted()for USDC/USDT - •Use
forceApprove()for non-standard tokens (USDT)
Token Quirks
| Token | Issue | Solution |
|---|---|---|
| USDC/USDT | Blacklist | assumeNoBlacklisted() |
| USDT | Non-standard | forceApprove() |
| Fee-on-transfer | Balance diff | Check actual received amount |
4. Invariant Tests
Architecture
code
tests/invariant/ ├── handlers/ # State manipulation (call functions with bounded params) ├── stores/ # State tracking (record totals, IDs) └── Invariant.t.sol
Rules
- •Target handlers only -
targetContract(address(handler)) - •Exclude protocol contracts -
excludeSender(address(vault)) - •Use stores to track totals for invariant assertions
- •Early return in handlers if preconditions not met
5. Solidity Scripts
Rules
- •Inherit from
BaseScriptwithbroadcastmodifier - •Use env vars:
ETH_FROM,MNEMONIC - •Simulation first, then broadcast
Commands
bash
# Simulation forge script scripts/Deploy.s.sol --sig "run(...)" ARGS --rpc-url $RPC # Broadcast forge script scripts/Deploy.s.sol --sig "run(...)" ARGS --rpc-url $RPC --broadcast --verify
Running Tests
bash
# By type forge test --match-path "tests/integration/concrete/**" forge test --match-path "tests/fork/**" forge test --match-contract Invariant_Test # Specific test forge test --match-test test_WhenCallerRecipient -vvvv # Fuzz with more runs forge test --match-test testFuzz_ --fuzz-runs 1000 # Coverage forge coverage --report lcov
Debugging
Verbosity Levels
| Flag | Shows |
|---|---|
-v | Logs for failing tests |
-vv | Logs for all tests |
-vvv | Stack traces for failures |
-vvvv | Stack traces + setup traces |
-vvvvv | Full execution traces |
Console Logging
solidity
import { console2 } from "forge-std/console2.sol";
console2.log("value:", someValue);
console2.log("address:", someAddress);
console2.logBytes32(someBytes32);
Debugging Commands
bash
# Trace specific failing test forge test --match-test test_MyTest -vvvv # Gas report for a test forge test --match-test test_MyTest --gas-report # Debug in interactive debugger forge debug --debug tests/MyTest.t.sol --sig "test_MyTest()" # Inspect storage layout forge inspect MyContract storage-layout
Debugging Tips
- •Label addresses -
vm.label(addr, "Recipient")for readable traces - •Check state with logs - Add
console2.logbefore reverts - •Isolate failures - Run single test with
--match-test - •Compare gas - Use
--gas-reportto spot unexpected costs - •Snapshot comparisons - Use
vm.snapshot()/vm.revertTo()to isolate state changes
Best Practices Summary
- •Use constants from
Defaults/Constants- never hardcode - •Specialized mocks - one per scenario, all in
tests/mocks/ - •Modifiers in
Modifiers.sol- centralize BTT path modifiers - •Label addresses with
vm.label()for traces - •Events before actions -
vm.expectEmit()then call - •Bound before assume - more efficient
External References
Example Invocations
Test this skill with these prompts:
- •Integration test: "Write a concrete test for
withdrawthat expectsErrors.Flow_Overdrawwhen amount exceeds balance" - •Fuzz test: "Create a fuzz test for
depositthat bounds amount between 1 and type(uint128).max" - •Fork test: "Write a fork test for USDC deposits on mainnet with blacklist handling"
- •Invariant test: "Create an invariant handler for the
depositandwithdrawfunctions" - •Deploy script: "Write a deployment script for SablierFlow with verification"