Skip to content

Simulated Networks by EDR

This page provides a reference for Hardhat’s simulated networks, powered by the Ethereum Development Runtime (EDR). Here you’ll find detailed information about all available features and behaviors of EDR-based local blockchains.

If you’re looking for a conceptual overview, see the explanation instead.

Simulated networks include features specifically designed for testing and debugging smart contracts. Unlike production blockchains, EDR is aware of your Solidity code and understands what’s running.

EDR also aims to precisely simulate the blockchains you’ll deploy your contracts to, both when running TypeScript and Solidity tests.

EDR has native support for simulating different types of blockchains. You can specify the chain type in your Network Config:

hardhat.config.ts
// ... imports ...
export default defineConfig({
// ... other config ...
networks: {
// ... other networks ...
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1", // Ethereum mainnet and its testnets
},
hardhatOptimism: {
type: "edr-simulated",
chainType: "op", // OP Stack chains, like OP Mainnet or its testnets
},
other: {
type: "edr-simulated",
chainType: "generic", // Default value, which is a permissive approximation to mainnet
},
},
});

To learn more about the op Chain Type, including predeploy contracts, supported chains, and forking, see the OP Stack support reference.

When a transaction or call fails, EDR provides Solidity stack traces. These traces include both the TypeScript test code where the transaction or call originated and the full Solidity call stack, making it easy to understand exactly where and why your code failed.

You can use console.log() in your Solidity code to print debugging information. Simply import hardhat/console.sol and call it like you would in JavaScript:

import "hardhat/console.sol";
contract MyContract {
function myFunction(uint256 value) public {
console.log("Processing value: %d", value);
}
}

The logs appear in your test output, helping you understand what’s happening inside your contracts. Learn more in the console.log reference.

EDR detects common error situations and reports clear error messages. For example, it will tell you if you’re:

  • Calling a non-payable function with ETH
  • Sending ETH to a contract without a payable fallback or receive function
  • Calling a non-existent function when there’s no fallback function
  • Calling a function with incorrect parameters
  • Calling an external function that doesn’t return the right amount of data
  • Calling an external function on a non-contract account
  • Failing to execute an external call because of its parameters (for example, trying to send too much ETH)
  • Calling a library without DELEGATECALL
  • Incorrectly calling a precompiled contract
  • Trying to deploy a contract that exceeds the bytecode size limit imposed by EIP-170

Hardhat uses EDR to compute code coverage for your tests by instrumenting your contracts and tracking which lines are executed. Simply run your tests with the --coverage flag. A Markdown report is printed to the terminal, while LCOV and HTML reports are generated in the coverage folder.

Terminal window
npx hardhat test --coverage

Learn more in the code coverage guide.

You can collect statistics about gas consumption during your test runs:

Terminal window
npx hardhat test --gas-stats

This shows you the minimum, maximum, average, and median gas costs for each function in your contracts. Learn more in the gas statistics guide.

EDR provides additional JSON-RPC methods that are useful for development and testing. These methods allow you to do things like manipulate the blockchain state or control mining behavior. You can read their reference documentation in this section.

We don’t recommend using these methods directly. Instead, use Hardhat’s network helpers, which wrap them in a more user-friendly way.

The initial state of a simulated network depends on whether you’re using forking mode and your Network Config.

By default, simulated networks start with an empty blockchain that includes:

  • Standard precompiled contracts (like ecrecover, sha256, etc.)
  • Any predeployed contracts specified by your chain type (for example, Optimism’s predeploys)
  • The accounts configured in your Network Config, each with the specified balance and no code

In forking mode, the simulated network starts as a copy of a remote blockchain at a specific block. The state is lazy-loaded from the remote network as needed, which means you don’t have to wait for the entire blockchain to download.

When forking, there are some differences from the remote network:

  • Accounts in your Network Config have their balance and code overwritten (removing EIP-7702 delegations if present)
  • The chainId is taken from your Network Config, not the remote chain, for security reasons (though this can be configured)

