Hardhat Forking vs Snapshot: A Decision Guide for Mainnet-State Contract Testing
A decision guide comparing Hardhat forking, snapshot and Anvil for mainnet-state contract testing, with configuration, trade-offs, and verification steps.
03 Jul 2026, 18:58 UTC

Decision: Hardhat Forking for Reproducible Mainnet-State Testing
Hardhat's forking configuration lets a local node operate on top of a remote Ethereum RPC, preserving block numbers, accounts, and contract state at a given block height. This is useful when you need deterministic contract tests that reflect the exact state of mainnet at a specific moment, without the overhead of a full snapshot replay or the limitations of a fresh local node.
Constraints to weigh
- RPC provider rate limits can cause intermittent failures when forking deep block histories.
- The forked environment does not include pending transactions or mempool, so tests relying on pending tx behavior will not reflect mainnet conditions.
- State consistency depends on the RPC provider's freshness; stale headers may cause assertions against outdated balances or contract slots.
- Hardhat 2.x provides a dedicated forking section in hardhat.config.ts; earlier versions require network.forking under the networks block.
Comparison of Hardhat forking alternatives
| Option | State Source | Startup Speed | Mempool Access | Rate-Limit Risk |
|---|---|---|---|---|
| Hardhat forking (blockNumber) | Remote RPC at specified block | Near-instant | No | Medium (provider-dependent) |
| Hardhat snapshot | Saved state from prior run | Fast after first run | No | Low (local file I/O) |
| Anvil (Foundry) mainnet fork | Remote RPC at specified block | Near-instant | Yes (via txpool) | Medium (provider-dependent) |
Trade-offs explained
Hardhat forking gives you the closest approximation to live mainnet state with a single command. Because the local node syncs only the header chain and on-chain storage from the chosen block onward, tests run in seconds. The main constraint is the RPC provider: if the provider throttles requests or returns cached headers, your test suite may fail or produce inconsistent results. Additionally, because the mempool is empty, you cannot test transaction pending behavior or gas-price snipe scenarios without supplementing with Anvil's txpool or a separate mock.
When comparing against Hardhat snapshot testing, forking wins on state freshness but loses on offline reproducibility—snapshots are fully self-contained once created, whereas forking always requires a live RPC connection. Against Anvil's mainnet fork, Hardhat forking offers tighter TypeChain integration out of the box, but Anvil provides richer mempool inspection if that matters for your test coverage.
Concrete implementation in Hardhat 2.x
Add the following to hardhat.config.ts:
import { HardhatUserConfig, task } from \"hardhat/config\";
import \"@nomicfoundation/hardhat-toolbox\";
const config: HardhatUserConfig = {
solidity: \"0.8.24\",
forking: {
// Fork mainnet at block 17_000_000 (approximately early 2025)
url: \"https://mainnet.infura.io/v3/YOUR_PROJECT_ID\",
blockNumber: 17000000,
},
};
export default config;
To write a test that exercises the forked state:
import { expect } from \"chai\";
import { ethers } from \"hardhat\";
describe(\"Hardhat fork validation\", function () {
it(\"should report the forked block number\", async function () {
const blockNumber = await ethers.provider.getBlockNumber();
// Block 17_000_000 was the fork point; assert it matches the expected epoch
expect(blockNumber).to.equal(17000000);
});
it(\"should allow a type-safe contract call via TypeChain\", async function () {
const Token = await ethers.getContractFactory(\"MyToken\");
const token = Token.attach(\"0xYourTokenAddressOnMainnet\");
const name = await token.name();
expect(name).to.be.a(\"string\").that.is.not.empty;
});
});
Verification checklist
- Run
hardhat node --fork https://mainnet.infura.io/v3/YOUR_PROJECT_ID --block-number 17000000and confirm the console outputs a block number at or near 17000000. - Execute
npx hardhat testand confirm the two tests above pass without compilation errors (TypeChain must be installed and configured for your contracts). - Compare the
eth_balanceof a known mainnet address (via a block explorer) with the value returned byeth_getBalancein the forked console; they should match for that block height. - If the test fails due to \"rate limit exceeded\", switch to an alternative RPC (e.g., Alchemy, public endpoints) or reduce the frequency of
eth_callrequests within your test suite.
Limitations and practical validation
RPC staleness: Run eth_blockNumber repeatedly in a loop; if the value stops advancing, the provider may be serving cached headers. Rotate providers or use a dedicated endpoint.
No mempool: Write a separate test using Anvil's txpool suite if pending-tx behavior is required.
Block-number drift: After each test, re-assert eth_blockNumber to catch silent state resets.
By following the configuration and verification steps above, teams can maintain deterministic contract tests that reflect the exact mainnet state at a chosen block, while staying aware of provider-dependent risks and mempool gaps.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.