Choosing Between Local Hardhat Network and Mainnet Forking for Smart Contract Tests
A decision guide for Ethereum testing: when to use Hardhat's fast in-memory network versus a forked mainnet, with config, a validation test, and the operational trade-offs.
08 Nov 2025, 10:31 UTC

The Testing Dilemma: Speed vs. Real-World State
When writing tests for Ethereum smart contracts, you face a trade-off between execution speed and environment accuracy. Testing against a blank, in-memory network is fast but ignores the reality of existing protocols. Testing against a mainnet fork provides the current state of the blockchain but introduces external dependencies and latency.
The goal is to maintain a fast feedback loop for unit tests while ensuring integration tests account for existing liquidity pools, oracles, and governance contracts without deploying those massive systems from scratch.
Comparison of Hardhat Network Options
| Feature | Pure Hardhat Network | Mainnet Forking | External Local Node (e.g. Ganache) |
|---|---|---|---|
| State Source | Empty/Fresh | Live Mainnet Snapshot | Local Database |
| Execution Speed | Near-instant | Moderate (RPC dependent) | Slow (Disk I/O) |
| External Contracts | Must be deployed manually | Available at live addresses | Must be deployed manually |
| Determinism | High | Medium (depends on block height) | High |
| Setup Overhead | Zero | Requires RPC API Key | Requires separate process |
Decision Framework: When to Use Which
Use the Pure Hardhat Network when:
- You are writing unit tests for internal logic (e.g., math functions, access control).
- You need tests to run in under 200ms per case.
- You are developing a new protocol that does not interact with existing DeFi ecosystems.
Use Mainnet Forking when:
- Your contract interacts with established protocols like Uniswap, Aave, or Chainlink.
- You need to test against specific, existing account balances or contract states.
- You want to use
hardhat_impersonateAccountto simulate transactions from a specific whale or governance multisig.
Avoid External Local Nodes when:
- You rely on
console.logwithin Solidity, as Hardhat's native network provides the best integration for these logs. - You want a seamless
npx hardhat testexperience without managing a separate background process.
Implementing a Mainnet Fork
To implement forking, modify your hardhat.config.js. This configuration tells Hardhat to fetch missing state from a remote Ethereum node (via an RPC provider like Alchemy or Infura) while keeping the actual execution in-memory for speed. Run all commands from your project root; no special permissions are needed beyond a valid RPC API key.
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.24",
networks: {
hardhat: {
forking: {
url: "https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY",
blockNumber: 19000000 // Optional: pin to a specific block for determinism
}
}
}
};Practical Validation: Testing an External Interaction
To verify that your fork is functioning, create a test that attempts to call a contract that only exists on mainnet. For example, reading a value from the Uniswap V2 Router at its known mainnet address.
// test/fork-test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Mainnet Fork Validation", function () {
it("Should read state from a live mainnet contract", async function () {
// Uniswap V2 Router address on Mainnet
const routerAddress = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
const router = await ethers.getContractAt("IUniswapV2Router02", routerAddress);
// This call fails on a pure Hardhat network: no code exists at this address
const factoryAddress = await router.factory();
expect(factoryAddress).to.be.properAddress;
});
});Note: this test requires an interface or ABI for IUniswapV2Router02 available in your project (for example via an imported artifact). Replace the address and interface with any mainnet contract relevant to your own protocol.
Operational Risks and Limitations
- RPC Quota: Every time your test requests state not already cached, it hits your RPC provider. Large test suites can quickly exhaust free-tier API credits and cause flaky failures from rate limiting.
- State Drift: If you do not specify a
blockNumber, the fork starts at the latest block. Tests can fail unpredictably if mainnet state changes between runs, so pin a block for CI. - Volatility: State changes made during a fork exist only for the duration of that Hardhat session. Long test runs can accumulate mutations; use Hardhat's snapshot/revert helpers to keep tests isolated.
Verification and Rollback
Verification: Run npx hardhat test from your project root. If the router.factory() call succeeds, the fork is active. If you receive a call exception or "contract not deployed" style error, the network is running in pure mode. You can also toggle the forking block off and confirm the same test fails, which proves the test genuinely depends on forked state. Hardhat's startup output indicates the active network mode.
Rollback: To return to a pure local environment, remove the forking object from hardhat.config.js or comment out the url line. No state is permanently altered on the actual Ethereum mainnet, since all fork mutations happen in local memory.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.