Fork Mainnet Locally With Hardhat Network For Realistic Contract Testing
Use Hardhat Network forking to run a local EVM that mirrors mainnet state at a pinned block, enabling realistic integration tests against real deployed contracts without spending ETH.
21 Jun 2026, 05:43 UTC

Testing a new contract against a mock Uniswap router is fast. It is also misleading. Mocks drift from real token decimals, fee tiers, and the actual liquidity that exists at a given block. Hardhat Network forking solves this by running a local EVM that mirrors mainnet state at a chosen block, so you can call real deployed contracts without spending real ETH.
The composability testing gap
When your code integrates with existing protocols, the interesting edge cases live in real state: a pool with low liquidity, a governance timelock that is active, a whale balance that gates an action. Deploying faithful mocks for these conditions is time‑consuming and fragile.
Forking gives you a local, volatile copy of mainnet. Reads hit the forked state. Writes stay local and are lost when the node restarts. That is the useful takeaway: you get realism for reads and safe experimentation for writes.
How forking works in Hardhat Network
Hardhat Network is the in‑process Ethereum node Hardhat starts for tests. With a forking configuration it bootstraps state from a remote JSON‑RPC provider, then serves subsequent requests locally.
Configuration lives in hardhat.config.js under the networks.hardhat object. The key fields are forking.url for the provider and an optional forking.blockNumber to pin state.
module.exports = {
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY",
blockNumber: 19000000
}
}
}
};
Replace YOUR_API_KEY with a valid key for Alchemy, Infura, or another provider that supports eth_getProof and archive access. Pinning a block number makes tests deterministic because mainnet state is immutable at that height.
Start the forked node from a terminal in the project root. No special permissions are required beyond network access to the provider.
npx hardhat node
Once running, Hardhat Network will serve the forked state. A read‑only call to a known mainnet address should return values consistent with that block.
Worked example: calling a real contract and impersonating an account
A common pattern is to verify state retrieval then simulate a privileged action without holding private keys. Hardhat provides hardhat_impersonateAccount for the second part.
Example script outline to run with npx hardhat run scripts/check.js --network hardhat:
const { ethers } = require("hardhat");
async function main() {
const target = "0x..."; // mainnet contract address
const abi = ["function name() view returns (string)"];
const contract = new ethers.Contract(target, abi, ethers.provider);
const name = await contract.name();
console.log("name:", name);
}
main();
Expected check: the returned name matches the on‑chain value at the forked block. If it does not, verify the provider URL is reachable, the block number is valid, and the RPC supports archive calls.
Impersonation example:
const whale = "0x..."; // existing mainnet address
await ethers.provider.send("hardhat_impersonateAccount", [whale]);
const signer = await ethers.getSigner(whale);
// use signer to send a local transaction
await ethers.provider.send("hardhat_stopImpersonatingAccount", [whale]);
Risk note: impersonation bypasses private key requirements inside the fork. It is only possible because the node is local and under your control. Never treat this as a security feature.
Trade‑offs and limits
- Forking depends on a stable remote JSON‑RPC connection. Latency and rate limiting from the provider slow down the initial state sync and any fallback calls that miss the local cache.
- State simulation consumes local RAM. The amount grows with the number of contracts and storage slots accessed during a session. Large‑scale simulations can exhaust memory on constrained machines.
- All changes are volatile. The fork does not write back to mainnet and is reset on restart. That is intentional, but it means you must re‑apply setup steps each run.
Practical verification: after starting the node, call a read‑only view function on a well‑known contract and compare the result to a block explorer for the pinned block. Then perform a small state‑changing call from an impersonated account and confirm the local balance changes while the real mainnet balance remains unchanged.
Use forking when you need realistic integration tests against real deployed code. Use mocks when you need speed, isolation, and full control over edge cases. Many projects use both.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.