# Migrate Foundry To Hardhat

> Migrate a Solidity project from Foundry to Hardhat 3. Maps `foundry.toml` to `hardhat.config.ts`, converts dependencies and imports, gets compilation and Solidity tests passing, and produces a migration report with feature-parity gaps and a cleanup checklist.

- Skill: `nomicfoundation/migrate-foundry-to-hardhat` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nomicfoundation/migrate-foundry-to-hardhat`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nomicfoundation/migrate-foundry-to-hardhat/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: nomicfoundation (https://skillmd.com/u/nomicfoundation)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nomicfoundation/migrate-foundry-to-hardhat

---


# Migrate Foundry project to Hardhat 3

**If the argument is `update`, skip to the [Update an existing migration](#update-an-existing-migration) section below.**

The goal is to get Hardhat compilation and Solidity tests passing, identify any blockers and missing features, and leave the user with a clear cleanup checklist for the original Foundry files. This involves config mapping, dependency conversion, import fixes, compilation, and test verification.

**Important:** Identifying blockers and missing Hardhat features relative to Forge is a primary goal of this migration. Do not overlook or guess at feature mappings. If a Forge feature has no clear Hardhat equivalent, do not silently skip it — leave a TODO comment in the config or code, link to a tracking issue if one exists, and flag it in the final migration report.

**Foundry file preservation:** Do NOT delete `foundry.toml`, `foundry.lock`, `remappings.txt`, `lib/`, or `.gas-snapshot` during the migration. The user needs them to cross-check results against Forge and roll back if needed. The final migration report contains a **Foundry cleanup checklist** that lists which files the user can delete once they've verified Hardhat works end-to-end — the user actions that checklist manually.

For `package.json` scripts: **replace** the convertible `forge`-based scripts (`test`, `build`, `coverage`, `snapshot`, `snapshot:check`) with their Hardhat equivalents under the same script name. **Leave Forge-only scripts intact** (e.g., `forge script`, `forge bind`, `forge verify-contract`) — they continue to work as long as Foundry is installed, and the report lists them as Foundry-only gaps the user must address before deleting `foundry.toml`.

Follow these steps in order. Do not skip ahead — each step depends on the previous one succeeding.

## Step 1: Analyze the Foundry project

Before making changes, read and understand the existing project **thoroughly**:

1. Read `foundry.toml` — note **ALL** configured profiles and settings. Do not overlook any section.
2. Read `remappings.txt` if it exists
3. Check for git submodules in `lib/` — run `ls lib/` and read `.gitmodules` if present. **Ensure submodules are initialized** — run `git submodule update --init --recursive`. Compilation will fail if submodule directories are empty.
4. Identify the contract source directory (usually `src/` or `contracts/`)
5. Identify the test directory (usually `test/` or `tests/`)
6. Identify any scripts in `script/`
7. Scan `.t.sol` files for **inline test config** — `forge-config:` directives in both line comments (`/// forge-config:`, `// forge-config:`) and block comments (`/** forge-config: ... */`). Since Hardhat 3.3.0, inline config is supported at the **function level** and `forge-config:` is backwards-compatible ([docs](https://hardhat.org/docs/guides/testing/inline-configuration)). Hardhat 3.5.0 / EDR `0.12.0-next.33` added function-level support for `isolate` and `evm_version` ([edr#1349](https://github.com/NomicFoundation/edr/issues/1349) closed). However, Hardhat only supports **function-level** inline config — **contract-level** inline config (directives placed on the contract definition rather than individual functions) is NOT supported. For contract-level configs, the workaround is to set the value globally in `hardhat.config.ts`. Flag any files using contract-level directives.
8. **Scan `package.json` scripts** — read every script entry and identify all Forge-dependent commands (not just `forge test` / `forge build`). Custom scripts using `forge bind`, `forge script`, `forge snapshot`, `forge verify-contract`, etc. represent additional Forge features the project actively uses. List them in the analysis file — they must appear in the feature parity table.
9. **Scan for zkSync-specific profiles and contracts:** Check `foundry.toml` for zkSync profiles (e.g., `[profile.zksync]`) and scan for zkSync-specific contract directories (`contracts/zkSync/`, `contracts/zk*/`, `src/zkSync/`, etc.). Assess what percentage of the codebase these contracts represent (file count relative to total `.sol` files). zkSync compilation settings and contracts may not be testable under Hardhat — flag them for the migration report.
10. **Determine the package manager:** Check which lockfile exists (`yarn.lock`, `package-lock.json`, `pnpm-lock.yaml`) and note the corresponding tool (`yarn`, `npm`, `pnpm`). If **no lockfile exists**, use `pnpm` as the default. If **multiple lockfiles exist**, pick the one that is more recently modified OR has more dependencies defined — whichever gives the clearest signal. Check modification timestamps with `ls -lt yarn.lock package-lock.json pnpm-lock.yaml 2>/dev/null` and line counts with `wc -l`. Document the choice and the reason in the analysis file. Do **not** introduce a second lockfile. **Lock in this choice** — use it for every install command throughout the session (including `patch-package` steps). Never mix package managers.

