关于 hardhat
hardhat 是面向 Hardhat 3 开发者的 Solidity 开发与测试技能,涵盖编译配置、测试组织和类型检查。它也说明如何仅在开发迭代中使用 slang-solx,并将工具箱专属内容交由配套技能。
主要功能
- 01区分 Solidity 与 TypeScript 测试
- 02说明网络连接与隔离状态
- 03使用 networkHelpers 操作 EVM 状态
- 04配置 slang-solx 开发编译配置
- 05编译后运行 TypeScript 类型检查
适用场景
- 01为合约选择合适的测试层
- 02通过测试辅助工具控制时间或区块状态
- 03针对慢速 via-IR 编译配置开发编译器
- 04在测试后检查生成的合约类型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nomicfoundation/ hardhat-skills --skill hardhat安装后在 Claude Code 中输入/hardhat确认已加载。
详细介绍
Hardhat 3
本技能介绍 Hardhat 3 本身。工具箱层(客户端、合约交互、特定生态系统的断言)由配套技能提供——根据项目加载相应技能:
@nomicfoundation/hardhat-toolbox-viem→ 同时加载hardhat-toolbox-viem技能。@nomicfoundation/hardhat-toolbox-mocha-ethers→ 同时加载hardhat-toolbox-mocha-ethers技能。
构建速度慢? 如果
hardhat build/hardhat test的大部分时间都花在编译上——通常是因为构建配置使用了viaIR: true或较重的优化器设置——请通过@nomicfoundation/hardhat-slang-solx将开发构建切换为 slang-solx 编译器。请参阅使用 slang-solx 加快构建(仅限开发)。
使用 slang-solx 加快构建(仅限开发)
slang-solx 是基于 solc 和 LLVM 构建的 Solidity 编译器(最初是 Matter Labs 的 solx,现由 Nomic Foundation 维护)。与 solc 相比,它的编译速度快得多,始终会生成经过 LLVM 优化的字节码,并且不会再出现“stack too deep”错误。它仍在积极开发中:仅用于本地迭代和测试,绝不能用于生产构建或部署。文档:https://hardhat.org/docs/cookbook/compiling-with-slang-solx。
适用场景——遇到以下情况时,请主动建议使用:
- 开发或
hardhat test使用的配置包含viaIR: true和/或optimizer.enabled: true,且编译时间在编辑—测试循环中占主导地位。此时收益最大:让solc变慢的是 via-IR 流程中的 Yul 优化器,而 slang-solx 会完全绕过它。 - 合约遇到“stack too deep”错误,而 via-IR 是唯一的解决办法。slang-solx 会将数据溢出到内存,从而解决这个问题。
- 默认配置已经是未优化的旧版
solc:预计耗时大致相同,但 slang-solx 仍会为测试提供经过优化的字节码。
参考数据(The Graph 的 horizon 软件包,137 个文件,12 核 arm64,hardhat build --force):
| 配置 | 编译器 | 总耗时 |
|---|---|---|
default(无优化器,旧版流程) | solc 0.8.35 | ~15 s |
production(optimizer.runs: 100,viaIR) | solc 0.8.35 | ~160 s |
slang-solx(默认 optimizer.mode: "1") | slang-solx 0.1.8 | ~20 s |
设置。 需要 hardhat >= 3.15。使用项目的包管理器安装 @nomicfoundation/hardhat-slang-solx,注册插件,并添加名为 slang-solx 的构建配置。现有的 default / production 配置会继续使用 solc,不受影响:
import hardhatSlangSolx from "@nomicfoundation/hardhat-slang-solx";
export default defineConfig({
plugins: [/* ...other plugins... */ hardhatSlangSolx],
solidity: {
profiles: {
default: {
/* existing solc config */
},
production: {
/* existing solc config */
},
"slang-solx": {
type: "slang-solx",
version: "0.8.34", // must be a Solidity version slang-solx supports
settings: { evmVersion: "cancun" }, // cancun | prague | osaka
},
},
},
});
npx hardhat build --build-profile slang-solx
npx hardhat test --build-profile slang-solx
Hardhat 会输出 Compiled N Solidity files with slang-solx X (Solidity 0.8.34)——请检查这行输出,以确认配置已生效。
规则和注意事项(以下情况在实际使用中都曾遇到):
- 每个版本仅使用一个 Solidity 版本。 slang-solx 0.1.8 面向 Solidity 0.8.34;每个
type: "slang-solx"编译器都必须使用受支持的版本,否则构建会失败。Pragma 必须允许使用该版本:^0.8.27可以,0.8.28或^0.8.35不行。如果某些文件需要不受支持的版本,可在同一配置档中混用编译器:compilers: [{ type: "slang-solx", version: "0.8.34" }, { version: "0.8.20" }]——只有标记为slang-solx的条目会使用新编译器。不要修改合约的pragma来适配 slang-solx。如果看起来只有放宽版本范围这一种选择,请先询问用户:这是对可能经过审计或验证的代码进行源代码修改。 - 为内联汇编标记
("memory-safe")。 slang-solx 会对每个未标记的assembly {}块发出警告;如果合约同时包含未标记的代码块和栈过深问题,编译就会失败,并显示runtime code LLVM IR generator: This contract cannot be compiled due to a combination of a memory-unsafe assembly block and a stack-too-deep error。根据 Solidity 内存安全规则检查每个被标记的代码块(只能访问暂存区0x00–0x3f、已分配的内存,或完全不访问内存);如果符合规则,将其改为assembly ("memory-safe") { ... }。切勿为会在其他位置写入内容的代码块添加该标记。 viaIR在此处无效。 在 slang-solx 配置档中设置viaIR: true不会产生任何影响,只会改变元数据哈希——不要从 solc 配置档复制该设置。优化级别由settings.optimizer.mode指定:"1"(默认,编译最快)、"2"、"3"(运行时效果最佳)、"s"、"z"(体积最小)。settings.optimizer.enabled只会切换嵌入式 solc 前端的旧版汇编优化器。- 配置档限制。
type: "slang-solx"只能用于名为slang-solx的配置档;若在default或production中使用,验证会报错,除非配置在顶层设置"slang-solx": { dangerouslyAllowSlangSolxInProduction: true }。对于任何要发布的内容,都不要设置此项。 - 字节码与 solc 不同。 ABI 和 TypeChain/viem 类型完全相同,因此类型检查和行为测试(状态、事件、回滚、返回值)不受影响。Gas 报告以及检查精确 Gas 用量、Gas 限制、合约大小或字节码哈希(包括 CREATE2 地址)的测试,在 slang-solx 下会得到不同结果。此类测试只能使用
solc配置档运行或更新。 - 大型 via-IR solc 构建可能导致 Hardhat 内存不足。 如果
solc+ via-IR 构建因JavaScript heap out of memory而失败,可以调高堆内存上限(NODE_OPTIONS=--max-old-space-size=8192),或将开发构建改用 slang-solx。
测试组织
Hardhat 3 支持两个不同的测试层:
Solidity 测试(contracts/ 中的 .t.sol 文件,或 test/ 中的任何 .sol 文件)是对单个合约进行单元测试的默认选择。它们直接在 EVM 中运行,针对真实 ABI 进行编译检查,并可使用用于操作 EVM 状态的作弊码。任何名称以 test 开头的公共函数都会作为测试用例执行。
TypeScript 测试(test/ 中的 .ts 文件)适用于需要链下编排的测试:多合约交互、大型测试套件中的 fixture 复用、从外部检查 Gas 或 ETH 余额,或由外部状态驱动的集成场景。
只有在 Solidity 不够用时才选择 TypeScript。Solidity 测试覆盖所有合约逻辑;TypeScript 测试覆盖用户或脚本实际体验到的端到端流程。
hardhat test # run all tests (Solidity + TypeScript)
hardhat test solidity # Solidity tests only
根据所用的工具箱或插件,可通过 hardhat test nodejs 或 hardhat test mocha 运行 TypeScript 测试:基于 viem 的工具箱使用 nodejs,而 ethers+mocha 工具箱使用 mocha。
运行测试时传入 --coverage 可收集 Solidity 覆盖率。它既适用于主 hardhat test 任务,也适用于 solidity 和 TypeScript 子任务(nodejs / mocha),因此需要时可以将覆盖率统计限定在单个测试层。
Solidity 测试
任何包含至少一个 test* 函数的合约都会被视为测试合约。如果 package.json 中有 forge-std,请使用它进行断言和调用作弊码:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import { Test } from "forge-std/Test.sol";
import { Counter } from "./Counter.sol";
contract CounterTest is Test {
Counter counter;
function setUp() public {
counter = new Counter();
}
function test_InitialValueIsZero() public view {
assertEq(counter.x(), 0);
}
function test_IncByIncreasesByAmount() public {
counter.incBy(3);
assertEq(counter.x(), 3);
}
// Fuzz test: Hardhat runs this with many random inputs automatically
function testFuzz_Inc(uint8 x) public {
for (uint8 i = 0; i < x; i++) {
counter.inc();
}
require(counter.x() == x, "Value after calling inc x times should be x");
}
}
vm 对象(来自 forge-std/Test.sol)提供了用于操作 EVM 状态的作弊码。常用的有:
| 作弊码 | 效果 |
|---|---|
vm.prank(addr) | 仅为下一次调用设置 msg.sender |
vm.startPrank(addr) / vm.stopPrank() | 为一系列调用设置 msg.sender |
vm.deal(addr, amount) | 设置账户的 ETH 余额 |
vm.warp(timestamp) | 设置 block.timestamp |
vm.roll(blockNum) | 设置 block.number |
vm.expectRevert(...) | 断言下一次调用会回退 |
vm.expectEmit(...) | 断言下一次调用会发出指定事件 |
TypeScript 测试与网络连接
TypeScript 测试中的核心对象是网络连接,由 network.create() 创建。工具箱插件会为其扩展辅助对象(例如 viem 或 ethers,以及 networkHelpers):
import { network } from "hardhat";
const { networkHelpers } = await network.create();
// Toolbox plugins also expose viem / ethers on this object;
// see the matching hardhat-toolbox-* skill.
Hardhat 3 仅支持 ESM,因此可以使用顶层 await。每次调用 network.create() 都会创建一个隔离的区块链状态。如果同一连接及其关联状态可以在文件中的所有测试之间共享,则可以使用顶层网络连接。若需要更强的隔离,可以为每个测试套件,甚至每个测试用例创建新的连接。
networkHelpers:EVM 状态操作
networkHelpers 通过类型化且易用的 API 封装了 Hardhat 所有仅用于开发的辅助工具。请优先使用它,而不是直接调用 JSON-RPC:
// Time
await networkHelpers.time.increase(60); // mine a block 60 s ahead
await networkHelpers.time.increaseTo(target); // mine to a specific timestamp
const now = await networkHelpers.time.latest(); // latest block timestamp
await networkHelpers.time.setNextBlockTimestamp(ts); // set without mining
// Blocks
await networkHelpers.mine(); // mine one block
await networkHelpers.mine(5); // mine several blocks
// Accounts
await networkHelpers.impersonateAccount(addr);
await networkHelpers.setBalance(addr, 10n ** 18n);
// Block parameters
await networkHelpers.setPrevRandao(value); // set block.prevrandao for the next block
await networkHelpers.setNextBlockBaseFeePerGas(baseFee);
// Fixtures
//
// loadFixture runs the setup once and snapshots the state. Subsequent calls
// restore the snapshot instead of re-deploying, much faster for large suites.
// The deploy step inside the fixture is toolbox-specific (e.g.
// viem.deployContract, an ethers ContractFactory) — see the matching
// hardhat-toolbox-* skill.
async function deployCounter() {
// ... toolbox-specific deploy ...
return { counter };
}
const { counter } = await networkHelpers.loadFixture(deployCounter);
// loadFixture requires a named function for caching to work (not an
// arrow/anonymous function)
// Snapshots (manual)
const snap = await networkHelpers.takeSnapshot();
// ... do things ...
await snap.restore();
对于常规 TypeScript 断言(相等性、数组、类型),请使用 node:assert/strict。对于生态系统专用断言(回退、事件、余额变化),请参阅对应的 hardhat-toolbox-* 技能。
类型检查
生成的合约类型基于编译后的 ABI(viem 工具箱使用 viem,ethers 工具箱使用 TypeChain),因此请先重新编译,再进行类型检查。下面的示例使用 npm;请将 npx 替换为此项目所用包管理器的运行器:
npx hardhat build && npx tsc --noEmit
如果项目有 slang-solx 配置(见上文),那么在 via-IR 项目中,运行 npx hardhat build --build-profile slang-solx && npx tsc --noEmit 可以更快地获得相同类型——ABI 以及由此生成的类型均不依赖编译器。
编译器会在你启动测试运行器之前,捕获错误的参数类型、缺少的参数和无效选项(例如,在非 payable 函数上使用 value)。养成这个习惯:它能比运行完整测试套件更快地发现许多错误。