mcpField
H
hardhat

hardhat

github.com/nomicfoundation开发2122 次安装2 周前更新
代码仓库

关于 hardhat

hardhat 是面向 Hardhat 3 开发者的 Solidity 开发与测试技能,涵盖编译配置、测试组织和类型检查。它也说明如何仅在开发迭代中使用 slang-solx,并将工具箱专属内容交由配套技能。

主要功能

  1. 01区分 Solidity 与 TypeScript 测试
  2. 02说明网络连接与隔离状态
  3. 03使用 networkHelpers 操作 EVM 状态
  4. 04配置 slang-solx 开发编译配置
  5. 05编译后运行 TypeScript 类型检查

适用场景

  1. 01为合约选择合适的测试层
  2. 02通过测试辅助工具控制时间或区块状态
  3. 03针对慢速 via-IR 编译配置开发编译器
  4. 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)。养成这个习惯:它能比运行完整测试套件更快地发现许多错误。

评论

支持 Markdown