Hardhatは、Ethereumなど EVM 互換チェーン向けのスマートコントラクト開発環境です。Solidityのコンパイル、テスト、デバッグ、デプロイを1つのコマンドラインツールで扱えます。開発元は非営利団体のNomic Foundationで、最新版は2026年11月12日公開の3.22.0です。この記事では、Hardhat 3でプロジェクトを作り、SolidityテストとTypeScriptテストを書き、OpenZeppelinを使ったトークンをデプロイして検証するまでを解説します。プロジェクト作成からテスト、ローカルへのデプロイまでは、2026年11月13日にmacOS(x86_64)・Node.js v26.5.0・npm 11.17.0・Hardhat 3.22.0で実行した結果を載せ、Sepoliaの手順は公式ドキュメントに沿って示します。次のAmsterdamハードフォークでガス使用量がどれだけ変わるかも、同じ環境で測りました。検索で見つかる日本語の入門記事はHardhat 2時代の手順(設定が hardhat.config.js、テストネットが終了済みのRopsten)のままのものが多いので、2系との違いとサポート終了日、Foundryとの選び方もまとめます。
まとめ:Hardhat 3で押さえる要点
- Hardhat 3が現行版:3.0.0は2025年8月13日にベータとして公開され、2026年5月28日の3.7.0でベータを抜けた(公式ブログでの正式版発表は6月1日)。Node.jsはv22.13.0以上が必要。
- Hardhat 2はサポート終了が決まっている:Hegotaハードフォークのメインネット有効化か2027年6月1日の早いほうで終了し、以後はセキュリティ修正も出ない。新規はHardhat 3で始める。
- テストはSolidityとTypeScriptの両方で書ける:Solidityテストはforge-std互換で、同じ
.t.solがFoundry 1.9.0のforge testでも同じ結果になった。TypeScriptテストはnetwork.create()で接続する(network.connect()は3.4.0で非推奨)。 - ガス計測とカバレッジは本体に入った:
--gas-statsと--coverageを付けるだけで動く。2系のhardhat-gas-reporterとsolidity-coverageは移行時に外す。 - Amsterdamのガス変更を先に試せる:
hardfork: "amsterdam"のネットワークで測ると、Counterのデプロイは263,088から1,714,624ガスへ約6.5倍になった。ガス量を固定値で書いたコードとテストは要点検。 - 秘密鍵は環境変数でなくkeystoreに置く:
configVariable()とhardhat-keystoreで暗号化して保存し、デプロイはHardhat Ignition、検証はhardhat verifyで行う。 - Foundryとの選択はチームの言語で決める:TypeScriptでフロントエンドやスクリプトを書くチームはHardhat 3、Solidityだけで完結させたいならFoundry。
Hardhatの役割とHardhat 3で変わった前提
Hardhatの仕事は、Solidityのソースをコンパイルし、手元の模擬チェーンでテストを回し、本番やテストネットへデプロイするまでの作業をタスクとしてまとめることです。言語そのものの書き方はSolidity入門|開発環境の作り方・基本構文・Sepoliaデプロイ手順【2026年10月】で扱っているので、ここではツールとしてのHardhatに絞ります。
Hardhat 3を構成する3つの部品
Hardhat 3は、タスクを実行するCLI本体、テストとローカル実行を担うシミュレーター、デプロイ管理のHardhat Ignitionで構成されます。シミュレーターはRustで書かれたEDR(Ethereum Development Runtime)で、2系の「Hardhat Network」に当たります。3.22.0に同梱されるEDRは0.25.1です。
EDRは、どの種類のチェーンを模擬するかを chainType で選べます。2系の開発用ネットワークは常にEthereumメインネットとして振る舞いましたが、3系ではEthereum(l1)とOP Mainnet系(op)を指定でき、それ以外のチェーンには汎用の generic を使います。
Hardhat 2との違いとサポート終了日
Hardhat 3は2系の全面的な書き直しで、2系のプロジェクトはそのままでは動きません。主な違いは次のとおりです。
| 項目 | Hardhat 2 | Hardhat 3 |
|---|---|---|
| 最新版(2026-11-13) | 2.29.2 | 3.22.0 |
| 設定ファイル | CommonJS可 | ESM必須 |
| プラグインの読み込み | importの副作用 | plugins配列に明記 |
| JS/TSテストの実行 | Mocha固定 | node:testかMochaを選択 |
| Solidityテスト | なし | 標準搭載 |
| 模擬チェーン | L1のみ | l1・op・generic |
| ガス計測・カバレッジ | 外部プラグイン | 本体のフラグ |
| サポート | 2027年6月1日までに終了 | 継続 |
2系のサポート終了は、公式の「Hardhat 2 end of life」に次のように定められています。2系は次のGlamsterdamハードフォークには対応するものの、その次のHegotaには対応しません。終了日はHegotaのメインネット有効化と2027年6月1日のどちらか早いほうで、それ以降はセキュリティ修正を含めてリリースが止まります。npmでは2系が hh2 タグで配布され続けているので、npm i hardhat を実行すると3系が入る点にも注意してください。
Hardhat 3プロジェクトの作り方
Node.jsの要件とテンプレート3種
公式のGetting startedが求めるNode.jsはv22.13.0以上です。サポート対象は、Node.jsチームが保守している偶数のメジャーバージョンに限られます。空のディレクトリで次を実行すると、対話形式でテンプレートを選べます。
mkdir hardhat-example && cd hardhat-example
npx [email protected] --init
選べるテンプレートは3つです。--templates で一覧を出し、--template で指定すれば質問なしで作成と依存パッケージのインストールまで進みます。CIや手順書ではこちらが向きます。
$ npx hardhat --init --templates
Available templates:
- node-test-runner-viem
- mocha-ethers
- minimal
$ npx hardhat --init --template node-test-runner-viem
迷ったら node-test-runner-viem を選びます。公式がNode.js標準のテストランナーを推奨しているためで、Mochaとethers.jsの組み合わせは2系から移行するプロジェクト向けです。minimal は設定ファイルだけの空のプロジェクトになります。
インストールの最後に npm audit が「12 high severity vulnerabilities」と表示しました。内訳を見ると、12件はすべてzodの1件の勧告(GHSA-mrhx-c7xc-m3w5、件数や長さに上限を付けた配列などの検証で巨大な入力を渡すとメモリを使い果たす)が、zod自身と、zodに依存するHardhatのパッケージごとに数えられたものでした。導入されたzodは3.25.76で、GitHubの勧告が示す対象範囲(4.6.6未満)に入っています。ただし、Zodの作者は2026年10月12日にGitHubのissueで、この挙動を脆弱性とは考えておらず、v3の新しい版はもう公開しないと述べています。npm audit fix --dry-run で確かめると、通常の修正では解消されず、表示どおり --force を付けると「Will install [email protected], which is a breaking change」と、サポート終了が決まった2系へ入れ替えようとします。--force は実行しないでください。auditの検出結果と、使用する処理が巨大な入力を受け付けるかという影響評価は分けて判断します。
生成されるファイルとhardhat.config.ts
node-test-runner-viem では、contracts/(Counter.sol とSolidityテストの Counter.t.sol)、test/(TypeScriptテスト)、ignition/modules/(デプロイ定義)、scripts/ と設定ファイルが作られます。3.22.0では、AIコーディングエージェント向けに CLAUDE.md と AGENTS.md、さらに .claude/skills/ と .agents/skills/ に hardhat と hardhat-toolbox-viem の2つのスキル(SKILL.md)も置かれました。テストの書き分けや network.create() の使い方が書かれているので、エージェントに古い2系の書き方をさせないための材料になります。生成された設定ファイルは次のとおりです。
import hardhatToolboxViemPlugin from "@nomicfoundation/hardhat-toolbox-viem";
import { configVariable, defineConfig } from "hardhat/config";
export default defineConfig({
plugins: [hardhatToolboxViemPlugin],
solidity: {
profiles: {
default: {
version: "0.8.34",
},
production: {
version: "0.8.34",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
},
},
networks: {
hardhatMainnet: {
type: "edr-simulated",
chainType: "l1",
},
hardhatOp: {
type: "edr-simulated",
chainType: "op",
},
sepolia: {
type: "http",
chainType: "l1",
url: configVariable("SEPOLIA_RPC_URL"),
accounts: [configVariable("SEPOLIA_PRIVATE_KEY")],
},
},
});
2系から来た人が戸惑うのは、プラグインが plugins 配列に入れないと有効にならない点です。solidity.profiles はビルドプロファイルで、普段のテストは最適化なしの default、デプロイと検証は最適化ありの production が使われます。ネットワークは type で区別し、edr-simulated は手元の模擬チェーン、http は外部のRPCです。
npx hardhat build(compile は別名)を初めて実行すると、指定したsolcが自動でダウンロードされます。後述の金庫コントラクトを加えた状態では Compiled 4 Solidity files with solc 0.8.34 (evm target: osaka) と表示されました。EVMの既定ターゲットがosakaである点は、未追随のL2へデプロイするときに効いてきます。
SolidityテストとTypeScriptテストの書き分け
npx hardhat test は両方のテストをまとめて実行し、test solidity と test nodejs で片方だけを回せます。テンプレートのままなら 5 passing (3 solidity, 2 nodejs) と表示されます。使い分けの目安は、コントラクト単体の振る舞いとファズテストはSolidity、イベントの集計や複数コントラクトをまたぐ手順、フロントエンドと同じviemのコードで確かめたい処理はTypeScriptです。
ここからは、残高を超える引き出しをカスタムエラーで止める小さな金庫コントラクトを例にします。コントラクトは contracts/Vault.sol、Solidityテストは contracts/Vault.t.sol、TypeScriptテストは test/Vault.ts に保存します。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.34;
import "hardhat/console.sol";
contract Vault {
error InsufficientBalance(uint256 requested, uint256 available);
mapping(address => uint256) public balanceOf;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
console.log("withdraw: requested=%s available=%s", amount, balanceOf[msg.sender]);
if (amount > balanceOf[msg.sender]) {
revert InsufficientBalance(amount, balanceOf[msg.sender]);
}
balanceOf[msg.sender] -= amount;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "transfer failed");
}
}
送金に transfer を使うと、solc 0.8.34は「’transfer’ is deprecated and scheduled for removal」と警告を出します。transfer は転送先に2,300ガスしか渡さないため、後述のAmsterdamのガス変更で壊れる書き方の代表でもあります。古い入門記事を写すとまず出会う警告なので、call で書いておきます。
forge-std互換のSolidityテスト
Solidityテストは contracts/ に .t.sol として置くか、test/ に置きます。forge-std の Test を継承し、vm.expectRevert などのチートコードがそのまま使えます。3.22.0のテンプレートが入れるforge-stdはv1.17.0です。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.34;
import {Vault} from "./Vault.sol";
import {Test} from "forge-std/Test.sol";
contract VaultTest is Test {
Vault vault;
function setUp() public {
vault = new Vault();
vault.deposit{value: 100}();
}
function test_RevertWhenOverWithdraw() public {
vm.expectRevert(abi.encodeWithSelector(Vault.InsufficientBalance.selector, 500, 100));
vault.withdraw(500);
}
receive() external payable {}
}
引数を取るテスト関数はファズテストになり、テンプレートの testFuzz_Inc(uint8) は既定で256回のランダム入力で実行されました。
node:testとviemで書くTypeScriptテスト
TypeScriptテストでは、まず network.create() で模擬チェーンへの接続を作り、そこからviemのクライアントとテスト用ヘルパーを取り出します。同じ初期状態を何度も使う場合は loadFixture に関数を渡すと、2回目以降はデプロイをやり直さずスナップショットから復元されます。
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { network } from "hardhat";
describe("Vault", async function () {
const { viem, networkHelpers } = await network.create();
async function deployVaultFixture() {
const vault = await viem.deployContract("Vault");
await vault.write.deposit({ value: 100n });
return { vault };
}
it("残高を超える引き出しはカスタムエラーで止まる", async function () {
const { vault } = await networkHelpers.loadFixture(deployVaultFixture);
await viem.assertions.revertWithCustomErrorWithArgs(
vault.write.withdraw([500n]),
vault,
"InsufficientBalance",
[500n, 100n],
);
});
it("残高の範囲なら引き出せる", async function () {
const { vault } = await networkHelpers.loadFixture(deployVaultFixture);
const [wallet] = await viem.getWalletClients();
await vault.write.withdraw([40n]);
assert.equal(await vault.read.balanceOf([wallet.account.address]), 60n);
assert.equal(await (await viem.getPublicClient()).getBalance({ address: vault.address }), 60n);
});
});
npx hardhat test nodejs test/Vault.ts で2件とも通り、tsc --noEmit でも型エラーは出ませんでした。vault.read.balanceOf の引数と戻り値に型が付くのは、Hardhat 3がコンパイル時にコントラクトの型定義を生成するためです。2系でTypeChainのようなコード生成ツールが担っていた役割を、本体が受け持っています。
console.logとSolidityスタックトレースでのデバッグ
コントラクトに import "hardhat/console.sol"; を書くと、Solidityの中から console.log で値を出力できます。上のSolidityテストに、残高を超えて引き出すだけの test_BrokenOnPurpose() を足してわざと落とすと、ログに続いてSolidityのどの行でrevertしたかが表示されます。
withdraw: requested=500 available=100
1) VaultTest#test_BrokenOnPurpose()
Error: reverted with custom error 'InsufficientBalance(500, 100)'
at Vault.withdraw (contracts/Vault.sol:18)
at VaultTest.test_BrokenOnPurpose (contracts/Vault.t.sol:21)
エラー名と引数が復号されて出るので、どの条件で落ちたかをイベントやトランザクションの中身から推測する必要がありません。呼び出しの階層とガスまで見たいときは -vvvv のように -v を重ねると、Solidityテストの呼び出しトレースが表示されます。console.log は実チェーンでは何もしない呼び出しになりますが、ガスは消費するので、デプロイ前に消しておきます。
ガス計測とカバレッジの取り方
どちらもテストコマンドにフラグを付けるだけです。--gas-stats では関数ごとの最小・平均・中央値・最大ガスと、デプロイのガス、バイトコードのサイズが表になります。テンプレートの Counter では、inc の中央値が27,674、デプロイが263,076ガス、バイトコードが975バイトでした。--gas-stats-json でJSONにも書き出せるので、プルリクエストごとに比べたい場合に使えます。
npx hardhat test solidity --gas-stats
npx hardhat test --coverage
--coverage は、SolidityテストとTypeScriptテストの両方を合算して行カバレッジを出し、HTMLのレポートを coverage/html に保存します。金庫のSolidityテストを上の残高不足の1件だけにし、TypeScriptテストの test/Vault.ts を一時的にプロジェクト外へ退避したうえで、テンプレートの Counter のテストと合わせて測ると、次のように未実行の行番号が出ました。
║ File Path │ Line % │ Statement % │ Uncovered Lines ║
║ contracts/Vault.sol │ 57.14 │ 62.50 │ 20-22 ║
║ contracts/Counter.sol │ 100.00 │ 100.00 │ - ║
20〜22行は引き出しが成功する経路で、送金部分が一度もテストされていないことがわかります。Uncovered Lines に送金や権限チェックの行が残っていたら、その経路をテストに加えます。ただし、実行済みの行でも、期待結果を確かめるアサーションが足りなければ不具合は見逃します。なお3.22.0からは、--no-compile を付けて計測用のコードが入らない状態で --coverage を実行すると、エラーで止まります。
Amsterdamハードフォークに備えたガスのテスト
Ethereumの次のアップグレードGlamsterdamは、実行層のAmsterdamと合意層のGloasをまとめたものです。テストネットのSepoliaでは2026年10月6日の有効化がEthereum Foundationのブログで告知され、Hoodiは10月26日に予定されていましたが延期されました。さらに11月6日には、状態の作成と読み書きのガスを引き上げるEIP-8037とEIP-8038で前提が崩れるコントラクトが見つかったとして、Hoodiでの有効化を問題の解決まで見送り、開発者に自分のコントラクトの点検を求める呼びかけが出ています。メインネットの日程は2026年11月13日時点で決まっていません。
hardfork: “amsterdam” のネットワーク設定
Hardhat 3はAmsterdamのEIPを段階的に実装しており(3.20.0でEIP-2780とEIP-8246、3.21.0でEIP-7954、3.22.0でEIP-8038を追加)、公式のAmsterdam supportには13のEIPが並んでいます。対応は実験的で、既定のハードフォークはosakaのままです。試すには、networks に次の1件を足します。
edrAmsterdam: {
type: "edr-simulated",
chainType: "l1",
hardfork: "amsterdam",
},
TypeScriptのスクリプトやテストは --network edrAmsterdam で、Solidityテストも同じ指定で、このネットワークのハードフォークで実行されます。実行すると「”amsterdam” hardfork, which is experimental and not yet finalized」という警告が出ます。
osakaとamsterdamで測ったgasUsedの差
同じ操作を hardhatMainnet(osaka)と edrAmsterdam で実行し、レシートの gasUsed を比べました。どちらもテンプレートの初期状態(テスト用アカウントに10,000 ETH)から始め、コントラクトは default プロファイル(最適化なし)でビルドしています。次の scripts/compare-gas.ts を npx hardhat run scripts/compare-gas.ts で実行すると、送金の行を再現できます。Counter の行は、同じループの中で viem.deployContract("Counter") と counter.write.inc() を2回続けて呼び、各トランザクションのハッシュを同じように調べて測りました。
import { network } from "hardhat";
for (const name of ["hardhatMainnet", "edrAmsterdam"]) {
const { viem, provider } = await network.create({ network: name });
const publicClient = await viem.getPublicClient();
const [sender] = await viem.getWalletClients();
// まだ使われていないアドレスへ1 ETHを送る
const hash = await sender.sendTransaction({
to: "0x000000000000000000000000000000000000beef",
value: 10n ** 18n,
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
const trace = await provider.request({
method: "debug_traceTransaction",
params: [hash, { tracer: "stateGasTracer" }],
});
console.log(name, receipt.gasUsed, trace);
}
| 操作 | osaka | amsterdam | 倍率 |
|---|---|---|---|
| Counterのデプロイ | 263,088 | 1,714,624 | 6.5倍 |
| Vaultのデプロイ | 479,150 | 3,264,862 | 6.8倍 |
| inc(初回・空のスロットへ書き込み) | 44,774 | 126,794 | 2.8倍 |
| inc(2回目以降) | 27,674 | 28,874 | 1.04倍 |
| 新しいアドレスへの1 ETH送金 | 21,000 | 204,600 | 9.7倍 |
| 自分宛ての1 wei送金 | 21,000 | 12,000 | 0.57倍 |
増え方が大きいのは、新しい状態を作る操作です。EIP-8037ではガスが「状態ガス」と「実行ガス」に分かれ、状態ガスは新しいアカウント、空のストレージスロットへの書き込み、デプロイされるコードに課されます。debug_traceTransaction に stateGasTracer を指定すると内訳が取れ、初回の inc は実行ガス28,874と状態ガス97,920、新しいアドレスへの送金は実行ガス21,000と状態ガス183,600でした。反対に、自分宛ての送金はEIP-2780で一律21,000だった基本料が12,000に下がっています。なお、ブロックの gasUsed は2つのうち大きいほうだけを数えるので、初回の inc だけを含むブロックは97,920と、トランザクションの合計より小さく表示されます。
この結果から点検すべき箇所は3つあります。gasUsed を21,000などの固定値で比べているテスト、transfer や send のように渡すガスが2,300に固定された送金、デプロイやバッチ処理のガス上限をコードや運用スクリプトに書き込んでいる箇所です。ガス量の固定値を検証するテストは期待値との不一致で失敗し、送金先の処理に必要なガスや固定したガス上限によっては、送金やデプロイも失敗する可能性があります。ただし、EIP-8037とEIP-8038は上の呼びかけのとおり仕様が見直される可能性があり、表の数値もHardhatの更新で変わりえます。メインネットの日程が出るまでは、数値を確定値として扱わず、CIにamsterdamのジョブを1本足して差分を追う使い方が現実的です。
ローカルノード・フォーク・OPチェーンの模擬
ローカルノードの起動とウォレット接続
テストはプロセス内の模擬チェーンで完結しますが、MetaMaskやフロントエンドから接続したいときは npx hardhat node でJSON-RPCサーバーを立てます。http://127.0.0.1:8545/ で待ち受け、10,000 ETHを持つテスト用アカウントと秘密鍵が表示されます。この秘密鍵は誰でも知っている値なので、メインネットで送金に使うと資金を失います。別のターミナルから --network localhost を付ければ、デプロイやスクリプトをこのノードに向けられます。
メインネットのフォーク設定とブロック番号の固定
実在するコントラクト(DEXやトークン)とのやり取りを試すときは、本物のネットワークの状態を手元に複製するフォークを使います。TypeScriptテストとSolidityテストで設定場所が違う点に注意してください。
export default defineConfig({
plugins: [hardhatToolboxViemPlugin],
solidity: "0.8.34",
networks: {
mainnetFork: {
type: "edr-simulated",
forking: {
url: configVariable("MAINNET_RPC_URL"),
blockNumber: 23819000,
},
},
},
test: {
solidity: {
forking: {
url: configVariable("MAINNET_RPC_URL"),
blockNumber: 23819000n,
},
},
},
});
TypeScriptテストは npx hardhat test nodejs --network mainnetFork か、テスト内の network.create("mainnetFork") でフォークに接続します。Solidityテストは test.solidity.forking の設定で動き、ブロック番号は 23819000n とBigIntで書きます(ネットワーク側は数値)。ブロック番号を固定すると結果が毎回同じになり、取得した状態がキャッシュされて2回目以降も速くなります。
OP Stackの模擬とL1データ手数料
OP Stack系のL2では、手数料にL1へデータを書く分が加わります。Hardhat 3が模擬できるのは標準のOP Stackの動作までで、独自に改造したチェーン固有の挙動は対象外です。テンプレートの hardhatOp ネットワーク(chainType: "op")で付属の scripts/send-op-tx.ts を実行すると、viemの estimateL1Gas が Estimated L1 gas: 1600n を返しました。同じスクリプトを chainType: "l1" のネットワークに向けると、viemのクライアントに estimateL1Gas が無いため実行時エラーで止まります。
3.21.0(2026年10月22日)からは、模擬OPネットワークのトランザクションが実際にL1データ手数料を払い、レシートにも載るようになりました(それまでは課金されていませんでした)。自分宛てに1 weiを送ったレシートには、l1GasUsed が1,600、l1GasPrice が10 gwei、l1Fee が1,097,845,000,000 wei(約0.0000011 ETH)と記録されました。L2の残高の増減を厳密に確かめるテストは、3.21.0以降ではこのL1手数料を差し引いて計算します。ただし、これは模擬チェーン上の値なので、本番の見積もりはフォークか実チェーンで取り直してください。L2の手数料の内訳はブロックチェーンのロールアップとは?レイヤー2の仕組み・種類と手数料・出金日数の比較【2026年版】、OP Stackに乗るBaseの最近の変更はBase Azulとは?マルチプルーフと出金日数・ノード移行、Denimで変わった前提【2026年11月】で解説しています。
Sepoliaへのデプロイと検証の手順
keystoreによるRPCのURLと秘密鍵の管理
設定ファイルの configVariable("SEPOLIA_PRIVATE_KEY") は、値が必要になった時点で読み込まれる「設定変数」です。既定では同名の環境変数から読みますが、公式ガイドはコマンド履歴に残るなどの理由で環境変数を安全でないとしています。テンプレートに含まれる hardhat-keystore を使えば、パスワードで暗号化したファイルに保存できます。
npx hardhat keystore set SEPOLIA_RPC_URL
npx hardhat keystore set SEPOLIA_PRIVATE_KEY
npx hardhat keystore list
実行するとパスワードと値の入力を求められます。保存先は npx hardhat keystore path で確認でき、macOSでは ~/Library/Preferences/hardhat-nodejs/keystore.json とプロジェクトの外でした。プロジェクト内の .env に書く場合と違い、git add . でうっかり秘密鍵をコミットする経路が無くなります。keystore set に --dev を付けると、本番用とは別の開発用keystoreに保存されます。開発用はパスワードなしで読み出せるので、公式ガイドの想定どおり、ローカル用のRPCのURLのような機密でない値だけに使います。
OpenZeppelinのトークン実装とIgnitionのデプロイ定義
実務では、ERC-20などの標準実装をOpenZeppelin Contractsから読み込むのが一般的です。Hardhat 3ではnpmで入れるだけで、リマッピングの設定は要りません。今回入ったのは5.6.1でした。以下のコントラクトは contracts/MyToken.sol に、続くIgnitionモジュールは ignition/modules/MyToken.ts に保存します。
npm i -D @openzeppelin/contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.34;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply);
}
}
Hardhat Ignitionは、デプロイの手順を「モジュール」として宣言するしくみです。コンストラクタ引数は m.getParameter() で受け取ると、コードを書き換えずにネットワークごとに値を変えられます。
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
export default buildModule("MyTokenModule", (m) => {
const initialSupply = m.getParameter("initialSupply", 1_000_000n * 10n ** 18n);
const token = m.contract("MyToken", [initialSupply]);
return { token };
});
値を変えるときは、{"MyTokenModule": {"initialSupply": "500000000000000000000000n"}} のようなJSONファイルを作り、--parameters で渡します。BigIntは末尾に n を付けた数字の文字列で書き、Ignitionが読み込み時にBigIntへ戻します。手元の模擬チェーンで50万トークン分の値を渡すと、totalSupply() は 500000000000000000000000n を返しました。テンプレートの Counter 用モジュールのように、m.call() でデプロイ後の関数呼び出しまで手順に含めることもできます。
Sepoliaへのデプロイとソースコード検証
実行の前に、送信元アカウントにSepoliaのテストETH(フォーセットで入手)を入れておきます。Etherscanで検証する場合は、テンプレートの設定に無いAPIキーの項目を defineConfig に足し、値はkeystoreに入れます。
verify: {
etherscan: {
apiKey: configVariable("ETHERSCAN_API_KEY"),
},
},
準備ができたら、Sepoliaへは次のコマンドで出します。進捗は ignition/deployments/chain-11155111/ に記録されるので、途中でRPCが落ちても同じコマンドで続きから再開でき、完了済みの手順は二重に実行されません。
npx hardhat ignition deploy ignition/modules/MyToken.ts --network sepolia --verify
--verify を付けると、コンストラクタ引数も含めてデプロイ後の検証まで続けて行います。Ignitionを使わずにデプロイしたコントラクトは npx hardhat verify --network sepolia <アドレス> <コンストラクタ引数> のように、アドレスの後ろに引数を並べて検証します。引数が多い場合や配列を含む場合は、引数を書き出したモジュールを --constructor-args-path で渡します。対応先はEtherscan、Blockscout、Sourcifyの3つで、サブタスクを付けずに実行すると3つすべてに送ります。EtherscanはAPI v2に対応しているので、APIキー1つを対応チェーン間で共用でき、BlockscoutとSourcifyはキーが要りません。検証は既定で production プロファイルのビルドを使うため、別のプロファイルでデプロイした場合は --build-profile で合わせます。トークンの送金を署名1つで代行させる拡張はEIP-3009(ERC-3009)とは?transferWithAuthorizationの仕組みと署名実装・チェーン別対応表【2026年11月】で扱っています。
FoundryとHardhat 3の違いと選び方
ローカルでプロジェクトを管理する開発ツールとして、ここではHardhat 3とFoundry(2026年11月2日公開のv1.9.0が最新)を比べます。かつての定番だったTruffleとGanacheは、2023年9月にConsensysが開発終了を発表し、GitHubのリポジトリもアーカイブされています。保守されないため、新規の採用は勧めません。
| 観点 | Hardhat 3 | Foundry |
|---|---|---|
| 本体の言語 | TypeScript+Rust(EDR) | Rust |
| 導入 | npm | foundryupかバイナリ |
| Solidityテスト | forge-std互換 | forge test |
| TS/JSテスト | node:testかMocha | なし |
| デプロイ | Ignition(宣言型) | forge script |
| ローカルノード | hardhat node | anvil |
| OP系の模擬 | chainType: op | anvil --network optimism |
両者の差は、Solidityテストの互換によって小さくなっています。Hardhatのテンプレートで作った Counter.t.sol と上の Vault.t.sol(わざと落とすテストを含む)を、ソースを1行も変えずにFoundry 1.9.0の forge test で実行したところ、成功4件・失敗1件と同じ結果になりました。用意した設定は、foundry.toml でソースとテストの場所を contracts にしたことと、remappings.txt の2行で forge-std/ と hardhat/ の参照先を指定したことです。このときforgeはpragmaを満たすsolc 0.8.38を自動で選び、Hardhat側の設定(0.8.34)とは別の版でコンパイルしました。ガスやバイトコードを両者で比べるなら、foundry.toml の solc_version で版を揃えます。確認できたのはこの規模のSolidityテストまでで、TypeScriptテストやデプロイの手順は移せません。乗り換えるときは、チートコードの対応状況と設定ファイルの差を公式のFoundry互換性のドキュメントで確かめてください。
判断はチームが書く言語で決めます。フロントエンドやバックエンドをTypeScriptで書いていて、viemやethers.jsのコードをテストとデプロイにも使い回したいならHardhat 3です。逆に、コントラクトだけを書くチームで、Node.jsの依存(今回のテンプレートで139パッケージ)を持ち込みたくないならFoundryが合います。Hardhat 3を選ばないほうがよいのは、社内のCIやサーバーがNode.js 20以前に固定されていて更新できない場合です。v22.13.0未満では動かないので、先にNode.jsの更新計画を立ててください。
Hardhat 2から3への移行で詰まる箇所
公式の移行ガイドは、既存の設定を書き換えるのではなく、新しい設定ファイルを作って機能を1つずつ移す方法を勧めています。手順は、Node.jsの確認、npx hardhat clean、2系の依存(hardhat、@nomiclabs/ や @nomicfoundation/ で始まるもの、solidity-coverage と hardhat-gas-reporter)の削除、旧設定のリネーム、package.json への "type": "module" の追加という順です。実際に止まりやすいのは次の4点です。
- プラグインを入れたのに効かない:2系はimportするだけで有効になったが、3系は
plugins配列に入れないと何も起きない。 - カスタムタスクが動かない:
task("accounts", ..., async () => {})の書き方は使えず、setInlineAction()で処理を渡して.build()し、設定のtasks配列に登録する。 hre.networkがそのまま使えない:3系では接続を自分で作る。3.4.0(2026年4月16日)でnetwork.connect()が非推奨になり、3.22.0でも実行すると「hre.network.connect() is deprecated and will be removed in a future version」と警告が出る。network.create()かnetwork.getOrCreate()に置き換える。extendEnvironmentとextendConfigが無い:フックの仕組みに置き換わった。HREにフィールドを足したい場合は、プラグインとしてhreのcreatedフックで値を入れ、型拡張で型を宣言する形に書き直す。設定ファイルの中で直接HREを書き換えていた自作の拡張があるプロジェクトは、ここが移行の最大の作業になる。
3.3.0までは公式テンプレートのテストとスクリプト自体が network.connect() を使っていました。そのため、Hardhat 3を扱った記事でも3.4.0より前に書かれたものはこの書き方になっています。写したコードで非推奨の警告が出たら、network.create() に読み替えてください。
よくある質問
Hardhatとは何ですか?
Ethereumなど EVM 互換チェーンのスマートコントラクトを、コンパイル、テスト、デバッグ、デプロイするための開発環境です。Nomic Foundationが開発するオープンソースで、npmで配布されています。2026年11月13日時点の最新版はHardhat 3.22.0です。
Hardhatの読み方は?
「ハードハット」と読みます。英語の hard hat(工事用のヘルメット)と同じ綴りです。英語の検索結果では作業用ヘルメットの商品も混ざるので、開発ツールを探すときは「hardhat solidity」のように言語名を足すと絞り込めます。
Hardhat 2はいつまで使えますか?
Hegotaハードフォークのメインネット有効化か2027年6月1日のどちらか早いほうでサポートが終了し、以後はセキュリティ修正も出ません。それまではGlamsterdamハードフォークへの対応とバグ修正が続きます。2系の最新は2026年11月11日公開の2.29.2です。
HardhatとRemixはどう使い分けますか?
Remixはブラウザだけで動くIDEで、インストールなしで数十行のコントラクトを試すのに向きます。コードをGitで管理し、テストを自動で回し、CIからデプロイするようになったらHardhatかFoundryに移ります。
Hardhatの利用にお金はかかりますか?
Hardhat自体は無料のオープンソースです。費用がかかるのは外部サービスで、メインネットへのデプロイにはETHのガス代が必要です。SepoliaのテストETHは無料のフォーセットで手に入りますが、RPCプロバイダーやEtherscanのAPIは利用量によって有料プランが必要になります。