Forking Ethereum mainnet means running a local copy of the live chain on your own machine, seeded with real mainnet data. You get the actual deployed contracts, token balances, and protocol state, but every transaction you send stays local and costs no real gas.
Both Hardhat and Foundry can do this, and the setup is short in each. This guide covers how to create a Hardhat fork and a Foundry fork of mainnet, how to pin a block for repeatable tests, where the two tools differ, and the one detail that quietly breaks forks for most people: the node behind them.
What does it mean to fork Ethereum mainnet?

A mainnet fork is a local blockchain that starts from a real block of Ethereum and pulls state from a live node as your code touches it. When a test reads a contract’s storage or a wallet’s balance, the fork fetches that data from mainnet through an RPC URL you provide, then caches it locally.
From that point on, the fork behaves like its own private chain. It usually runs under chain ID 31337, mines blocks instantly, and throws everything away when you stop it. You can send transactions, deploy contracts, and move funds around without touching the real network or spending ETH.
A Hardhat fork works this way inside Hardhat’s test runner, and Foundry’s anvil does the same as a standalone node. The source of truth in both cases is the endpoint you point them at.
One thing worth knowing upfront: Hardhat changed how forking is configured in version 3, so many older guides show settings that no longer apply. Hardhat 2 is scheduled to reach end of life no later than June 1, 2027 — or the Hegota upgrade, whichever lands first — after which it stops getting releases, including security fixes. The examples below use the current Hardhat 3 format.
Why fork mainnet instead of using a testnet?
Because a testnet doesn’t have the thing you usually want to test against: real, live protocol state. Sepolia won’t have mainnet’s Uniswap pools, Aave markets, or the exact token balances and contract versions your integration depends on. A fork does.
That makes forking the practical choice for a handful of jobs:
- Testing integrations against live contracts. Call a real DEX router or lending pool with current liquidity, without deploying your own mock version.
- Reproducing and debugging mainnet behavior. Pin the block right before a failed transaction or an exploit and replay it locally to see what happened.
- Dry-running a deploy or an upgrade. Execute the deploy script against a copy of production and check the result before it costs anything.
- Simulating large or risky actions. Model a big swap, a liquidation, or a governance vote and inspect the outcome.
Protocol engineers, auditors, and teams integrating with DeFi reach for this daily, because it’s the closest you can get to production without being in production.
The trade-off is that a fork depends on an external node and is slower than a pure in-memory chain, since state has to travel over the network the first time it’s read. Pinning a block, shown below, fixes most of the speed and consistency problems.
How do you fork Ethereum mainnet with Hardhat?
Define a network with type: "edr-simulated" and a forking block in your config, then run your tests against it. That’s the whole idea; the rest is detail.
Set up the fork in your config
Install Hardhat and scaffold a project (npx hardhat --init), then edit hardhat.config.ts:
ts
import type { HardhatUserConfig } from "hardhat/config";
import { configVariable } from "hardhat/config";
const config: HardhatUserConfig = {
solidity: "0.8.28",
networks: {
mainnetFork: {
type: "edr-simulated",
forking: {
url: configVariable("MAINNET_RPC_URL"),
blockNumber: 23819000,
},
},
},
};
export default config;
Run the forked tests with:
bash
npx hardhat test nodejs --network mainnetFork
Store the endpoint as a configuration variable instead of hardcoding it, as the Hardhat docs recommend — a URL with an API key in it should not sit in a committed config file.
Fork inside Solidity tests
If you write tests in Solidity rather than TypeScript, put the fork settings under test.solidity:
ts
test: {
solidity: {
forking: {
url: configVariable("MAINNET_RPC_URL"),
rpcEndpoints: {
mainnet: configVariable("MAINNET_RPC_URL"),
},
},
},
},
Run those with npx hardhat test solidity. The rpcEndpoints map lets you fork selectively from inside a single test using a cheatcode:
solidity
vm.createSelectFork("mainnet");
// or pin a block:
vm.createSelectFork("mainnet", 23819000);
Pin a block for repeatable results
Leave the block number out and Hardhat forks from a recent block, which shifts between runs and makes tests non-deterministic. Setting blockNumber locks the fork to one point in history, so results stay the same and cached state can be reused. In the network config it’s a plain number; in the Solidity test config it’s a bigint, written 23819000n.
How do you fork mainnet with Foundry?
Foundry gives you two ways to fork: spin up anvil as a local forked node, or fork inside forge test. They share the same cheatcodes, so you can mix them freely.
Run a forked node with anvil
anvil is Foundry’s local node. Point it at a mainnet endpoint and it serves a JSON-RPC at http://127.0.0.1:8545 (chain ID 31337) that you can connect MetaMask, a script, or Remix to:
bash
anvil --fork-url "$MAINNET_RPC_URL"
# pin a block:
anvil --fork-url "$MAINNET_RPC_URL" --fork-block-number 18000000
If you don’t have Foundry yet, install it with curl -L https://foundry.paradigm.xyz | bash and then run foundryup.
Fork inside forge tests
For Solidity tests, you can fork the whole run straight from the command line:
bash
forge test --fork-url "$MAINNET_RPC_URL"
forge test --fork-url "$MAINNET_RPC_URL" --fork-block-number 18000000
Or create the fork in code with cheatcodes, which reads cleaner when different tests need different chains. Define endpoint aliases in foundry.toml:
toml
[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
Then select the fork in your test:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
contract ForkTest is Test {
function setUp() public {
vm.createSelectFork("mainnet"); // latest block
// vm.createSelectFork("mainnet", 18_000_000); // pinned
}
function test_readsMainnetState() public {
// call real deployed contracts here
}
}
vm.createSelectFork creates a fork and switches to it in one step. When you need more than one chain in the same test, vm.createFork returns a fork ID you store and vm.selectFork switches between them:
solidity
uint256 mainnetFork = vm.createFork("mainnet");
uint256 sepoliaFork = vm.createFork("sepolia");
vm.selectFork(mainnetFork);
On a rate-limited endpoint, adding --fork-retry-backoff <ms> to forge test spaces out retries so a burst of requests doesn’t fail the run.
Hardhat vs Foundry: which fork setup should you use?
Both land in the same place — a local chain backed by mainnet state — so the choice usually comes down to the stack you already work in. Hardhat fits a JavaScript or TypeScript workflow and its plugin ecosystem. Foundry is Solidity-native and fast, which matters for large test suites and fuzzing.
| Hardhat 3 | Foundry | |
|---|---|---|
| Test language | TypeScript or Solidity | Solidity |
| Standalone forked node | local dev node | anvil --fork-url |
| Fork inside tests | network config or vm.createSelectFork | --fork-url or vm.createSelectFork |
| Pin a block | blockNumber in config / cheatcode arg | --fork-block-number / cheatcode arg |
| Multi-chain forks | rpcEndpoints + cheatcodes | [rpc_endpoints] + cheatcodes |
| Strong point | JS/TS tooling, scripting | Speed, Solidity tests, fuzzing |
Plenty of teams use both: Foundry for fast unit and fork tests, Hardhat for deploy scripts and tasks that lean on the JavaScript ecosystem. You don’t have to pick one forever.
Why does the node behind your fork matter so much?
Because a fork is only as good as the node it reads from, and the quietest failures come from there, not from your code. Two things decide it: whether the node holds the historical state you ask for, and whether it can handle the request volume.
Pinning an old block needs archive data

A fork fetches state at the block you pin. Ask for a recent block and almost any node can answer. Ask for an older one and you hit a wall, because a default Geth node keeps only the most recent 128 block states — roughly 25.6 minutes of history at 12-second blocks — and prunes the rest. Request state older than that from a pruned node and the call fails with a missing-state error.
An archive node avoids this. It retains all historical state back to genesis, so it can serve any block you pin. If your tests pin a block from last week or last year, which is common when reproducing a specific event, the node you fork from has to be an archive one.
Request volume adds up fast
Forking isn’t one request. Every new piece of state your tests touch is a separate call, so a large suite can fire thousands of reads during a single run. Free public endpoints are capped — NOWNodes’ public endpoints, for example, allow 5 requests per second — which is fine for a quick experiment but not for a CI run.
This is where a dedicated endpoint earns its place. A NOWNodes Ethereum endpoint gives you a fork URL with archive access on supported networks, so older-block pinning works, and a paid plan lifts the request ceiling well above the public limit. Drop the URL into the forking.url field in Hardhat or the --fork-url flag in Foundry and the fork reads from it.
Conclusion
Forking Ethereum mainnet is the fastest way to test real contract interactions without real risk. In Hardhat 3, you define an edr-simulated network with a forking block and run your tests against it. In Foundry, you point anvil or forge test at a mainnet endpoint and use vm.createSelectFork to fork in code.
Pin a block when you need reproducible results, and match the node to the job: a standard endpoint for recent blocks, an archive one for anything older, and enough request headroom for a full suite. Get those right and the fork does exactly what you want — behaves like mainnet, costs nothing, and forgets everything when you’re done.
FAQ
Do I need an archive node to fork mainnet?
Only if you pin an older block. Forking at a recent block works with a standard endpoint, but a default node keeps just the last 128 block states, so pinning anything older requires archive access.
Does forking mainnet cost real ETH or gas?
No. A fork is a local chain. Transactions run against copied state and never reach the real network, so you spend no real ETH and nothing you do shows up on mainnet.
Can I fork at a specific block number?
Yes. In Hardhat, set blockNumber in the forking config or pass it to vm.createSelectFork. In Foundry, use --fork-block-number on the command line or pass the block as the second argument to the fork cheatcode.
Why does my fork fail with a “missing trie node” or state error?
Usually because the node can’t serve the historical state you requested. A pruned node only holds recent state, so switch to an archive endpoint if you’re forking an older block.
Can I use the same endpoint for both Hardhat and Foundry?
Yes. Both just need an Ethereum JSON-RPC URL. Store it once as an environment or configuration variable and reference it from each tool’s fork setting.



