Fork Ethereum mainnet locally with Hardhat Network
Run a local Hardhat node that mirrors live mainnet state for contract testing without deploying to a public testnet.
13 May 2026, 15:16 UTC

Useful answer
Hardhat Network forking lets you run a local Ethereum node that mirrors live mainnet state. Your tests can read real token balances, deployed protocol contracts, and on-chain data without deploying to a public testnet or holding mainnet private keys. Any state changes your tests make exist only in the local Hardhat instance; the real network is never touched.
How the fork works
When forking is enabled, Hardhat starts a local JSON-RPC node. Requests for accounts, storage, and block data that are not present locally are forwarded to an upstream Ethereum JSON-RPC provider. Transactions and contract deployments you send to the local node modify only the ephemeral local copy. If you restart the node, the forked state is lost unless you explicitly save snapshots.
Terms: fork block is the historical block height the local chain starts from. Impersonation lets a test send transactions from an address it does not control, such as a large token holder on mainnet.
Worked configuration
This example targets Hardhat 2.x and an Ethereum mainnet RPC endpoint. Configuration keys and supported RPC methods can differ between major versions, so confirm the syntax against your installed Hardhat release notes before adopting it.
module.exports = {
solidity: {
version: "0.8.20",
},
networks: {
hardhat: {
forking: {
url: process.env.MAINNET_RPC_URL,
blockNumber: 18000000,
},
},
},
};
Run this from your project root. Replace MAINNET_RPC_URL with your provider endpoint. No elevated operating-system permissions are required, but treat the URL as a secret because it can be used to read on-chain state and, depending on provider permissions, submit transactions.
Start the local forked node
npx hardhat node
Expected result: Hardhat prints a list of local accounts and starts listening on http://127.0.0.1:8545. Because blockNumber is set, the node should initialize at the configured fork block rather than the latest upstream block.
Verify the fork block
From a second terminal, query the local node:
curl -s -X POST http://127.0.0.1:8545 \
-H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
If the fork is working, the hexadecimal result should correspond to the configured block number. Convert it with a tool such as printf "%d\n" 0x11352A0. If the node returns the latest mainnet block instead, the forking configuration may not have loaded or the provider may not support historical queries at that height.
Read live state in a test
The following script queries a mainnet ERC-20 token through the fork. It does not deploy to mainnet; it reads storage already mirrored by the local node.
import { ethers } from "ethers";
async function main() {
const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
const usdc = new ethers.Contract(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
["function balanceOf(address) view returns (uint256)"],
provider
);
const address = "0x..."; // placeholder: any mainnet address
const balance = await usdc.balanceOf(address);
console.log(address, "USDC balance:", ethers.formatUnits(balance, 6));
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Run it against the local network:
npx hardhat run scripts/check-usdc-balance.js --network hardhat
Expected check: compare the printed balance against a block explorer at the same fork block. If they match, the fork is serving live on-chain state. If the request fails, the upstream provider may require an archival node for that contract's storage slot or may be rate-limiting the local node.
Limits and common mistakes
- Ephemeral state. Restarting npx hardhat node wipes local changes. Use Hardhat snapshots in tests if you need to reset between cases.
- Upstream dependency. The fork still calls the provider for data not cached locally. Rate limits, API-key authentication, and provider outages will break tests.
- Archival requirements. Pinning to an old blockNumber fails unless the provider serves archival historical state. A standard full-node endpoint may return an error for blocks outside its recent window.
- No real impact. Impersonating a whale address lets you move its tokens locally, but nothing changes on Ethereum mainnet.
- Version drift. Forking configuration keys and supported RPC methods differ between Hardhat major versions. Confirm syntax against your installed version.
Practical verification
- Start npx hardhat node with forking enabled.
- Call eth_blockNumber on http://127.0.0.1:8545 and confirm it matches the configured fork block.
- Query a known mainnet contract through the fork and compare the result to a block explorer at that block.
If the values match, the fork is configured correctly. If they do not, review the provider URL, archival support, and Hardhat version documentation before relying on the fork for debugging.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.