You can configure forking in your Network Config like this:

hardhat.config.ts
// ... imports ...
export default defineConfig({
// ... other config ...
networks: {
// ... other networks ...
mainnetFork: {
type: "edr-simulated",
forking: {
url: "https://mainnet.infura.io/v3/YOUR_API_KEY",
blockNumber: 14390000, // optional
},
},
},
});

EDR supports different mining modes that control when and how new blocks are created.

By default, automine is enabled. In this mode, a new block is mined immediately whenever a transaction is received. This makes tests fast because you don’t have to wait for blocks to be mined.

You can enable interval mining to mine new blocks periodically, regardless of whether there are pending transactions. This is useful for testing time-dependent contract behavior.

You can configure interval mining to use a fixed interval or a random interval between two values:

hardhat.config.ts
// ... imports ...
export default defineConfig({
// ... other config ...
networks: {
// ... other networks ...
intervalMined: {
type: "edr-simulated",
mining: {
auto: false, // disable automining
interval: 5000, // mine a new block every 5 seconds
},
},
},
});

Or with a random interval:

hardhat.config.ts
// ... imports ...
export default defineConfig({
// ... other config ...
networks: {
// ... other networks ...
intervalMined: {
type: "edr-simulated",
mining: {
auto: false, // disable automining
interval: [3000, 6000], // mine blocks at random intervals between 3-6 seconds
},
},
},
});

If you disable both automine and interval mining, transactions will accumulate in the mempool without being mined. You can manually mine blocks using the evm_mine RPC method:

scripts/manual-mining.ts
import { network } from "hardhat";
const { networkHelpers } = await network.create();
await networkHelpers.mine();

This is useful when you need precise control over when blocks are mined.

You can use automine and interval mining together. In this case, blocks are mined both when transactions arrive and at regular intervals.

You can also decide not to use either mode and rely solely on manual mining.

When automine is disabled, transactions are added to the mempool before being included in a block. By default, EDR’s mempool follows the same rules as Geth:

  • Transactions with a higher gas price are included first
  • If two transactions offer the same total fees, the one received first is included first
  • Invalid transactions (for example, with an incorrect nonce) are dropped

You can configure the mempool to use FIFO (first-in, first-out) instead of priority ordering. In FIFO mode, transactions are included in blocks in the exact order they were received:

hardhat.config.ts
// ... imports ...
export default defineConfig({
// ... other config ...
networks: {
// ... other networks ...
fifoNetwork: {
type: "edr-simulated",
mining: {
auto: false,
interval: 2000,
mempool: {
order: "fifo",
},
},
},
},
});

This is useful for recreating blocks from other networks where the transaction order is known.

To answer an eth_estimateGas request, EDR runs a binary search over the transaction’s gas limit, looking for the lowest value that makes the transaction succeed. The gasEstimationMode network config option controls what counts as a successful execution during this search:

  • "noInternalOutOfGas": The transaction must succeed and no call internal to it may run out of gas. This avoids estimates that make the top-level call succeed while an internal call silently fails with an out-of-gas error, which can happen when a contract ignores a failing call (for example, with try/catch or an unchecked low-level call). Default value.
  • "topLevelSuccess": The transaction only needs to succeed at the top level, even if an internal call runs out of gas along the way.

In the "noInternalOutOfGas" mode, if an internal call runs out of gas even when the transaction is executed with the maximum amount of gas available, no gas limit can prevent the failure, so the estimation fails with a descriptive error instead of returning a misleading value. This usually indicates a call made with a fixed gas amount that is too low. If the behavior is intentional, you can set an explicit gas limit on the transaction, or switch the network to "topLevelSuccess".

Transactions that rely on Hardhat’s automatic gas estimation (the gas: "auto" setting, which is the default) are not rejected in this situation: Hardhat falls back to the network’s default transaction gas limit instead.

To learn more about configuring simulated networks, read the configuration reference.