Fork Test
Generate Foundry fork tests for contracts whose behavior depends on real on-chain state, live liquidity, routers, gauges, or oracle reads.
1. Directory layout
contracts/tests/fork/<category>/<ContractName>/
├── shared/
│ └── Shared.t.sol
└── concrete/
├── Deposit.t.sol
├── Withdraw.t.sol
└── Rebalance.t.sol
Rules:
- fork tests are concrete only; do not add a
fuzz/ directory
- create files only for functions with meaningful on-chain integration behavior
- keep simple setters, access control checks, and pure validation in unit tests
2. Inheritance chain
forge-std/Test
└─ Base
└─ BaseFork
└─ Fork_<Contract>_Shared_Test
└─ Fork_Concrete_<Contract>_<Feature>_Test
Base owns shared actors, constants, IERC20 external token refs, and fork IDs. All typed contract/proxy/mock state variables are declared in each Shared.t.sol using interface types. BaseFork owns chain fork helpers.
Interface-only testing
Same rules as unit tests — use interfaces, not concrete contracts:
- Import interfaces:
IVault, IOToken, IProxy, strategy interfaces from contracts/interfaces/strategies/
- Deploy fresh contracts with
vm.deployCode instead of new (except mocks), and always reference artifact paths through tests/utils/Artifacts.sol (e.g. vm.deployCode(Vaults.OETH, abi.encode(address(weth)))); add the entry to the relevant sub-library if it does not exist yet
- Cast forked addresses to interfaces:
oethVault = IVault(Mainnet.OETH_VAULT)
- Reference events from interfaces:
emit IVault.EventName(...)
Product-specific vault types
| Product | Token | Vault | Chain | Artifacts reference |
|---|
| OUSD | OUSD | OUSDVault | Mainnet | Vaults.OUSD |
| OETH | OETH | OETHVault | Mainnet | Vaults.OETH |
| OETHBase | OETHBase | OETHBaseVault | Base | Vaults.OETH_BASE |
Add the entry to tests/utils/Artifacts.sol if it does not exist yet.
3. Shared setup contract
shared/Shared.t.sol should keep setup in this order:
function setUp() public virtual override {
super.setUp();
_createAndSelectFork<Chain>();
_deployFreshContracts();
_configureContracts();
label();
}
Decision rule:
- deploy fresh contracts that the strategy or vault under test owns or manages
- use forked addresses for external infrastructure such as routers, tokens, factories, and oracles
Pull canonical addresses from tests/utils/Addresses.sol.
4. Concrete test naming
File and contract naming:
concrete/Deposit.t.sol
Fork_Concrete_<ContractName>_Deposit_Test
Function naming patterns:
test_<function>()
test_<function>_<behavior>()
test_<function>_RevertWhen_<condition>()
test_<function>_emits<EventName>()
Casing rules:
- function, behavior, and condition stay
camelCase
RevertWhen is the only PascalCase token in the test name
5. What belongs in fork tests
Fork-test these categories:
- AMO pool interactions
- real router swaps
- oracle reads
- gauge reward flows
- cross-chain and bridge flows
- vault rebases with real balances
- zapper flows
- multi-step end-to-end operations
Do not fork-test:
- simple setters
- straightforward view functions
- access control checks
- constructor validation
- pure math and helper logic
- input-validation-only reverts
Litmus test:
If a mock can faithfully reproduce the behavior, keep it in unit tests.
6. Chain mapping
Use the repository's fork helpers and address libraries consistently:
- Mainnet ->
_createAndSelectForkMainnet()
- Base ->
_createAndSelectForkBase()
- Arbitrum if relevant ->
_createAndSelectForkArbitrum()
Output expectations
When implementing fork tests:
- keep them narrowly focused on real integration value
- prefer a few strong end-to-end tests over broad but redundant coverage
- label both fresh and forked contracts for readable traces
- use interface-only imports; no concrete contract imports except mocks
- deploy fresh contracts with
vm.deployCode, not new (mocks are fine with new), and reference all artifact paths through tests/utils/Artifacts.sol — no inline "contracts/...sol:Name" strings
- mirror existing fork test structure in the nearest comparable test suite before introducing a new pattern