Hardhat 3 Documentation

Hardhat 3 is a modern development environment for Ethereum: compilation, testing, forking, deployment and verification. This page is a compact but dense guide for a TypeScript-based workflow.

Requires: Node.js ≥ 22.10.0 · Hardhat 3 · TypeScript + @nomicfoundation/hardhat-toolbox-ethers.

What is Hardhat

Hardhat is a Node.js-based tool that manages the full lifecycle of smart contract development: compilation, testing, local networks, mainnet forking, deployment, and integration with a rich plugin ecosystem.

  • npx hardhat <task> — unified CLI for running tasks.
  • Built-in local network (simulated networks / type: "edr-simulated").
  • Plugins: toolbox-ethers, gas reporter, coverage, verify, and more.
  • Runtime Environment (HRE) — access to ethers, networks, artifacts, config, etc.
Solidity ≠ workflow

Solidity is just the language. Hardhat fills in everything around it: configuration, builds, tests, networks, forking, deployment, and verification.

Why Hardhat 3

Hardhat 3 introduces a new configuration model and network types, while keeping the familiar developer experience.

  • New configuration via defineConfig and configuration variables.
  • TypeScript first: the default config file is hardhat.config.ts.
  • Two network models: edr-simulated (local) and http (RPC).
  • Toolbox plugins for viem/ethers, coverage, gas reporting and more.

Installation

We assume you already have Node.js ≥ 22.10 and a package manager like npm or pnpm installed.

1. Create a project folder

mkdir web3-hardhat-demo
cd web3-hardhat-demo
npm init -y          # or pnpm init, yarn init

2. Install Hardhat 3

npm install --save-dev hardhat

# or
pnpm add -D hardhat

3. Initialize the project

npx hardhat

In Hardhat 3, the CLI can scaffold a TypeScript project by default, with hardhat.config.ts and a basic project structure.

Always use local Hardhat

Use npx hardhat to run the locally installed version. Global Hardhat installations are almost never needed and often cause version conflicts between projects.

Creating a Hardhat project

Typical flow after running npx hardhat:

  1. Select “Create a TypeScript project”.
  2. Accept creating a .gitignore.
  3. Accept installing recommended dependencies.

The resulting structure looks like:

.
├─ contracts/
│  └─ Lock.sol
├─ scripts/
│  └─ deploy-counter.ts
├─ test/
│  └─ Lock.ts
├─ hardhat.config.ts
└─ package.json
Good fit for a modern stack

This template fits perfectly for a modern TypeScript stack: TS tests, ethers 6, forking, integration with OpenZeppelin and more.

Project structure

A typical Hardhat project contains:

  • contracts/ — Solidity contracts.
  • scripts/ — deployment, migrations, and utility scripts.
  • test/ — tests (TypeScript / Node test runner or Mocha).
  • hardhat.config.ts — configuration.
  • artifacts/ and cache/ — build output and cache (usually in .gitignore).

hardhat.config.ts

In Hardhat 3, configuration is defined via defineConfig, and secrets / RPC URLs are loaded using configuration variables.

Minimal config with ethers toolbox

// hardhat.config.ts
import { defineConfig, configVariable } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox-ethers";

export default defineConfig({
  solidity: {
    version: "0.8.28",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },

  paths: {
    sources: "./contracts",
    tests: "./test",
    cache: "./cache",
    artifacts: "./artifacts",
  },

  networks: {
    // Built-in simulated network that behaves like an L1 chain
    hardhatMainnet: {
      type: "edr-simulated",
      chainType: "l1",
    },

    // RPC network (example: Sepolia)
    sepolia: {
      type: "http",
      chainType: "l1",
      url: configVariable("SEPOLIA_RPC_URL"),
      accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
    },
  },

  test: {
    solidity: {
      timeout: 40000,
    },
  },
});
Never commit private keys

SEPOLIA_PRIVATE_KEY, SEPOLIA_RPC_URL and other sensitive values should be stored outside the code — via configuration variables and, optionally, hardhat-keystore.

Solidity compiler

The compiler configuration is defined in the solidity field of the config.

Single compiler

export default defineConfig({
  solidity: {
    version: "0.8.28",
  },
});

Multiple versions

export default defineConfig({
  solidity: {
    compilers: [
      { version: "0.8.28" },
      { version: "0.7.6" },
    ],
  },
});

Hardhat will pick the smallest compatible version according to the pragma solidity in the contract.

Hardhat Runtime Environment (HRE)

The Hardhat Runtime Environment gives you access to ethers, networks, artifacts, config and more. In Hardhat 3, a common pattern is to use network.connect().

// tasks/accounts.ts
import { task } from "hardhat/config";

task("accounts", "Prints accounts", async (_, hre) => {
  const { ethers } = await hre.network.connect();
  const signers = await ethers.getSigners();

  for (const s of signers) {
    console.log(s.address);
  }
});

Common HRE properties:

  • ethers — integrated ethers v6 instance.
  • network — network manager and network.connect().
  • artifacts — ABI/bytecode access.
  • config — the active Hardhat configuration.

Networks & forking

Hardhat 3 supports two main network types: simulated networks (edr-simulated) and RPC networks (http).

Local simulated networks

export default defineConfig({
  networks: {
    hardhatMainnet: {
      type: "edr-simulated",
      chainType: "l1",
      hardfork: "prague",
    },
  },
});

Forking mainnet / L2