**IMPORTANT:** Migrating all configurations is KEY to the migration succeeding. Every section of `foundry.toml` must be accounted for — either mapped to a Hardhat equivalent, or explicitly documented as unsupported with a comment and issue link.

After analyzing, write a structured summary to a file named `hardhat-migration/<project-name>-foundry-migration-analysis.md` (where `<project-name>` is the directory name of the project being migrated) in the repository root. Create the `hardhat-migration/` directory if it doesn't exist. The file includes:

- Every `foundry.toml` section and setting found
- All profiles (`[profile.default]`, `[profile.ci]`, etc.) and their settings
- Remappings, submodules, source/test/script directories
- Any notable patterns (absolute imports, unusual configs)
- Inline test config usage (`forge-config:` comments) — list affected files and which inline settings are used, noting whether each directive is at the **function level** (supported in Hardhat 3.5.0+, including `isolate` / `evm_version` per [edr#1349](https://github.com/NomicFoundation/edr/issues/1349)) or **contract level** (NOT supported in Hardhat — must be set globally).
- All Forge-dependent `package.json` scripts identified in step 8 above
- zkSync-specific profiles and contracts identified in step 9 above (if any), with percentage of codebase affected
- Selected package manager (from step 10 above)

This file serves as a reference throughout the migration so you don't have to re-read `foundry.toml` repeatedly. Present the summary to the user before proceeding.

## Step 2: Add Hardhat to the project

**Package manager:** Use the package manager determined in Step 1.

**Fetch the latest Hardhat version:** Run `view hardhat version` using the package manager determined in Step 1 (e.g. `pnpm view hardhat version`), and note the version string returned. If the command fails, fall back to `3.9.0`.

Edit `package.json`:

- Add `"hardhat": "^<latest-version>"` to `devDependencies`, using the version fetched above.
- Add `"@nomicfoundation/hardhat-verify": "^3.0.11"` to `devDependencies` (if `[etherscan]` section exists)
- Add `"hardhat-ignore-warnings": "^0.3.0"` to `devDependencies` (if `ignored_warnings_from` is set in `foundry.toml`) — see "Compiler warning suppression" in Step 3 for the matching config.
- Ensure the top-level field `"type": "module"` is set. **Note:** This switches the project to ESM, which is required by Hardhat 3. If the project has existing CommonJS files (`.js` with `require()`), this may break them — flag it to the user if so.
- **Replace** the convertible `forge`-based scripts (`test`, `build`, `coverage`, `snapshot`, `snapshot:check`) with their Hardhat equivalents under the same script name — do NOT add a parallel `-hardhat`-suffixed entry. The original Forge commands remain recoverable from git history.
- **Leave Forge-only scripts intact** (e.g., `forge script`, `forge bind`, `forge verify-contract`, custom `forge` invocations). They continue to work as long as Foundry is installed; the migration report lists them as Foundry-only gaps the user must address before deleting `foundry.toml`.
- If the project has **no Forge-related scripts** (no `scripts` section, or only non-Forge scripts like `lint`, `prettier`), do **not** add any Hardhat scripts to `package.json`.

Common script replacements (replace in-place under the same key):

| Existing Foundry script | Replace with |
| --- | --- |
| `"test": "forge test"` | `"test": "hardhat test solidity"` |
| `"build": "forge build"` | `"build": "hardhat compile"` |
| `"coverage": "forge coverage ..."` | `"coverage": "hardhat test solidity --coverage"` |
| `"snapshot": "forge snapshot ..."` | `"snapshot": "hardhat test solidity --snapshot"` |
| `"snapshot:check": "forge snapshot --check ..."` | `"snapshot:check": "hardhat test solidity --snapshot-check"` |

**Note on coverage:** Hardhat 3 has built-in coverage support (no plugin needed). The `--coverage` flag produces LCOV (`coverage/lcov.info`) and HTML (`coverage/html/index.html`) reports. Reference: https://hardhat.org/docs/tutorial/coverage

Then install dependencies using the package manager determined above. If the project has a `postinstall` script that runs Forge commands (e.g., `forge install`), use `--ignore-scripts` to skip it — it's unnecessary for the Hardhat migration and may be slow or fail. For yarn: `yarn install --ignore-scripts`. For npm: `npm install --ignore-scripts`. For pnpm: `pnpm install --ignore-scripts`.

Finally, add Hardhat's output directories to `.gitignore` if not already present:

```
# Hardhat
artifacts/
cache/
```

## Step 3: Create hardhat.config.ts

Create `hardhat.config.ts` by mapping settings from `foundry.toml`. Use the reference tables below for known mappings.

### Config structure: Always use `defineConfig()`

**Always use `defineConfig()`** — it is the recommended pattern across all Hardhat 3 documentation. Do NOT use the `HardhatUserConfig` type annotation approach (a Hardhat 2-era pattern that still compiles but is not the documented way to configure Hardhat 3).

When using plugins (e.g. `hardhat-verify`), pass them in the `plugins` array:

```typescript
import { configVariable, defineConfig } from "hardhat/config";
import hardhatVerify from "@nomicfoundation/hardhat-verify";

export default defineConfig({
  plugins: [hardhatVerify],
  solidity: { ... },
  networks: { ... },
  verify: { ... },  // typed correctly via plugin
});
```

When no plugins are needed:

```typescript
import { defineConfig } from "hardhat/config";

export default defineConfig({
  solidity: { ... },
});
```

### Solidity compiler settings (`solidity`)

Map these from `[profile.default]` in foundry.toml:

| foundry.toml | hardhat.config.ts |
| --- | --- |
| `solc = "0.8.X"` or `solc_version = "0.8.X"` | `solidity.compilers[0].version: "0.8.X"` |
| `optimizer = true` | `solidity.compilers[0].settings.optimizer.enabled: true` |
| `optimizer_runs = 200` | `solidity.compilers[0].settings.optimizer.runs: 200` |
| `evm_version = "cancun"` | `solidity.compilers[0].settings.evmVersion: "cancun"` |
| `via_ir = true` | `solidity.compilers[0].settings.viaIR: true` |
| `bytecode_hash = "none"` | `solidity.compilers[0].settings.metadata.bytecodeHash: "none"` |
| `src = "src"` | `paths.sources: "./src"` |
| `test = "tests"` | `paths.tests: "./tests"` |

**IMPORTANT — `solidity` structure rules:**

- When using per-file `overrides`, you MUST use the `compilers` array format. The top-level `version` field is incompatible with `overrides`.
- When using `profiles`, you CANNOT have top-level `compilers` or `version` — all compiler config must live inside named profiles. A `default` profile is **required**.

```typescript
// WRONG — overrides + version are incompatible
solidity: { version: "0.8.28", overrides: { ... } }

// WRONG — top-level compilers + profiles are incompatible
solidity: { compilers: [...], profiles: { ... } }

// CORRECT — without profiles, use compilers array with overrides
solidity: { compilers: [{ version: "0.8.28", settings: { ... } }], overrides: { ... } }

// CORRECT — with profiles, put everything inside named profiles (default is required)
solidity: { profiles: { default: { compilers: [...] }, production: { compilers: [...], overrides: { ... } } } }
```

### Build profiles (`solidity.profiles`)

Foundry's `[profile.*]` sections, `additional_compiler_profiles`, and `compilation_restrictions` map to Hardhat's build profiles system.

**When to use profiles vs plain compilers:**

- Use `solidity.profiles` when `foundry.toml` has multiple `[profile.*]` sections with **different compiler settings** (e.g., `[profile.debug]` changes `via_ir` or `optimizer_runs`). A `default` profile is required.
- Use plain `solidity.compilers` (without profiles) when there is only `[profile.default]` and no other profiles change compiler settings. Profiles that only override test settings (like `[profile.pr.fuzz]`) do not count — they have no Hardhat build profile equivalent.

**Create ALL Foundry profiles that have compiler settings** — do not leave any unmigrated with a TODO comment if they can be expressed in Hardhat. Every `[profile.*]` section with compiler settings (optimizer, viaIR, optimizer_details, etc.) must become a named Hardhat build profile.

Reference: https://hardhat.org/docs/guides/writing-contracts/build-profiles

| Foundry concept | Hardhat 3 equivalent |
| --- | --- |
| `[profile.default]` base settings | `solidity.profiles.default` (**required** when using profiles) |
| `additional_compiler_profiles` + `compilation_restrictions` | `solidity.profiles.<name>.overrides` with exact file paths |
| `[profile.coverage]` compiler settings | `solidity.profiles.coverage` |

**Key limitations:**

- Build profiles only cover **compiler settings**, NOT test settings (fuzz runs, etc.). Foundry profiles like `[profile.pr.fuzz]` or `[profile.ci.fuzz]` that only change test settings have no direct equivalent — use env vars or CLI args. For settings that can't be expressed in Hardhat profiles (fuzz runs, gas snapshots, isolate, etc.), use the values from `[profile.default]` in the top-level `test.solidity` config and leave a comment explaining that the other Foundry profiles (pr, ci, coverage, gas) override these values but Hardhat profiles don't support test settings.
- If a Foundry profile has **both compiler and test settings** (e.g., `[profile.debug]` sets `via_ir = false` AND `fuzz.runs = 100`), create the Hardhat build profile with only the compiler settings and add a `// TODO:` comment for the test settings that cannot be included.
- Hardhat does NOT yet support glob patterns (`**`) in `overrides`. Each file must be listed individually. See: https://github.com/NomicFoundation/hardhat/issues/4686
- Leave TODO comments for glob overrides that can't be expressed.

Usage: `npx hardhat compile --build-profile production`

**DRY principle for profiles:** When multiple profiles share most compiler settings, extract the common settings into a local variable to avoid duplication:

```typescript
const baseCompilerSettings = {
  optimizer: { enabled: true, runs: 1000000 },
  evmVersion: "shanghai" as const,
  viaIR: true,
};

export default defineConfig({
  solidity: {
    profiles: {
      default: {
        compilers: [{ version: "0.8.23", settings: baseCompilerSettings }],
      },
      lite: {
        compilers: [
          {
            version: "0.8.23",
            settings: {
              ...baseCompilerSettings,
              optimizer: {
                ...baseCompilerSettings.optimizer,
                details: { yulDetails: { optimizerSteps: "" } },
              },
            },
          },
        ],
      },
    },
  },
});
```

### Test settings (`test.solidity`)

Map these from `[profile.default]` or `[fuzz]`/`[invariant]` sections:

| foundry.toml | hardhat.config.ts |
| --- | --- |
| `fuzz.runs = 256` | `test.solidity.fuzz.runs: 256` |
| `fuzz.seed = "0x640"` | `test.solidity.fuzz.seed: "0x640"` |
| `fuzz.max_test_rejects = 65536` | `test.solidity.fuzz.maxTestRejects: 65536` |
| `fuzz.dictionary_weight = 40` | `test.solidity.fuzz.dictionaryWeight: 40` |
| `invariant.runs = 256` | `test.solidity.invariant.runs: 256` |
| `invariant.depth = 500` | `test.solidity.invariant.depth: 500` |
| `invariant.fail_on_revert = false` | `test.solidity.invariant.failOnRevert: false` |
| `ffi = true` | `test.solidity.ffi: true` |
| `block_gas_limit` | `test.solidity.blockGasLimit` |
| `gas_limit = N` | `test.solidity.gasLimit: Nn` (**must be bigint**, use `n` suffix) |
| `fs_permissions = [{ access = "read", path = "./file" }]` | `test.solidity.fsPermissions: { readFile: ["./file"] }` |
| `fs_permissions = [{ access = "write", path = "./file" }]` | `test.solidity.fsPermissions: { writeFile: ["./file"] }` |
| `fs_permissions = [{ access = "read-write", path = "./file" }]` | `test.solidity.fsPermissions: { readWriteFile: ["./file"] }` |
| `fs_permissions = [{ access = "read", path = "./dir" }]` | `test.solidity.fsPermissions: { readDirectory: ["./dir"] }` |
| `fs_permissions = [{ access = "write", path = "./dir" }]` | `test.solidity.fsPermissions: { dangerouslyWriteDirectory: ["./dir"] }` |
| `fs_permissions = [{ access = "read-write", path = "./dir" }]` | `test.solidity.fsPermissions: { dangerouslyReadWriteDirectory: ["./dir"] }` |
| `allow_internal_expect_revert = true` | `test.solidity.allowInternalExpectRevert: true` |
| `isolate = true` | `test.solidity.isolate: true` |
| `[bind_json]` `include = ["path/to/EIP712Types.sol"]` | `test.solidity.eip712Types: { include: ["path/to/EIP712Types.sol"] }` (Hardhat 3.5.0+) — see "EIP-712 cheatcodes" reference section |

**Note on `isolate`:** The global `isolate = true` in `foundry.toml` maps to `test.solidity.isolate: true`. **Function-level** inline `/// forge-config: default.isolate = true` overrides are supported as of Hardhat 3.5.0 / EDR `0.12.0-next.33` ([edr#1349](https://github.com/NomicFoundation/edr/issues/1349) closed). **Contract-level** inline `isolate` directives are still silently ignored. **Do NOT set `isolate` globally as a workaround for contract-level usage** — it dramatically slows down the entire test suite. Only set it globally if `foundry.toml` has `isolate = true` at the `[profile.default]` level (meaning it was already global in Forge). If only specific contracts use contract-level inline `isolate`, document it as a gap.

**Note on inline config scope:** Hardhat only supports inline `forge-config:` directives at the **function level** (on individual test functions). Foundry also supports **contract-level** inline config (directives placed on the contract definition, which apply to all functions in that contract). Contract-level directives are silently ignored by Hardhat.

**When to apply global workarounds for unsupported inline config:** Not all settings are safe to set globally. Evaluate the side effects before applying a global workaround:

- **Safe to set globally:** `allowInternalExpectRevert` — enabling this globally is harmless; it only changes behavior when `vm.expectRevert` is used on internal/library calls, and enabling it on tests that don't use that pattern has no effect.
- **NOT safe to set globally:** `isolate` — this forces each test call to run in a separate EVM context, which dramatically slows down the entire test suite. If only a few test contracts/functions use `isolate`, setting it globally would impose a major performance penalty on all tests. **Do NOT set `isolate` globally as a workaround.** Instead, leave it as a documented gap in the migration report.
- **Use judgement for other settings:** `disableBlockGasLimit`, etc. — consider whether enabling them globally changes behavior for tests that don't expect it. When in doubt, do NOT set globally; document as a gap instead. (Note: `evm_version` is supported inline at function level since 3.5.0; only contract-level scope or global needs would force a workaround.)

The guiding principle: only apply a global workaround if it is **behaviorally neutral** for tests that don't use the setting. If a global setting would change behavior or performance for unrelated tests, leave it as a gap.

**`fsPermissions` key distinction:** `readFile`/`writeFile`/`readWriteFile` use **exact path matching** (single file). `readDirectory`/`dangerouslyWriteDirectory`/`dangerouslyReadWriteDirectory` use **prefix matching** (recursive directory access). Use the directory variants when the Foundry path points to a directory.

**Note:** This table is not exhaustive. Hardhat 3 may support additional test settings not listed here. When encountering a Foundry test setting without a mapping in this table, check the [Hardhat 3 documentation](https://hardhat.org/docs) and the TypeScript type definitions in `node_modules/hardhat/src/internal/builtin-plugins/solidity-test/type-extensions.ts` before concluding it has no equivalent.

### Network configuration (`networks`)

Map `[rpc_endpoints]` from foundry.toml. Use `configVariable()` for environment variables (lazy resolution — only resolved when needed).

**Note:** Foundry uses `"${VAR_NAME}"` syntax for env vars. Strip the `${}` wrapper when passing to `configVariable("VAR_NAME")`.

**Tip:** The `chainId` for each network can often be found in the `[etherscan]` section of the same foundry.toml.

| foundry.toml | hardhat.config.ts |
| --- | --- |
| `[rpc_endpoints]` `mainnet = "${RPC_MAINNET}"` | `networks.mainnet: { type: "http", chainId: 1, url: configVariable("RPC_MAINNET") }` |

`configVariable()` also supports a format parameter for URL templates:

```typescript
url: configVariable(
  "ALCHEMY_API_KEY",
  "https://eth-mainnet.g.alchemy.com/v2/{variable}",
);
```

Reference: https://hardhat.org/docs/reference/configuration#network-configuration

### Verification / Etherscan (`verify`)

Map `[etherscan]` from foundry.toml. In Foundry, this section configures API keys for contract verification via `forge verify-contract` and `forge create --verify` (ref: https://book.getfoundry.sh/reference/config/etherscan). Requires the `@nomicfoundation/hardhat-verify` plugin in Hardhat 3.

**IMPORTANT:** Hardhat 3 uses Etherscan API v2 which requires a **single API key** for all supported chains. Foundry often has per-chain keys (`ETHERSCAN_API_KEY_MAINNET`, etc.). Document this difference with a comment.

```typescript
verify: {
  etherscan: {
    apiKey: configVariable("ETHERSCAN_API_KEY"),
  },
},
```

### Compiler warning suppression (`warnings`)

Map `ignored_warnings_from` from `foundry.toml` to the [`hardhat-ignore-warnings`](https://www.npmjs.com/package/hardhat-ignore-warnings) plugin. The plugin is HH3-compatible (`peerDependencies: { hardhat: "^3.1.0" }` as of v0.3.0). Add it to `devDependencies` per Step 2.

Foundry's `ignored_warnings_from = ["lib/foo", "src/legacy"]` silences **all** compiler warnings originating from the listed paths. Hardhat 3 has no built-in equivalent — the plugin provides path-keyed suppression via a top-level `warnings` field.

Register the plugin and translate each path into a glob key with `{ default: "off" }`:

```typescript
import { defineConfig } from "hardhat/config";
import hardhatIgnoreWarnings from "hardhat-ignore-warnings";

export default defineConfig({
  plugins: [hardhatIgnoreWarnings],
  solidity: { ... },
  warnings: {
    "lib/foo/**/*": { default: "off" },
    "src/legacy/**/*": { default: "off" },
  },
});
```

**Path-to-glob conversion:**

- Directory paths (e.g., `"lib/foo"`) become `"lib/foo/**/*"` to match every file under that directory recursively.
- File paths (e.g., `"src/legacy/Old.sol"`) stay as-is — no glob suffix needed.

When combining with other plugins (e.g., `hardhat-verify`), pass them all in the `plugins` array.

**Beyond `ignored_warnings_from`:** The plugin also supports warning-code-specific rules (e.g., `'unused-param': 'off'`) and inline `// solc-ignore-next-line <code>` comments. Foundry has no equivalent for those — they're additional capabilities the user gains by adopting the plugin, not part of the mapping.

### Foundry settings without direct Hardhat equivalents

These foundry.toml settings and commands have no direct Hardhat equivalent. Leave a comment in hardhat.config.ts for each one present in the project.

**Important:** Presence in this table does NOT mean the feature should be classified as 🚩 Gap in the migration report. Each row includes a Status that indicates the actual parity level — many have workarounds, partial support, or are tools that work standalone regardless of build system. Use the Status column to determine the correct parity classification.

| foundry.toml | Status |
| --- | --- |
| `dynamic_test_linking` | Foundry-only |
| `gas_snapshot_check` / `[profile.gas]` | Supported (✅ Full) — `npx hardhat test solidity --snapshot` / `--snapshot-check`; gas values match Forge. **Caveat (not a gap):** Hardhat's whole-suite `.gas-snapshot` uses a `Contract#function` format incompatible with Forge's `Contract:function()` (by design), so `--snapshot-check` against a committed _Forge_-format `.gas-snapshot` errors with HHE803 — regenerate the baseline with `--snapshot`. See Update Step 5b "KEY FACT" and [hardhat#8357](https://github.com/NomicFoundation/hardhat/issues/8357). Docs: https://hardhat.org/docs/guides/testing/gas-snapshots |
| `[bind_json]` | 🟡 Partial — the `include` field has a direct Hardhat equivalent in `test.solidity.eip712Types.include` (added in Hardhat 3.5.0). Both tools require the user to point at the file(s) defining EIP-712 structs so `vm.eip712HashStruct` / `vm.eip712HashType` can resolve names. Hardhat does NOT emit a `JsonBindings.sol`-style helper file — if any Solidity file in the project imports `schema_*` constants or `serialize`/`deserialize` helpers from the generated file, that part has no Hardhat equivalent (grep for `JsonBindings` imports / `schema_` usage to confirm before classifying). See the "EIP-712 cheatcodes" reference section below. |
| `[fmt]` | 🟡 Partial — `forge fmt` works standalone regardless of build tool; `prettier-plugin-solidity` is a mature alternative |
| `[lint]` | Foundry-only (projects typically use prettier or solhint) |
| `forge doc` | 🟡 Partial — community plugin [`@solarity/hardhat-markup`](https://www.npmjs.com/package/@solarity/hardhat-markup) supports HH3 for NatSpec documentation generation |
| `out = "out"` | Hardhat uses its own `artifacts/` + `cache/` dirs |
| `libs = ["lib"]` | Hardhat resolves `lib/` deps via `remappings.txt`; npm deps via Node.js resolution |
| `[profile.zksync]` / zkSync compilation (`--zksync`, `fallback_oz`, `is_system`, `mode`) | Foundry-only — check whether `@matterlabs/hardhat-zksync` supports HH3 (as of early 2026 it targets HH2 only). If not, zkSync contracts compile but can't be meaningfully tested |
| `/// forge-config:` inline test config | 🟡 Partially supported — **function-level only**. `forge-config:` prefix is backwards-compatible. Fuzz/invariant settings and `allowInternalExpectRevert` have worked inline at function level since Hardhat 3.3.0; `isolate` and `evm_version` were added in Hardhat 3.5.0 / EDR `0.12.0-next.33` ([edr#1349](https://github.com/NomicFoundation/edr/issues/1349) closed). **Contract-level** inline config (directives on contract definitions) is NOT supported in any Hardhat 3 release — use global config as workaround. See: [docs](https://hardhat.org/docs/guides/testing/inline-configuration) |
| `forge coverage --ir-minimum` | Not needed — Hardhat 3's built-in `--coverage` handles via-IR projects natively without a separate flag. Note: coverage instrumentation may cause some tests to fail that pass without coverage — these are coverage-specific and should be investigated separately. |

**Note:** This table is not exhaustive. Before concluding a Foundry setting has no Hardhat equivalent, check `test.solidity` type definitions in `node_modules/hardhat/src/internal/builtin-plugins/solidity-test/type-extensions.ts` and the [Hardhat 3 documentation](https://hardhat.org/docs).

### IMPORTANT: Unknown settings

If you encounter foundry.toml settings that don't have a clear mapping in the tables above:

1. Check if it fits under `solidity.settings` (which accepts any solc input JSON option)
2. Check if it fits under `test.solidity`
3. **If neither — STOP and ask the user. Do not guess.**

**Do not assume mappings without documentation.** When uncertain about what a Foundry setting does, look up the Foundry docs (https://book.getfoundry.sh/reference/config/) before mapping it to a Hardhat equivalent.

### IMPORTANT: Leave comments for unsupported configs

For any foundry.toml setting that cannot be mapped due to Hardhat limitations:

1. Add a `// TODO:` comment in `hardhat.config.ts` describing the original Foundry setting
2. Include a link to the relevant Hardhat GitHub issue if one exists
3. Explain the workaround (if any) or that no equivalent exists

### IMPORTANT: Apply Forge defaults that differ from Hardhat

After mapping all **explicit** `foundry.toml` settings, consult the **"Reference: Forge vs Hardhat 3 default values"** table at the bottom of this file. For every setting where Forge's default differs from Hardhat's default, **explicitly set that value in `hardhat.config.ts`** — even if the setting is absent from `foundry.toml`.

For example: if `foundry.toml` does not set `optimizer` or `src`, Forge still defaults to `optimizer = true` and `src = "src"`. Hardhat's defaults are different (`optimizer` off, `paths.sources = "./contracts"`), so these must be explicitly configured to preserve the same behavior.

### Inline progress check

After finishing the config migration, briefly confirm with the user which settings were migrated, which are Foundry-only, and which have gaps — before moving on to Step 4. The full detailed report is presented in Step 7.

## Step 4: Handle absolute imports

Hardhat 3 does not support absolute imports in `.sol` files. Scan all `.sol` files for absolute imports (imports that don't start with `./`, `../`, or a package name like `@openzeppelin/`).

Common patterns to look for:

- `import "src/SomeContract.sol";`
- `import "contracts/SomeContract.sol";`
- `import "test/helpers/SomeHelper.sol";`
- `import "lib/some-dep/SomeFile.sol";`

**How to find absolute imports:** Absolute imports come in two Solidity syntaxes. Search for both:

- Direct imports: `import "src/...";`
- Named imports: `import { Foo } from "src/...";`

Use a regex like `import\s.*["'](src|test|lib|contracts)/` to catch both forms and both quote styles (Solidity allows single or double quotes for import paths). Make sure to search **all** directories that contain `.sol` files, including `examples/`, `script/`, and any other non-standard directories — not just `src/` and `test/`.

### Decision rule:

Count the number of **files** containing absolute imports (not the number of import statements):

- **~10 or fewer files** → convert them to relative imports (more compatible with all tools)
- **Significantly more than 10 files** → add remappings to the root `remappings.txt` file (e.g., `src/=./src/`, `tests/=./tests/`, `lib/=./lib/`). This is simpler than creating scoped remappings in subdirectories and works for all files.

**Note:** This is a heuristic, not a strict threshold. When the count is close to the limit (e.g., 8–12 files), use your judgement. Consider the overall complexity of the changes, how many imports per file, and whether the project already uses remappings extensively. When in doubt, prefer relative imports — they are more portable and don't add implicit resolution rules.

Reference: https://hardhat.org/docs/cookbook/absolute-imports

## Step 5: Compile

**IMPORTANT:** Only compile and test using the **default** build profile. Non-default profiles (production, coverage, etc.) are migrated for completeness but should NOT be compiled or tested during this migration session. They may require environment-specific tooling (e.g., native solc for viaIR) that isn't available.

Run and store the output:

```
npx hardhat compile --show-stack-traces 2>&1 | tee /tmp/hardhat-compile-output.txt
```

Do NOT run `npx hardhat compile --build-profile production` or any other profile.

**Output caching:** If `/tmp/hardhat-compile-output.txt` already exists, analyze whether any relevant changes have been made since it was written. If not, read the stored file instead of re-running the command. Only re-run when it is justified. When re-running, **state in your response what was fixed and why the re-run is necessary** before issuing the command — e.g., _"Added `lib/=./lib/` remapping to fix HHE902 error on absolute lib imports — re-compiling."_

If compilation fails, **group errors by type** before presenting them to the user. Then:

1. Fix import path issues, missing dependencies, or config problems
2. If you see `HHE902: ... is not exported by the package`, see the "npm packages with restrictive `exports` fields" reference section
3. If you see Solidity type mismatch errors involving types from shared dependencies, see the "Transitive dependency version conflicts" reference section
4. Re-run (overwriting the cached output file) until compilation succeeds

Do not proceed to Step 6 until compilation passes cleanly.

## Step 6: Run Solidity tests

Run and store the output:

```
npx hardhat test solidity --show-stack-traces 2>&1 | tee /tmp/hardhat-test-output.txt
```

Do NOT run tests with non-default build profiles.

**Output caching:** If `/tmp/hardhat-test-output.txt` already exists, analyze whether any relevant changes have been made since it was written. If not, read the stored file instead of re-running the command. Only re-run when it is justified. When re-running, **state in your response what was fixed and why the re-run is necessary** before issuing the command — e.g., _"Commented out UnsupportedCheatcode tests — re-running to confirm remaining tests pass."_

If tests fail:

1. Analyze the failures — distinguish between real test failures vs migration issues
2. **Group errors by type** before presenting them to the user
3. **Handle `UnsupportedCheatcode` errors:**
   - **ONLY** apply this procedure to tests whose error line reads exactly `Error: UnsupportedCheatcode: vm.xyz(...)`. Do **NOT** comment out tests that fail with any other error type — even if those errors appear migration-related. All other failures must be handled via items 4–6 below (fix or flag to the user).
   - For each test function that triggers an `UnsupportedCheatcode` error, comment out the **entire function** (signature + body) using `//` line comments, and **preserve the original code** as commented-out lines so the disabled test remains readable. Use `/* */` block comments only if the function body itself contains no block comments. Prefer `//` line comments in all cases to avoid nesting issues.
   - Add the HARDHAT-SKIP header immediately before the commented-out function, e.g.:
     ```solidity
     // HARDHAT-SKIP: This test uses vm.xyz() which is not supported by Hardhat 3 (UnsupportedCheatcode).
     // See: https://github.com/NomicFoundation/hardhat/issues/XXXX
     // function test_foo() public {
     //   vm.xyz("some", abi.encode(param));
     //   assertEq(result, expected);
     // }
     ```
     **Do NOT** replace the original function body with an empty stub. An empty `test_foo()` would pass trivially and give a false green — the test must not run at all.
   - Re-run the tests (overwriting the cached output file) to confirm remaining tests pass
4. **Handle suspected EDR / Hardhat bugs:**
   - Some test failures are neither `UnsupportedCheatcode` nor fixable by editing the config — they indicate a behavioral difference or outright bug in Hardhat / EDR. These tests should **not** be modified, commented out, or worked around — the test code is correct Foundry-idiomatic code.
   - When you suspect a group of failures has the same root cause, **investigate the call chain** to confirm. Trace from the failing test function through any helpers or modifiers to identify exactly which cheatcode call returns unexpected results or throws unexpectedly.
   - **Group tests by root cause** — tests failing for the same reason should be treated as a single bug, not as separate issues.
   - Once the root cause is confirmed, **write a structured bug report** to `hardhat-migration/bugs/<descriptive-slug>.md` (create the `hardhat-migration/bugs/` directory if needed). **Use a slug that describes the affected feature or cheatcode — do NOT prefix with `edr-` or `hardhat-`** (e.g. `vm-readCallers-prank-state.md`, not `edr-vm-readCallers-prank-state.md`). The responsible layer will be determined through the upstream filing process. The file should include:
     - Issue title suitable for filing as a GitHub issue on [NomicFoundation/hardhat](https://github.com/NomicFoundation/hardhat/issues) or [NomicFoundation/edr](https://github.com/NomicFoundation/edr/issues) depending on where the root cause lies
     - A self-contained minimal Solidity reproduction (small test contract + minimal `hardhat.config.ts`)
     - Steps to reproduce
     - Expected vs actual behaviour (table form)
     - The observed error message
     - The exact failing call chain with file paths and line numbers linking into the project
     - Which test(s) fail (file, contract, function name)
     - Suggested fix direction for the EDR team (reference Foundry's equivalent implementation if helpful)
     - The available workaround, if any — plus an explicit note stating the workaround is **not applied** and why (the test code is correct; applying it would mask the bug and add unnecessary boilerplate)
   - **This file is a draft for the user to file as a GitHub issue** at [NomicFoundation/hardhat](https://github.com/NomicFoundation/hardhat/issues) or [NomicFoundation/edr](https://github.com/NomicFoundation/edr/issues), depending on where the root cause lies. The skill produces the report locally; the user decides whether and where to file it upstream. The migration report references it and surfaces filing as the user's next step — do not assume an upstream issue exists yet.
   - Leave the failing tests as-is in the codebase. Do not comment them out. They are genuine blockers.
5. Fix other migration-related issues (import paths, config differences)
6. For real test failures unrelated to migration, flag them to the user
7. If failures persist after applying fixes, ask the user whether they want to keep iterating or proceed directly to generating a **Failed** verdict report

**Common error patterns and fixes:**

- `"call didn't revert at a lower depth than cheatcode call depth"` or `"reverted with an unrecognized custom error"` on tests using `vm.expectRevert` on internal/library calls → enable `test.solidity.allowInternalExpectRevert: true` in config (maps Foundry's `allow_internal_expect_revert`)
- Gas-related failures, out-of-gas reverts, or unexpected reverts in large tests → check that `test.solidity.gasLimit` matches `foundry.toml`'s `gas_limit` (must be a bigint with `n` suffix)
- `vm.envString: environment variable "X" not found` — tests that read env vars via `vm.envString()` or `vm.envOr()` in `setUp()` will fail if those vars aren't set. Fix: set the required env var in the Hardhat test script in `package.json` (e.g., `"test": "MY_VAR=value hardhat test solidity"`)

When tests pass, **always show the full test output to the user** so they can verify the results themselves.

**Coverage verification (optional):** If the project has a `forge coverage` script in `package.json`, run `npx hardhat test solidity --coverage` after tests pass to verify coverage works. Coverage instrumentation can reveal issues not seen in normal test runs. Compare the number of passing tests with and without `--coverage` — any discrepancy should be noted in the migration report. Note: Hardhat 3's coverage handles via-IR projects natively; Forge's `--ir-minimum` flag has no equ

…(truncated)
