Forking Ethereum Mainnet in Hardhat Network for Local Testing
Learn how to configure Hardhat Network to fork Ethereum mainnet, run tests against real contract state, and avoid common pitfalls.
16 Oct 2025, 17:45 UTC

Useful answer
Hardhat Network can fork Ethereum mainnet so your local tests run against the exact on‑chain contract state at a chosen block, without deploying to a public network. You enable forking in hardhat.config.js by pointing networks.hardhat.forking.url at an Ethereum JSON‑RPC endpoint and optionally pinning a block number.
How forking works
When you start a Hardhat node with forking enabled, Hardhat creates a local blockchain that initially mirrors the state of the forked block. It retrieves account balances, contract code, and storage slots via eth_call and eth_getStorageAt from the RPC endpoint. Subsequent transactions are mined on the local chain only; they do not affect the real Ethereum network. The fork behaves like a regular Hardhat Network, so you can use ethers or web3 as usual.
Configuration example
// hardhat.config.js
require('@nomicfoundation/hardhat-toolbox');
module.exports = {
solidity: '0.8.20',
networks: {
hardhat: {
forking: {
// Load RPC URL from environment; never commit the raw value
url: process.env.ETH_RPC_URL,
// Optional: pin to a specific block number (e.g., a recent stable block)
blockNumber: 18000000,
},
},
},
};
Start the node with:
npx hardhat nodeOr run tests directly against the fork:
npx hardhat test --network hardhatUsing the fork in a test
Once the fork is running, you can interact with contracts exactly as they exist on mainnet at the pinned block:
// test/MyContract.test.js const { ethers } = require('hardhat'); describe('MyContract on forked mainnet', function () { it('reads the token balance of an address', async function () { // Replace with a real mainnet contract address const contractAddress = '0xAbc...def'; const MyContract = await ethers.getContractFactory('MyContract'); const contract = MyContract.attach(contractAddress); // Use the first signer (by default Hardhat provides unlocked accounts) const signer = await ethers.getSigner(); const balance = await contract.balanceOf(signer.getAddress()); console.log(`Balance of ${signer.getAddress()}: ${ethers.formatEther(balance)} ETH`); // Expect a value that matches a block explorer at the forked block }); });Limits and considerations
- Archive‑capable RPC: Reading storage from blocks far in the past requires an RPC node that retains historical state (archive node). If your endpoint is not archive‑enabled, calls for old slots may return
0x0or error. - Gas limits: By default Hardhat gives each block a very high gas limit, effectively unlimited for local testing. You can constrain it via
forking.blockGasLimitif you need to mimic mainnet constraints. - No mempool or network latency: The fork does not simulate pending transactions or the propagation delay of the live network.
- State reset on restart: Each time you stop and restart
npx hardhat node, the forked state is rebuilt from scratch. Persisting the chain across runs requires manual snapshotting or a custom plugin.
Common mistakes to avoid
- Omitting
blockNumber: Without a fixed block, the fork follows the chain tip of the RPC. If the tip changes during your test suite, reads may become inconsistent. Pin a block that matches the state you intend to test. - Committing RPC secrets: The
ETH_RPC_URLoften contains an API key. Load it from environment variables or a secrets manager and add the file containing it to.gitignore. - Assuming identical chain IDs: Hardhat Network defaults to chain ID
31337. If your contract code checksblock.chainidor relies on EIP‑155 replay protection, you may need to setchainId: 1in the forking configuration or adjust the contract. - Expecting impersonated accounts to hold real ETH: When you impersonate a mainnet address via
hardhat_impersonateAccount, the account starts with zero balance unless you explicitly fund it (e.g., withhardhat_setBalance) or configureforking.accountsBalance.
Verification steps
- Check your Hardhat version:
npx hardhat --version. Forking behavior is stable from Hardhat 2.0 onward, but review the release notes for any version‑specific defaults. - Start the node with your forking config, then query the block number locally:
curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://127.0.0.1:8545. Compare the returned block number to theblockNumberyou set. - Pick a known contract (e.g., a popular ERC‑20) and read a storage slot via both the fork and a block explorer at the same block. If the values match, the fork is correctly mirroring state.
- Inspect the Hardhat node logs; you should see outbound RPC requests to the URL you supplied, confirming that state reads are being forwarded rather than mined locally.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.