Skip to content

Simulated Networks by EDR

Hardhat comes with Nomic Foundation’s Ethereum Development Runtime, or EDR. It simulates Ethereum and Layer 2 blockchains, and runs Solidity tests.

This document explains network simulations with EDR, how they work, and what makes them a powerful tool for developing and testing smart contracts.

Simulated networks are in-process blockchains that run entirely on your local machine.

Unlike connecting to a remote node through JSON-RPC, simulated networks give you complete control over the blockchain state, block production, and transaction execution. They’re faster and free to use.

The only difference between a blockchain simulated by EDR and a production one is that EDR requires no consensus mechanism or peer-to-peer network, as it’s run by a single process each time.

When you call await network.connect() with a Network Config of type edr-simulated, Hardhat creates a new, independent blockchain simulation. Each simulation is isolated, so you can create multiple simulations simultaneously without them interfering with each other.

You can use simulated networks in two ways:

When you run your tests or scripts, Hardhat automatically creates an in-process simulated network. For example:

import { network } from "hardhat";
const connection = await network.connect();

This creates a new blockchain simulation based on the Network Config specified in your configuration file or via the --network flag.

You can also run a simulated network as a standalone process that exposes a JSON-RPC interface over HTTP:

Terminal window
npx hardhat node

This starts a local node at http://127.0.0.1:8545 that you can connect to with wallets, applications, Hardhat, or other tools.

To learn more about network management, read this explanation.

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 Mainnet and its testnets
},
other: {
type: "edr-simulated",
chainType: "generic", // Default value, which is a permissive approximation to mainnet
},
},
});

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:

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.

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.connect();
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) ordering instead. 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 learn more about configuring simulated networks, read the configuration reference.