Test Against Real Mainnet State With Hardhat Forking Without Spending Gas
Use Hardhat Network with mainnet forking to run integration tests against a pinned replica of real on-chain state, with impersonation and time controls for safe simulation.
25 Jan 2026, 13:52 UTC

Shipping a contract that integrates with existing mainnet protocols is risky when tests only use fresh mocks. A concrete problem is verifying an allowance and swap flow against a real token and router where balances, approvals and fee tiers already exist. The useful takeaway is to run Hardhat Network with mainnet forking, pin a block, and execute integration tests in-process against a local replica of real state.
Why a pinned fork helps integration confidence
Hardhat Network can start with forking enabled. The local EVM mirrors on-chain state at a chosen block, allowing read calls and simulated writes against real protocol storage without spending gas. Forking is expressed in hardhat.config via network.forking.url and optionally forking.blockNumber. Pinning the block makes the replica deterministic and reduces flakiness from live chain reorgs.
The forked node supports Hardhat console and EVM manipulation helpers such as evm_increaseTime and evm_mine. That enables tests of time-locked logic, vesting and auction deadlines on realistic state. Accounts can be impersonated with hardhat_impersonateAccount to simulate transactions from funded mainnet addresses, useful for integration tests that require specific token balances or approvals.
Pinning a deterministic snapshot
Configure a hardhat network that forks mainnet. Run this from the project root where hardhat.config.ts lives. It requires read access to an RPC endpoint that serves archival data for the chosen block.
// hardhat.config.ts
import { HardhatUserConfig } from 'hardhat/config';
const config: HardhatUserConfig = {
networks: {
hardhat: {
forking: {
url: process.env.MAINNET_RPC_URL || '',
blockNumber: 21000000
}
}
}
};
export default config;
Meaningful placeholders: MAINNET_RPC_URL must point to a provider with archival access. blockNumber pins the replica. Without a pinned block, the fork follows the RPC head and tests become non-deterministic.
Start the node with npx hardhat node. The node runs entirely in-process. Expected check: a read-only call to a well-known contract, such as a token balanceOf, returns a value consistent with the state at the pinned block. Verify by comparing the returned value with a separate read-only call made against the same RPC at the same block number.
Impersonation and time control on the fork
Impersonation does not expose private keys and only works on the local fork. Use it to simulate actions from funded mainnet addresses for integration scenarios that would otherwise require real funds.
// tests/integration/fork.test.ts
import { ethers, network } from 'hardhat';
describe('fork integration', () => {
it('can impersonate a funded account', async () => {
const target = '0x000000000000000000000000000000000000dead';
await network.provider.request({ method: 'hardhat_impersonateAccount', params: [target] });
const signer = await ethers.getSigner(target);
// use signer for calls against forked contracts
});
});
Time manipulation is done via evm_increaseTime and evm_mine. This is safe for testing deadlines on the fork because the change is local to the in-memory EVM.
Worked example: allowance flow against a forked token
Goal: confirm a contract can pull tokens after an approval from a real holder on the fork.
- Pin the fork to a block where the holder address has a known balance.
- Impersonate the holder and call approve on the forked token.
- Call the contract under test from a test signer and assert the transfer succeeds.
- Advance time if the contract has a timelock, then re-assert.
Risks to note: impersonation bypasses real signatures, so gas estimation and nonce handling can differ from production. The forked state is static at the pinned block; it does not reflect later mainnet changes unless the block number or RPC is updated, which can hide regressions.
Trade-offs and limits
Reliance on an external RPC introduces rate limits, latency and availability risk. Large test suites can exhaust quotas or become non-deterministic. Forked tests can be slow and memory intensive for deep state, and they assume the RPC returns complete archival data for the chosen block.
Because the snapshot is frozen, tests will not catch issues introduced by upgrades or state changes after the pinned block. Changing forking.blockNumber to a later block and re-running the suite is a practical way to observe state drift and confirm tests are sensitive to the snapshot choice.
Verify it works in your repo
- Start a Hardhat node with forking enabled and query a well-known contract balance, then compare with the expected state at the pinned block using a separate read-only call.
- Run a test that reverts on the real network due to insufficient allowance and confirm it passes after impersonating a funded account on the fork.
- Advance time with evm_increaseTime and mine blocks, then assert that time-dependent contract logic changes as expected on the forked instance.
Keep the fork block in version control, document the RPC requirements for CI, and treat forked integration tests as pre-deployment checks, not a substitute for mainnet monitoring.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.