Testing Against Reality: Using Hardhat Network Forking for Mainnet Integration
Stop relying on mocks for external contracts. Learn how to use Hardhat Network Forking to clone mainnet state for deterministic, realistic integration testing.
17 Jan 2026, 19:07 UTC

The Gap Between Local Tests and Mainnet
Writing unit tests for smart contracts usually involves deploying a "mock" version of every external dependency. If your contract interacts with Uniswap, Chainlink, or a governance token, you likely spend more time writing fake versions of those protocols than writing your own business logic. The problem is that mocks are assumptions; they don't account for the actual state of the blockchain or the edge cases of complex external contracts.
The solution is Network Forking. Instead of simulating the world, Hardhat allows you to clone the live state of a network (like Ethereum Mainnet) at a specific block. You get a local environment that behaves exactly like the real chain, but where you have total control over accounts and balances without spending actual ETH.
How Forking Actually Works
When you enable forking, Hardhat doesn't download the entire blockchain. Instead, it uses a remote JSON-RPC provider (such as Alchemy or Infura) as a backend. When your local code requests data from a contract address, Hardhat checks its local cache; if the data isn't there, it fetches the state from the remote provider at the specified block height.
This creates a hybrid environment: the existing state is read-only from the remote provider, but any transactions you send are recorded locally. This means you can "impersonate" a whale account or a contract owner to test administrative functions without needing their private keys.
Implementation: Forking a Specific Block
To ensure your tests are deterministic—meaning they don't fail just because the mainnet moved forward—you should always fork a specific block number. This prevents "flaky" tests caused by state changes on the live network.
Run the following configuration in your hardhat.config.js (assuming Hardhat v2.x):
module.exports = {
solidity: '0.8.24',
networks: {
hardhat: {
forking: {
url: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY',
blockNumber: 19000000 // Pinning the block ensures consistency
}
}
}
};
Worked Example: Testing a DAI Interaction
Suppose you are building a vault that accepts DAI. Instead of deploying a fake DAI token, you can interact with the real DAI contract (0x6b175474e89094c44da98b954eedeac495271d0f). Here is how you would verify the state and manipulate it in a test file:
const { ethers, network } = require("hardhat");
async function main() {
const DAI_ADDRESS = "0x6b175474e89094c44da98b954eedeac495271d0f";
const dai = await ethers.getContractAt("IERC20", DAI_ADDRESS);
// 1. Verify we are on a fork by checking a known balance
const balance = await dai.balanceOf("0x0000000000000000000000000000000000000000");
console.log(`Initial zero-address balance: ${balance.toString()}`);
// 2. Impersonate a whale to fund your test account
const whaleAddress = "0x...some_rich_address...";
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [whaleAddress],
});
// 3. Send DAI from the whale to your local deployer
const signer = await ethers.getSigner(whaleAddress);
const daiWithSigner = dai.connect(signer);
await daiWithSigner.transfer(ethers.getSigner(0).address, ethers.parseEther("1000"));
console.log("Successfully transferred 1000 DAI from mainnet whale to local account");
}
Execution: Run this via npx hardhat run scripts/test-fork.js --network hardhat. You will need a valid API key in your config for this to succeed.
Engineering Trade-offs and Limitations
While powerful, forking introduces specific overheads that can slow down a development pipeline:
- RPC Dependency: Your tests are now dependent on an external API. If your provider hits a rate limit or goes offline, your CI/CD pipeline will fail.
- Latency: Every time your test accesses a piece of state not yet cached locally, Hardhat must make a network request. This makes forked tests significantly slower than pure local unit tests.
- Memory Consumption: Maintaining a local mirror of complex state trees can be memory-intensive, especially during long-running test suites.
Verification and Results
To verify your fork is working correctly, perform a "sanity check" by querying a well-known contract's balance or a governance parameter that is unlikely to change frequently. If the returned value matches a block explorer (like Etherscan) for that specific block number, the fork is synchronized.
To reset the state between tests and avoid side-effect leakage, use the hardhat_reset method in your beforeEach hook:
await network.provider.request({
method: "hardhat_reset",
params: [{ forking: { jsonRpcUrl: "...", blockNumber: 19000000 } }],
});0 replies
A thoughtful contribution can make all the difference. Be the first to share one.