Integration Testing with Hardhat Mainnet Forking
Learn how to use Hardhat mainnet forking to run integration tests against live protocol state using block pinning, account impersonation, and EVM snapshots.
04 Jun 2026, 12:56 UTC

The Problem: Mocking Complex Protocol State
Writing integration tests for DeFi or governance protocols often requires simulating interactions with existing live contracts like Uniswap, Aave, or Chainlink. Creating manual mocks for these protocols is error-prone, time-consuming, and fails to capture the actual state of the blockchain (e.g., current liquidity pools or oracle prices).
The solution is Mainnet Forking. This allows the Hardhat Network to act as a local proxy for a live network. When the local node needs data it doesn't have, it fetches the state from a remote archive node, allowing you to test against production bytecode and real-world state without spending actual ETH.
Prerequisites
- Hardhat Project: A project initialized with
npx hardhatusing TypeScript. - Archive Node RPC: An API key from a provider (e.g., Alchemy, Infura, or QuickNode) that supports archive data. Standard nodes often prune historical state, which will cause tests to fail when accessing older blocks.
- Environment Variables: A
.envfile to store your RPC URL to avoid leaking credentials in source control.
Configuring the Fork
To enable forking, modify your hardhat.config.ts. It is critical to pin the blockNumber; otherwise, your tests will run against the latest block, leading to non-deterministic results as the live chain moves forward.
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";
dotenv.config();
const config: HardhatUserConfig = {
solidity: "0.8.24",
networks: {
hardhat: {
forking: {
url: process.env.FORK_RPC_URL || "",
blockNumber: 19000000, // Pinning ensures reproducible CI runs
},
},
},
};
export default config;
Executing Transactions as Production Addresses
Forking allows you to interact with the chain, but you do not have the private keys for mainnet "whales" or governance multisigs. Hardhat provides the hardhat_impersonateAccount method to bypass signature requirements for specific addresses.
Warning: Impersonated accounts must have enough ETH to pay for gas. Since you are on a local fork, you can artificially fund them using hardhat_setBalance.
// Run this within your test suite
const whaleAddress = "0x..."; // A known high-balance address
// 1. Impersonate the account
await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [whaleAddress],
});
// 2. Fund the account (if necessary)
await hre.network.provider.send("hardhat_setBalance", [
whaleAddress,
"0x1000000000000000000", // 1 ETH in wei
]);
// 3. Connect the signer to the impersonated address
const whaleSigner = await hre.ethers.getSigner(whaleAddress);
// Execute transaction
await myContract.connect(whaleSigner).transfer(recipient, amount);
// 4. Stop impersonating to clean up state
await hre.network.provider.request({
method: "hardhat_stopImpersonatingAccount",
params: [whaleAddress],
});
Optimizing Test Speed with Snapshots
Re-forking the network for every test file is slow. Instead, use EVM Snapshots. This captures the state of the blockchain at a specific point and allows you to revert to it instantly, clearing all state changes, balance modifications, and contract deployments.
Implement this in your test hooks:
beforeEach: Take a snapshot of the current state.afterEach: Revert to that snapshot.
let snapshotId: string;
beforeEach(async () => {
snapshotId = await hre.network.provider.send("evm_snapshot", []);
});
afterEach(async () => {
await hre.network.provider.send("evm_revert", [snapshotId]);
});
Verification and Diagnostics
To verify that your fork is operating at the correct block height and not drifting, add a check at the start of your test suite:
const currentBlock = await hre.ethers.provider.getBlockNumber();
console.log(`Forked Block Height: ${currentBlock}`);
// Assert that currentBlock === 19000000
If you encounter unexpected behavior, run your tests with the Hardhat debug flag to see the raw RPC requests being sent to your provider:
DEBUG=hardhat:* npx hardhat test
Limitations and Risks
| Limitation | Impact | Workaround |
|---|---|---|
| Mempool Absence | Pending transactions and MEV dynamics are not simulated. | Test logic only; use a dedicated testnet for MEV testing. |
| RPC Pruning | Pinned blocks older than ~128 blocks may fail on some providers. | Verify your provider's archive node retention policy. |
| Memory Usage | Large forks with deep state increase local node RAM usage. | Use hardhat_node --fork-block-number for a persistent process. |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.