export default defineConfig({
  networks: {
    hardhatMainnet: {
      type: "edr-simulated",
      chainType: "l1",
      forking: {
        url: configVariable("MAINNET_RPC_URL"),
        blockNumber: 19000000,
      },
    },
  },
});

In tests and scripts you can now work against a mainnet-like state locally: impersonate accounts, interact with real protocols, and debug integrations without touching real funds.

Tasks

A task is a named CLI command. Tasks are useful for repetitive operations: listing accounts, migrations, generating snapshots, maintenance scripts, and more.

// tasks/accounts.ts
import { task } from "hardhat/config";

task("accounts", "Prints the list of accounts")
  .setAction(async (_, hre) => {
    const { ethers } = await hre.network.connect();
    const signers = await ethers.getSigners();

    for (const s of signers) {
      console.log(s.address);
    }
  });

Register the task in your config:

// hardhat.config.ts
import "./tasks/accounts";

Run it:

npx hardhat accounts

Scripts

Scripts are plain TypeScript files that you run using npx hardhat run. They’re most often used for deployments and maintenance.

Example deployment script with ethers

// scripts/deploy-counter.ts
import { network } from "hardhat";

const { ethers, networkName } = await network.connect();

console.log(`Deploying Counter to ${networkName}...`);

const counter = await ethers.deployContract("Counter");

console.log("Waiting for the deployment tx to confirm...");
await counter.waitForDeployment();

console.log("Counter address:", await counter.getAddress());

console.log("Calling counter.incBy(5)...");
const tx = await counter.incBy(5n);
await tx.wait();

console.log("Deployment successful!");

Run on Sepolia:

npx hardhat run scripts/deploy-counter.ts --build-profile production --network sepolia

Deployment

In Hardhat 3 there are two main approaches: Ignition (declarative deployment) and classic scripts. Ignition is ideal for larger projects; scripts are simpler when you’re just starting out.

  1. Configure the network in hardhat.config.ts.
  2. Write a deployment script (like above) or an Ignition module.
  3. Provide RPC URLs and keys via configuration variables.
  4. Run npx hardhat run ... --network ....
Deployment logs

Log deployment information to the console and save deployed addresses into a JSON file, so you can reuse them in your frontend, scripts, or tests.

Verification

Contract verification is handled via the Etherscan plugin (part of the toolbox) or other explorer plugins.

Etherscan configuration

// hardhat.config.ts
import { defineConfig, configVariable } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox-ethers";

export default defineConfig({
  solidity: { version: "0.8.28" },
  networks: {
    sepolia: {
      type: "http",
      chainType: "l1",
      url: configVariable("SEPOLIA_RPC_URL"),
      accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
    },
  },
  etherscan: {
    apiKey: configVariable("ETHERSCAN_API_KEY"),
  },
});

Verification command

npx hardhat verify --network sepolia <deployed_address> arg1 arg2 ...

Testing (TypeScript)

For TypeScript tests with ethers v6, a common setup is @nomicfoundation/hardhat-toolbox-ethers, which provides utilities like expect(...).to.changeTokenBalances, fixtures, and more.

Simple TypeScript test

// test/MyToken.ts
import { expect } from "chai";
import { ethers, loadFixture } from "hardhat";

describe("MyToken", function () {
  async function deployTokenFixture() {
    const [owner, otherAccount] = await ethers.getSigners();

    const token = await ethers.deployContract("MyToken");
    await token.waitForDeployment();

    return { token, owner, otherAccount };
  }

  it("assigns initial supply to owner", async function () {
    const { token, owner } = await loadFixture(deployTokenFixture);

    const ownerBalance = await token.balanceOf(owner.address);
    expect(await token.totalSupply()).to.equal(ownerBalance);
  });

  it("transfers tokens", async function () {
    const { token, owner, otherAccount } = await loadFixture(deployTokenFixture);

    await expect(
      token.transfer(otherAccount.address, 100n)
    ).to.changeTokenBalances(token, [owner, otherAccount], [-100n, 100n]);
  });
});

Run tests with:

npx hardhat test
Fixtures = faster tests

loadFixture creates a snapshot of the network state and restores it before each test. This is much faster than redeploying contracts every time.

Plugins

Plugins are what turn Hardhat into a full-featured development stack. For a typical Web3 docs-style project, you usually want at least:

  • @nomicfoundation/hardhat-toolbox-ethers — tests, assertions, ethers integration.
  • hardhat-gas-reporter — gas usage per test.
  • solidity-coverage — test coverage reports.

Gas reporter example

npm install --save-dev hardhat-gas-reporter
// hardhat.config.ts
import "hardhat-gas-reporter";

export default defineConfig({
  solidity: { version: "0.8.28" },
  gasReporter: {
    enabled: true,
    currency: "USD",
  },
});

Troubleshooting

Common issues:

“No solc version compatible with pragma...”

Check that the pragma solidity in your contracts and the solidity field in the config are compatible. If needed, add multiple compilers.

“Unknown network <name>”

You passed --network name, but there is no such entry in networks in hardhat.config.ts.

“Insufficient funds for gas”

On testnets or mainnet your deployer address must have enough ETH to pay for gas. Use faucets or bridges to fund it.

Resources

  • Official Hardhat 3 documentation (tutorials and guides).
  • Guides on TypeScript testing, deployment scripts, and configuration variables.
  • OpenZeppelin Contracts for tokens, access control, and standards.
  • Combined Foundry + Hardhat workflows for advanced testing setups.