Setting Up Mainnet Fork for Testing
A mock Uniswap pool for testing swaps is imitating the interface without imitating the economics. A real Uniswap V3 pool has specific liquidity at specific ticks, specific swap history, specific fee growth. A test passing against a mock can fail against a real pool — because tick spacing is different, or liquidity in the needed range turned out to be zero.
Forking mainnet solves this: we take the real state of all contracts at a specific block and run tests against it locally.
How This Works Technically
Hardhat and Foundry implement forking via JSON-RPC: when accessing an address or storage slot not in local cache, they make eth_getStorageAt / eth_getCode requests to a remote node (Alchemy, Infura, QuickNode) and cache the response. Subsequent accesses to the same slot come from cache.
This means the first test run will be slow (many RPC requests), but subsequent ones — fast (everything in disk cache). Foundry caches to ~/.foundry/cache/, Hardhat — to cache/hardhat-network-fork/.
Setup in Hardhat
// hardhat.config.ts
networks: {
hardhat: {
forking: {
url: process.env.ALCHEMY_MAINNET_URL!,
blockNumber: 19750000, // fix the block
enabled: true,
},
chainId: 1, // important: chainId must match the forked chain
}
}
Fixing blockNumber is mandatory for deterministic tests. Without it, each run fetches a different block, and tests can fail or pass depending on current pool state.
For tests needing fresh data (e.g., Chainlink oracle checks), use hardhat_reset with a new blockNumber right in the test:
await network.provider.request({
method: "hardhat_reset",
params: [{
forking: {
jsonRpcUrl: process.env.ALCHEMY_MAINNET_URL,
blockNumber: 19800000,
}
}]
});
Setup in Foundry
# foundry.toml
[profile.default]
fork_url = "${ALCHEMY_MAINNET_URL}"
fork_block_number = 19750000
Or in a test via vm.createFork() / vm.selectFork() to switch between forks in one test file — convenient for testing cross-chain logic:
uint256 mainnetFork = vm.createFork(vm.envString("ALCHEMY_MAINNET_URL"), 19750000);
uint256 polygonFork = vm.createFork(vm.envString("ALCHEMY_POLYGON_URL"), 55000000);
vm.selectFork(mainnetFork);
// test on Ethereum
vm.selectFork(polygonFork);
// test on Polygon
Manipulating Fork State
Forking gives real data, but tests often need to change it: give an account tokens, change protocol parameters, fast-forward time.
Give ETH to an account:
await network.provider.send("hardhat_setBalance", [
address, ethers.toQuantity(ethers.parseEther("100"))
]);
Give ERC-20 tokens — need to find the storage slot of the balance mapping. For most tokens it's slot 0 or slot 1. The utility hardhat-storage-layout or Foundry cast storage helps find the right slot:
// for USDC, balances are in slot 9
const slot = ethers.keccak256(
ethers.concat([ethers.zeroPadValue(address, 32), ethers.zeroPadValue("0x09", 32)])
);
await network.provider.send("hardhat_setStorageAt", [
USDC_ADDRESS, slot, ethers.toBeHex(amount, 32)
]);
Impersonate account — act on behalf of any address, including multisig or DAO:
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [WHALE_ADDRESS]
});
const whale = await ethers.getSigner(WHALE_ADDRESS);
// now you can call contracts on behalf of whale
Fast-forward time:
await time.increase(86400 * 7); // +7 days
await time.setNextBlockTimestamp(specificTimestamp);
What to Test Via Fork
AMM interactions. Swap via Uniswap V3 with real liquidity, slippage checks, price impact for large volumes. A mock won't reproduce the situation where liquidity in the needed tick range disappeared.
Flash loan attacks. Take a flash loan via Aave V3 (real contract), try to manipulate price, check that your contract's protection works.
Chainlink integration. Check that the contract correctly reads price feeds and handles stale data (staleness check).
Working with real tokens. USDT without return value, stETH with rebasing, USDC with blacklist — all properties of real tokens are present in fork automatically.
RPC and Cache
Good RPC is critical. Alchemy and QuickNode provide archive nodes with history of all storage slots. Infura's free plan is limited and can rate limit on large test suites.
For CI, we recommend caching Foundry/Hardhat cache between runs — this saves 70-80% of time:
- uses: actions/cache@v3
with:
path: ~/.foundry/cache
key: foundry-fork-${{ env.FORK_BLOCK_NUMBER }}
Setting up a fork environment takes 1 business day. Cost is calculated individually.







