Foundry’s fork mode lets you pin a local node to the exact state of any EVM network at a given block. You interact with it as if you were on the real network, but nothing is broadcast and nothing costs real tokens.
This is the fastest way to test against real contract state, real liquidity and real addresses — without a faucet.
When to use a fork
- Testing interactions with deployed contracts (DEXs, lending protocols, bridges)
- Reproducing a bug that only happens with mainnet/testnet state
- Running integration tests without draining faucet ETH
- Simulating a specific block before or after an event
1. Start a local fork
anvil --fork-url https://rpc.sepolia.org
Anvil starts a local node at http://localhost:8545 with the current Sepolia state. It also creates 10 funded test accounts automatically.
Fork at a specific block for reproducible tests:
anvil --fork-url https://rpc.sepolia.org --fork-block-number 7500000
2. Use the fork in tests
In Foundry tests, you can fork inside the test itself without running Anvil separately:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
interface IERC20 {
function balanceOf(address) external view returns (uint256);
function transfer(address, uint256) external returns (bool);
}
contract ForkTest is Test {
// A real USDC contract on Sepolia
IERC20 constant USDC = IERC20(0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238);
address constant WHALE = 0x36B5a8D677a7BCB45c02Bb6FCeC9Bb12A5B4E4f;
uint256 sepoliaFork;
function setUp() public {
sepoliaFork = vm.createFork("https://rpc.sepolia.org");
vm.selectFork(sepoliaFork);
}
function test_USDCBalance() public view {
uint256 balance = USDC.balanceOf(WHALE);
assertGt(balance, 0, "Whale should have USDC");
}
function test_TransferFromWhale() public {
vm.prank(WHALE); // impersonate the whale
USDC.transfer(address(this), 1000e6);
assertEq(USDC.balanceOf(address(this)), 1000e6);
}
}
Run it:
forge test --fork-url https://rpc.sepolia.org -vv
3. Impersonate any address
vm.prank lets you send a transaction as any address — no private key needed:
function test_ImpersonateOwner() public {
address owner = someContract.owner();
vm.prank(owner);
someContract.doAdminThing();
}
For multiple calls, use vm.startPrank / vm.stopPrank:
vm.startPrank(owner);
someContract.step1();
someContract.step2();
vm.stopPrank();
4. Manipulate state
Override any storage slot or balance:
// Give ETH to any address
vm.deal(address(this), 10 ether);
// Override an ERC-20 balance directly
deal(address(USDC), address(this), 1000e6);
// Override any storage slot
vm.store(contractAddress, storageSlot, newValue);
5. Multi-fork tests
Test across two networks in the same test:
function test_MultiFork() public {
uint256 sepoliaFork = vm.createFork("https://rpc.sepolia.org");
uint256 baseFork = vm.createFork("https://sepolia.base.org");
vm.selectFork(sepoliaFork);
// ... interact with Sepolia state
vm.selectFork(baseFork);
// ... interact with Base Sepolia state
}
6. Store RPC URLs in foundry.toml
Avoid hardcoding URLs in tests:
[rpc_endpoints]
sepolia = "${SEPOLIA_RPC_URL}"
base_sepolia = "${BASE_SEPOLIA_RPC_URL}"
Then in tests:
sepoliaFork = vm.createFork("sepolia");
7. Deploy against a fork
You can also run a deployment script against your local fork to preview what would happen:
source .env
# Start fork in background
anvil --fork-url $SEPOLIA_RPC_URL &
# Deploy to local fork (no --broadcast = dry run)
forge script script/Deploy.s.sol \
--rpc-url http://localhost:8545 \
--private-key $PRIVATE_KEY
Performance tips
| Tip | Why |
|---|---|
Use --fork-block-number |
Prevents re-fetching state on each run |
Set FOUNDRY_ETH_RPC_URL env var |
Avoids passing --fork-url every time |
Use vm.createSelectFork |
Combines createFork + selectFork in one call |
Cache with --fork-cache-dir |
Stores RPC responses locally for faster reruns |
anvil --fork-url $SEPOLIA_RPC_URL \
--fork-block-number 7500000 \
--fork-cache-dir .fork-cache