Guide
Diagnosing Invalid Opcode or Out‑of‑Gas Errors in Hardhat Network Forks
Learn why transactions revert when using a Hardhat fork, how to check the fork block versus contract deployment, and what fixes or escalation steps to take.
Published by Tasadduq Burney
30 May 2026, 08:30 UTC
3 min119K views0

Recognizable Condition
When running tests against a Hardhat Network fork, transactions that interact with a deployed contract revert with either invalid opcode or out of gas. The same call succeeds on the live mainnet.
Cause / Diagnostic Table
| Possible Cause | What to Look For |
|---|---|
| Fork block is older than the contract’s deployment block | Check forking.blockNumber in hardhat.config.js; compare with the transaction block where the contract was created. |
| Required storage slots were modified after the fork point | Identify storage reads the contract performs (e.g., via sload) and verify whether those slots changed after the fork block. |
Ordered Checks
- Confirm the fork block number
- Open
hardhat.config.jsand locate theforkingsection. - Note the value of
blockNumber(orblockHashif used).
- Open
- Find the contract’s deployment block
- Using a block explorer (e.g., Etherscan) or
hardhat network provider, retrieve the transaction that created the contract. - Record its block number (
deploymentBlock).
- Using a block explorer (e.g., Etherscan) or
- Compare
- If
forking.blockNumber < deploymentBlock, the fork is missing the contract’s bytecode and storage. - If equal or greater, proceed to check storage slots.
- If
- Inspect storage‑dependent logic
- Identify any
sloadoperations the contract performs (via source code orhardhat trace). - For each storage slot, query its value at the fork block:
hardhat.network.provider.send('hardhat_getStorageAt', [contractAddress, slotNumber, 'latest']) - Compare with the value at a later block (e.g.,
deploymentBlock + 100) to see if it changed after the fork.
- Identify any
Fixes Tied to Findings
- Update the fork block
- Set
forking.blockNumberto a block that is >=deploymentBlockand includes all relevant state‑changing transactions. - Example configuration:
module.exports = { solidity: '0.8.20', networks: { hardhat: { forking: { url: 'https://mainnet.infura.io/v3/', blockNumber: 19000000 // replace with a recent block } } } };
- Set
- Mock missing storage
- If moving the fork forward is not feasible (e.g., RPC limits), inject the required storage values:
await hardhat.network.provider.send('hardhat_setStorageAt', [ contractAddress, '0x0', // slot number as hex hardhat.ethers.utils.hexZeroPad('0x0000000000000000000000000000000000000000000000000000000000000001', 32) // example value ]);
- If moving the fork forward is not feasible (e.g., RPC limits), inject the required storage values:
- Re‑fork at a newer block
- Stop the current Hardhat node, update the config as shown above, and restart.
Escalation Criteria
If after adjusting the fork block and/or mocking storage the transactions still revert with invalid opcode or out of gas:
- Isolate the test to a pure Hardhat Network (no forking) using mock contracts or fixtures to confirm the issue is fork‑specific.
- Collect the following data and open an issue in the Hardhat repository:
- Hardhat version (
npx hardhat version) - Fork block number and RPC endpoint used
- Contract address and ABI (or source)
- Full error trace (
hardhat networklogs with--show-stack-traces)
- Hardhat version (
Verification
- Run a simple read‑only test that queries a known storage slot (e.g., totalSupply of an ERC‑20) before and after changing the fork block; the value should match the block explorer’s reading at that block.
- Confirm the node is on the expected block with:
hardhat.network.provider.send('hardhat_getBlockByNumber', [forkBlockNumber, false]) - If you mocked storage, verify the slot reads the mocked value:
hardhat.network.provider.send('hardhat_getStorageAt', [contractAddress, slot, 'latest'])
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.