标签

Hardhat

Hardhat 是一个专为以太坊开发者设计的以太坊开发环境。它主要用于智能合约的开发、编译、部署和测试。Hardhat 提供了一个本地以太坊网络,开发者可以在这个网络上部署和测试他们的智能合约,而无需在真实的以太坊网络上进行,这大大降低了开发成本并加速了开发过程。

Hardhat
查看更多相关内容
服务端5月28日 05:35
Hardhat 智能合约 Gas 优化有哪些核心方法?Gas 费是以太坊开发中绕不开的成本问题。一次简单的 ERC-20 转账大约需要 51000 Gas,而一个复杂的 DeFi 交互可能消耗 20 万以上。在 Hardhat 开发流程中,从测量到优化 Gas,有一套成熟的工具链和实践方法,涵盖了编译器配置、Solidity 编码技巧和存储结构设计三个层面。 ## 用 Gas Reporter 量化消耗 不知道哪里费 Gas,优化就无从谈起。hardhat-gas-reporter 是 Hardhat 生态中最常用的 Gas 测量工具,它会在测试运行时自动生成每个合约函数的 Gas 消耗报告。 安装插件: ```bash npm install --save-dev hardhat-gas-reporter ``` 在 `hardhat.config.js` 中配置: ```javascript require("hardhat-gas-reporter"); module.exports = { gasReporter: { enabled: true, currency: "USD", gasPrice: 20, coinmarketcap: "YOUR_API_KEY" // 可选,获取实时 ETH/USD 汇率 } }; ``` 运行测试即可看到每个合约方法的 Gas 消耗和部署成本: ```bash npx hardhat test ``` 输出表格会显示合约部署 Gas、每个函数调用的平均 Gas,以及方法级别的对比,方便快速定位高消耗函数。如果只想在 CI 环境中启用,可以用环境变量控制: ```javascript gasReporter: { enabled: process.env.REPORT_GAS === "true" } ``` 除了 gas-reporter,Hardhat 还支持 Hardhat Toolbox 集成的 gas 测量,以及第三方的 eth-gas-reporter,功能类似但报告格式和集成方式各有侧重。 ## 启用 Solidity 编译器优化 编译器自带的优化器是最直接的 Gas 优化手段,不需要改动任何业务代码。它通过消除死代码、简化表达式、内联小函数等方式压缩字节码体积和执行路径。 ```javascript solidity: { version: "0.8.20", settings: { optimizer: { enabled: true, runs: 200 } } } ``` `runs` 参数决定编译器的优化方向,理解它的含义至关重要: - **runs 值大(如 800-1000)**:编译器优化运行时 Gas 消耗,部署字节码更大、部署 Gas 更高,但每次函数调用更省。适合频繁调用的合约(DEX、借贷协议)。 - **runs 值小(如 50-100)**:编译器优化部署成本,字节码更紧凑但运行效率较低。适合部署后调用有限的合约(投票、一次性初始化合约)。 - **runs = 200**:较平衡的默认值,大多数场景适用。 另外,Solidity 0.8.20+ 引入了 `viaIR` 编译管线,对复杂合约的优化效果更好: ```javascript settings: { optimizer: { enabled: true, runs: 200 }, viaIR: true } ``` ## 存储槽打包:收益最高的优化 EVM 的存储以 256 位(32 字节)为基本单位,每次 SSTORE 操作花费 20000 Gas(从零写非零)或 5000 Gas(修改已有值)。多个小类型变量如果能放进同一个 slot,就能省掉大量存储开销。 **变量声明顺序决定了打包效果**,Solidity 按声明顺序分配 slot: ```solidity // 差:占用 3 个 slot contract BadLayout { uint64 a; // slot 0(只用 64 位,剩余 192 位浪费) uint256 b; // slot 1(256 位独占,打断打包) uint64 c; // slot 2(新的 slot) } // 好:占用 2 个 slot contract GoodLayout { uint64 a; // slot 0 前 64 位 uint64 c; // slot 0 后 64 位(和 a 共享 slot) uint256 b; // slot 1(独占) } ``` 这个例子中,`GoodLayout` 省了一个 slot,每次读写 a 和 c 都省了一次 SLOAD/SSTORE(至少 2100 Gas)。 几个存储层面的关键原则: - **`mapping` 替代 `array`**:mapping 的读写是 O(1),而 array 遍历是 O(n),且 length 本身占一个 slot - **`delete` 清零退还 Gas**:SSTORE 从非零写零会退还 4800 Gas,不再需要的状态变量及时清零 - **`constant` 和 `immutable`**:constant 在编译期直接替换为字面量,immutable 在部署时写入字节码,都不占 storage slot ## calldata vs memory:省掉一次拷贝 外部函数的引用类型参数默认用 `memory`,这会触发从 calldata 到 memory 的拷贝。如果函数内不需要修改这个参数,改用 `calldata` 可以直接从交易输入中读取,省掉拷贝开销。 ```solidity // 多一次 memory 拷贝 function process(uint[] memory data) external { uint total = 0; for (uint i = 0; i < data.length; i++) { total += data[i]; } } // 直接读 calldata,省 Gas function process(uint[] calldata data) external { uint total = 0; for (uint i = 0; i < data.length; i++) { total += data[i]; } } ``` 对于大数组,这个差异尤其明显——数组越大,省下的拷贝 Gas 越多。 ## 自定义 Error 替代 require 字符串 Solidity 0.8.4 引入的自定义 Error 比 `require("reason string")` 在部署和运行时都更省 Gas。原因很简单:字符串需要 ABI 编码后存入字节码,而 Error 只占 4 字节的函数选择器。 ```solidity // 部署时字符串写入 bytecode,每个字符约 1 字节 require(balance >= amount, "Insufficient balance for transfer"); // Error 只有 4 字节 selector,参数按 ABI 编码 error InsufficientBalance(uint256 available, uint256 required); if (balance < amount) revert InsufficientBalance(balance, amount); ``` 自定义 Error 的另一个好处是可以携带结构化数据,方便链下解析和前端展示错误详情。 ## 短路求值与批量操作 Solidity 的 `&&` 和 `||` 从左到右求值,一旦能确定结果就停止。把更可能短路或更便宜的条件放前面: ```solidity // isPaused 大多为 false,放前面可以跳过昂贵的 _balance 读取 require(!isPaused && _balance >= _amount, "Check failed"); ``` 批量操作减少交易次数同样重要。每笔以太坊交易有 21000 Gas 的基础成本,多次调用意味着多次基础费用: ```solidity // 单个铸造:每次调用付一次基础费用 function mintOne(uint256 id) external { ... } // 批量铸造:一次交易处理多个,分摊基础费用 function mintBatch(uint256[] calldata ids) external { ... } ``` ERC-721 的 `mintBatch` 相比逐个 `mint`,在处理 10 个 token 时可以节省约 30%-40% 的总 Gas。 ## 用测试断言防止 Gas 回归 优化前后必须有数据对比。在 Hardhat 测试中直接读取交易回执的 `gasUsed`: ```javascript it("mint 操作 Gas 应低于 100000", async function () { const tx = await contract.mint(owner.address, 1); const receipt = await tx.wait(); console.log("Gas used:", receipt.gasUsed.toString()); expect(receipt.gasUsed).to.be.below(100000); }); ``` 也可以用 `estimateGas` 在不实际发送交易的情况下预估: ```javascript const estimatedGas = await contract.mint.estimateGas(owner.address, 1); console.log("Estimated gas:", estimatedGas.toString()); ``` 把 Gas 上限断言写进 CI 测试,一旦某次提交让 Gas 异常升高,测试自动失败。这比人工对比 gas-reporter 输出可靠得多。 ## Hardhat Console 快速调试 开发阶段不需要写完整测试,用 Hardhat Console 可以快速验证优化效果: ```bash npx hardhat console --network localhost ``` ```javascript const Contract = await ethers.getContractFactory("MyToken"); const contract = await Contract.deploy(); const tx = await contract.transfer(addr1.address, 100); const receipt = await tx.wait(); console.log("Transfer gas:", receipt.gasUsed.toString()); ``` 这种方式适合快速验证某个优化思路,确认有效后再补上正式的测试用例和断言。 --- Gas 优化不是一次性的工作,而是开发流程中的持续环节。从 hardhat-gas-reporter 建立基线,用编译器优化拿到低成本收益,再通过存储槽打包、calldata、自定义 Error 等手段逐步压缩运行时 Gas,最后用测试断言防止回归。这套流程在 Hardhat 项目中已经验证过无数次。核心原则就一条:先测量,再优化,别靠猜。
服务端5月28日 05:33
如何在 Hardhat 中将智能合约部署到多个网络?在以太坊开发中,一份智能合约往往需要先部署到本地网络调试,再发布到测试网验证,最后才上主网。Hardhat 提供了灵活的多网络部署机制,让你用同一套代码在不同链上完成发布和验证。 ## 网络配置:hardhat.config 文件 部署的第一步是在配置文件中声明目标网络。在 `hardhat.config.js`(或 `.ts`)中添加 `networks` 字段: ```javascript require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config(); module.exports = { solidity: "0.8.24", networks: { hardhat: { // 内置本地网络,无需额外配置 }, sepolia: { url: process.env.SEPOLIA_RPC_URL, accounts: [process.env.PRIVATE_KEY], chainId: 11155111, }, baseSepolia: { url: process.env.BASE_SEPOLIA_RPC_URL, accounts: [process.env.PRIVATE_KEY], chainId: 84532, }, mainnet: { url: process.env.MAINNET_RPC_URL, accounts: [process.env.PRIVATE_KEY], chainId: 1, }, }, }; ``` RPC URL 和私钥通过 `.env` 文件管理,不要硬编码到代码中: ```bash # .env SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY PRIVATE_KEY=your_private_key_here ETHERSCAN_API_KEY=your_etherscan_key ``` `.env` 文件务必加入 `.gitignore`,避免私钥泄露。 ## 编写部署脚本 在 `scripts/` 目录下创建部署脚本,使用 ethers.js 完成合约的部署和可选的验证: ```javascript const hre = require("hardhat"); async function main() { const Contract = await hre.ethers.getContractFactory("MyContract"); const contract = await Contract.deploy(); await contract.deployed(); console.log("Deployed to:", contract.address); // 仅在真实网络上验证合约 if (hre.network.name !== "hardhat" && hre.network.name !== "localhost") { console.log("Waiting for confirmations..."); await contract.deployTransaction.wait(6); try { await hre.run("verify:verify", { address: contract.address, constructorArguments: [], }); console.log("Contract verified on Etherscan"); } catch (e) { console.log("Verification failed:", e.message); } } } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` 脚本逻辑很直观:先部署,再判断是否在真实网络上,如果是就等待 6 个区块确认后提交验证请求。验证失败不会中断流程,只会打印警告。 ## 执行部署命令 部署时通过 `--network` 参数指定目标网络: ```bash # 本地测试 npx hardhat run scripts/deploy.js # Sepolia 测试网 npx hardhat run scripts/deploy.js --network sepolia # Base Sepolia 测试网 npx hardhat run scripts/deploy.js --network baseSepolia # 以太坊主网 npx hardhat run scripts/deploy.js --network mainnet ``` 每次部署后,记录合约地址和构造函数参数。你可以在项目里维护一个 `deployments.json` 文件跟踪各网络的部署记录: ```json { "sepolia": { "MyContract": "0x1234...abcd", "deployedAt": "2026-05-28" }, "mainnet": { "MyContract": "0x5678...efgh", "deployedAt": "2026-06-01" } } ``` ## 使用 Hardhat Ignition(推荐方式) Hardhat Ignition 是官方推荐的声明式部署方案,比脚本方式更可靠。它支持断点续部署、自动重试和并行执行。 在 `ignition/modules/` 下创建部署模块: ```javascript const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); module.exports = buildModule("MyContractModule", (m) => { const contract = m.contract("MyContract"); return { contract }; }); ``` 如果合约构造函数需要参数,直接传入: ```javascript module.exports = buildModule("TokenModule", (m) => { const initialSupply = m.getParameter("initialSupply", 1000000); const token = m.contract("MyToken", [initialSupply]); return { token }; }); ``` 部署时加上 `--verify` 可以一步完成部署和验证: ```bash # 部署到 Sepolia 并自动验证 npx hardhat ignition deploy ignition/modules/MyContractModule.js --network sepolia --verify # 部署到主网 npx hardhat ignition deploy ignition/modules/MyContractModule.js --network mainnet ``` Ignition 会将部署状态保存在 `ignition/deployments/chain-{chainId}/` 目录下,即使中途中断也能从上次的位置继续,不会重复执行已完成的步骤。 ## 配置 Etherscan 验证插件 合约验证需要安装并配置 `@nomicfoundation/hardhat-verify` 插件: ```javascript // hardhat.config.js require("@nomicfoundation/hardhat-verify"); module.exports = { // ... networks 配置 etherscan: { apiKey: { sepolia: process.env.ETHERSCAN_API_KEY, mainnet: process.env.ETHERSCAN_API_KEY, }, }, }; ``` 如果部署时忘记加 `--verify`,也可以事后手动验证: ```bash npx hardhat verify --network sepolia <CONTRACT_ADDRESS> <CONSTRUCTOR_ARGS> ``` 验证成功后,任何人都能在 Etherscan 上直接读取和交互你的合约源码。 ## 多网络部署的常见问题 **部署时 nonce 冲突怎么办?** 这通常是因为本地节点与远程网络状态不同步。检查 RPC 节点是否正常,或者在 MetaMask 中重置账户交易历史。 **测试网 ETH 从哪里获取?** 使用 Chainlink Faucet 或 Alchemy Faucet,每个钱包每天可以领取一定量的 Sepolia ETH。 **主网部署有哪些注意事项?** 务必使用多签钱包(如 Gnosis Safe)管理私钥,设置合理的 gas 上限避免过高手续费,部署前在测试网上完整走一遍流程。建议在主网部署时使用 `maxFeePerGas` 限制最高 gas 价格: ```javascript mainnet: { url: process.env.MAINNET_RPC_URL, accounts: [process.env.PRIVATE_KEY], chainId: 1, gasPrice: 20000000000, // 20 Gwei } ``` **Ignition 部署中断了怎么恢复?** 直接重新运行同一命令即可。Ignition 的 journal 机制会记录每一步执行状态,已完成的步骤不会重复执行。 掌握 Hardhat 的多网络部署流程后,你可以高效地在本地、测试网和主网之间切换发布合约,结合 Ignition 的声明式部署和自动验证功能,让整个发布流程更加可靠和可重复。
服务端5月28日 05:32
Hardhat Ignition 是什么?声明式部署智能合约实战指南部署智能合约是 Web3 开发里最让人头大的环节之一——手动跑脚本、记地址、处理依赖、一旦中断就得从头来。Hardhat Ignition 就是来解决这些问题的:它用声明式的方式定义部署逻辑,自动管理状态和依赖,部署中断了能接着跑,不用推倒重来。 ## 为什么需要 Hardhat Ignition 传统的部署方式是写一个 JavaScript 脚本,按顺序调用合约的 deploy 方法。问题很明显: - **不可恢复**:脚本跑到一半失败,已部署的合约地址可能丢,重来一遍又浪费 gas - **依赖混乱**:合约 B 依赖合约 A 的地址,手动传递容易出错 - **无法并行**:多个互不依赖的合约只能串行部署,浪费时间 Hardhat Ignition 的思路完全不同——你只声明"我要部署什么",执行顺序、并行优化、状态管理全部交给 Ignition 处理。这跟 Terraform 管理基础设施的理念很像:描述期望状态,而不是写操作步骤。 和社区插件 hardhat-deploy 相比,Ignition 是 Nomic Foundation(Hardhat 团队)的官方方案,在 Hardhat 3 中已默认集成。hardhat-deploy 已经进入维护模式,官方也提供了迁移指南,新项目建议直接用 Ignition。 ## 安装和配置 在已有的 Hardhat 项目中安装 Ignition 插件: ```bash # 使用 viem(推荐,Hardhat 3 默认) npm add --save-dev @nomicfoundation/hardhat-ignition-viem # 或使用 ethers npm add --save-dev @nomicfoundation/hardhat-ignition-ethers ``` 然后在 `hardhat.config.ts` 中引入: ```typescript import { defineConfig } from "hardhat/config"; import hardhatIgnitionViemPlugin from "@nomicfoundation/hardhat-ignition-viem"; export default defineConfig({ plugins: [hardhatIgnitionViemPlugin], solidity: "0.8.28", }); ``` 注意一个常见坑:ethers 和 viem 插件**不能同时安装**,否则会报 `HHE10119` 错误。选一个就行。 ## 创建第一个 Ignition 模块 Ignition 的核心概念是**模块(Module)**——一个模块定义一组合约实例和操作,类似一个部署蓝图。 先建好目录结构: ```bash mkdir -p ignition/modules ``` 写一个最简单的模块,部署一个 ERC20 代币: ```javascript import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("TokenModule", (m) => { const token = m.contract("MyToken", ["MyToken", "MTK", 18]); return { token }; }); ``` `buildModule` 接收模块名和一个回调函数,回调里的 `m` 就是模块上下文。`m.contract()` 声明要部署的合约,第二个参数是构造函数参数。`return` 出去的对象可以在其他模块中引用。 ## 参数化部署 硬编码参数不灵活,用 `m.getParameter()` 让部署时可配置: ```javascript export default buildModule("TokenModule", (m) => { const name = m.getParameter("name", "MyToken"); const symbol = m.getParameter("symbol", "MTK"); const token = m.contract("MyToken", [name, symbol, 18]); return { token }; }); ``` 第二个参数是默认值。部署时通过 `--parameters` 覆盖: ```bash npx hardhat ignition deploy ignition/modules/TokenModule.js --parameters name:CustomToken,symbol:CTK ``` ## 多合约依赖和交互 真实项目中合约之间往往有依赖关系。比如一个代币销售合约需要引用代币合约的地址: ```javascript export default buildModule("DAppModule", (m) => { const token = m.contract("MyToken", ["MyToken", "MTK", 18]); const sale = m.contract("TokenSale", [token]); // 部署后调用 token 合约的方法 m.call(token, "transferOwnership", [sale]); return { token, sale }; }); ``` Ignition 会自动分析依赖关系——`TokenSale` 的构造函数需要 `token`,所以 Ignition 保证先部署 MyToken,拿到地址后再部署 TokenSale。`m.call()` 会在两个合约都部署完成后执行。 如果多个合约互不依赖,Ignition 会并行部署,省时间。 ## 引用已部署的合约 有时你需要跟链上已经存在的合约交互,不需要重新部署: ```javascript export default buildModule("InteractModule", (m) => { const existingToken = m.contractAt("MyToken", "0x1234..."); const sale = m.contract("TokenSale", [existingToken]); return { sale }; }); ``` `m.contractAt()` 创建一个指向已有合约的引用,后续可以传给其他合约的构造函数或 `m.call()`。 ## 执行部署 ```bash # 部署到本地 Hardhat 网络 npx hardhat ignition deploy ignition/modules/TokenModule.js # 部署到测试网 npx hardhat ignition deploy ignition/modules/TokenModule.js --network sepolia # 部署并验证合约源码 npx hardhat ignition deploy ignition/modules/TokenModule.js --verify ``` 部署前可以先预览执行计划: ```bash npx hardhat ignition plan ignition/modules/TokenModule.js ``` Ignition 会列出所有将要执行的步骤及其依赖关系,确认无误再部署。 部署结果默认保存在 `ignition/deployments/` 目录下,包含合约地址、ABI 和交易哈希。 ## 增量部署和错误恢复 这是 Ignition 最实用的特性。假设你有一个包含 5 个合约的模块,部署到第 3 个时网络超时了: - 传统脚本:要么从头跑(前两个合约重新部署,浪费 gas),要么手动记录已部署的地址然后改脚本跳过 - Ignition:直接重新运行同一条命令,Ignition 检测到前两个合约已部署,跳过它们,从第 3 个继续 这个特性基于 Ignition 的状态追踪机制——每次成功执行一个 Future(部署合约、调用方法都是一个 Future),状态就会被持久化。中断后重启,Ignition 读取状态跳过已完成的部分。 **注意**:部署 ID 只能包含字母数字、短横线和下划线,否则会报 `HHE10108` 错误。 ## 常见踩坑和排错 **"Unrecognized task 'ignition'"** 说明 Ignition 插件没正确加载。检查 `hardhat.config.ts` 是否 import 了插件,`node_modules` 里是否安装了对应包。 **验证失败** 部署时加 `--verify` 需要配置 Etherscan API key。在 `hardhat.config.ts` 中添加: ```javascript etherscan: { apiKey: "YOUR_ETHERSCAN_API_KEY" } ``` **合约找不到** 先确认合约已编译(`npx hardhat compile`),如果修改了合约内容但部署结果没变,运行 `npx hardhat clean` 清除缓存后重试。 **链 ID 变更** Ignition 记录了每个部署对应的链 ID。如果你把同一个部署目录指向了不同的链,会报 `HHE10900` 错误。解决方案是删除 `ignition/deployments/` 下对应的部署记录,或者指定不同的部署 ID。 ## 从 hardhat-deploy 迁移 如果你之前用的是 hardhat-deploy,迁移步骤: 1. 卸载 `hardhat-deploy` 包 2. 安装 `@nomicfoundation/hardhat-ignition-ethers` 或 `viem` 版本 3. 把 `deploy/` 目录下的部署脚本改写为 Ignition 模块格式(`buildModule` + `m.contract`) 4. 更新 `hardhat.config.ts` 中的插件引用 核心改动是把命令式的部署脚本(`deploy()` 函数)改成声明式的模块定义。逻辑上等价,但 Ignition 版本由框架管理执行顺序和状态。 ## 什么时候该用 Ignition - 多合约系统,合约间有依赖关系 → 用 Ignition,自动管理部署顺序 - 需要在多个网络(本地/测试网/主网)反复部署 → 用 Ignition,增量部署省 gas - 只部署一个简单合约 → 传统脚本也够用,但 Ignition 也不复杂,用起来一样简单 - 团队协作 → Ignition 的声明式模块比脚本更好维护,部署逻辑不会因为谁改了脚本就出问题
服务端5月28日 05:32
Hardhat、Truffle 和 Remix 有什么区别?以太坊开发框架怎么选?选 Hardhat。2023 年底 Consensys 已经把 Truffle 和 Ganache 关了,GitHub 仓库归档,官方推荐迁移到 Hardhat。所以现在这个问题的答案比以前简单多了——Truffle 已经退出历史舞台,实际选择只在 Hardhat 和 Remix(以及新晋的 Foundry)之间。 ## Hardhat:生产项目的事实标准 Hardhat 是目前以太坊开发用得最多的框架,OpenZeppelin、Aave、1inch 这些项目都在用。 核心优势: - **Solidity 调试体验最好**——交易失败直接给堆栈跟踪和错误消息,不用像 Truffle 时代那样对着 revert 干瞪眼 - **内置 Hardhat Network**——本地区块链,支持即时挖矿和 `console.log`,测试跑得快 - **TypeScript 原生支持**——配置文件、测试脚本都能用 TS 写,类型安全 - **插件生态丰富**——coverage、gas reporter、verify 等功能都是插件按需装,不像 Truffle 全塞一块 踩坑点: - 配置项多,新手上手要花点时间搞懂 `hardhat.config.ts` 的各种字段 - 纯 JS/TS 技术栈,如果你的团队更熟悉 Solidity 原生开发,Foundry 可能更顺手 ## Truffle:已经退役 Truffle 曾经是以太坊开发框架的老大哥,2015 年发布,2020 年被 Consensys 收购时覆盖了 130 万开发者。但 2023 年 9 月 Consensys 宣布停运,2024 年 2 月 GitHub 仓库正式归档。 死因很简单:维护成本高、代码老旧、开发体验被 Hardhat 甩开。Consensys 自己都选了 Hardhat 作为官方推荐迁移目标,附带了完整的迁移指南。 如果你的老项目还在用 Truffle:赶紧迁。Truffle 不再接收 bugfix,安全问题不会修,依赖它的工具链迟早出事。 ## Remix:快速验证和学习用 Remix 是浏览器里的 IDE,不用装任何东西,打开 remix.ethereum.org 就能写合约、编译、部署、调试,一条龙。 适合的场景: - 学 Solidity 的第一步,不用折腾环境 - 快速验证一个合约逻辑对不对 - 参加 hackathon 需要极速出原型 不适合的场景: - 正式项目开发——没有版本控制、没有自动化测试流程、插件扩展能力有限 - 团队协作——项目没法用 Git 管理,代码审查流程缺失 ## 2025 年还要考虑 Foundry 三框架对比是老问题了。现在面试官更可能追问的是:Foundry 和 Hardhat 怎么选? Foundry 用 Rust 写的,测试跑得飞快,测试脚本直接写 Solidity,不用切语言。做 DeFi 协议、需要 fuzzing 和不变量测试的团队越来越多选 Foundry。 简单判断:团队主力是 JS/TS 全栈 → Hardhat;团队主力写 Solidity、追求极致测试速度 → Foundry。不少团队两个都装,Hardhat 处理部署和前端交互,Foundry 跑合约测试。 ## 追问 ### Truffle 项目怎么迁移到 Hardhat? Consensys 出了官方迁移指南。主要步骤:用 `hardhat-init` 脚手架建项目 → 把合约移到 `contracts/` → 测试从 Mocha 改成 Hardhat 的测试格式 → 迁移脚本从 Truffle 的 `migrations/` 改成 Hardhat 的 `scripts/` → 用 Hardhat verify 插件替代 Truffle 的验证流程。配置语法差异是最大的坑,建议对照官方文档逐项改。 ### Hardhat 和 Foundry 有什么区别? | 维度 | Hardhat | Foundry | |------|---------|----------| | 语言 | JS/TS | Rust + Solidity | | 测试速度 | 中等 | 极快 | | 测试语言 | JS/TS | Solidity | | Fuzzing | 需插件 | 内置 | | 生态成熟度 | 高 | 快速增长中 | 选 Hardhat 的理由:JS 生态、插件丰富、团队好上手。选 Foundry 的理由:Solidity 原生、测试快、fuzzing 强。 ### Remix 能用在生产环境吗? 不推荐。Remix 缺少版本控制集成、自动化 CI/CD 流程、团队协作工具链。它更适合学习和快速原型。如果只是部署一个简单合约到主网,可以用 Remix,但正式项目还是用 Hardhat 或 Foundry。 ### 三个框架的 gas 优化能力如何? Hardhat 有 `hardhat-gas-reporter` 插件,能看到每个函数的 gas 消耗。Foundry 内置 gas snapshot 功能,更细粒度。Remix 的 gas 分析比较基础,只能看到交易级别的 gas 用量。做 gas 优化优先选 Hardhat 或 Foundry。
服务端5月28日 05:31
Hardhat 调试 Solidity 合约的核心方法有哪些?Hardhat 是 Solidity 开发中调试体验最好的框架,没有之一。它的核心调试能力有四个:console.log 在合约内部打印变量值、Solidity Stack Traces 自动还原交易失败的完整调用链、Hardhat Network 的状态快照与挖矿控制、以及 gas reporter 定量分析每笔交易的开销。日常开发中前两个用得最频繁——一个让你"看见"运行时状态,一个帮你"定位"崩溃位置。 ## 追问 ### console.log 和事件(event)都能输出信息,调试时该用哪个? 调试用 console.log,生产用 event。console.log 只在 Hardhat Network 生效,部署到主网或测试网后是空操作(no-op),不消耗 gas 也不留链上记录。event 会永久写入交易日志,适合需要链下索引或监听的场景。简单说:console.log 是临时诊断工具,event 是产品功能的一部分。两者不冲突,但用途完全不同。 ### 遇到 `Transaction reverted` 没有任何错误信息,Hardhat 能帮上什么忙? 这是 Hardhat Network 最核心的调试优势。普通节点只会告诉你交易回滚了,Hardhat Network 会自动生成 Solidity Stack Trace——展示完整的调用链:从 JS/TS 测试代码进入,经过哪些合约的哪些函数,在哪个具体行号失败,逐层展开。输出类似: ``` Error: VM Exception while processing transaction: reverted with reason string "Insufficient balance" at Token.transfer (contracts/Token.sol:45) at TokenRouter.batchTransfer (contracts/TokenRouter.sol:22) ``` 如果连 reason string 都没有,可能是除零、数组越界或调用了不存在的函数选择器,Hardhat 也会为这些场景生成专门的错误描述。 ### console.log 支持哪些数据类型?数组和结构体怎么处理? 支持 uint、int、string、bool、address、bytes1-32,最多同时传 4 个参数。数组和结构体不直接支持——数组需要循环打印每个元素,结构体逐字段输出。格式化语法跟 Node.js 的 `util.format` 一致,用占位符: ```solidity console.log("Sender %s transferred %d tokens", msg.sender, amount); ``` 一个容易踩的坑:console.log 可以在 `view` 和 `pure` 函数里使用,这在调试只读方法时非常方便。 ### 状态快照(evm_snapshot / evm_revert)什么时候用? 测试套件里做状态隔离,避免每个测试用例都重新部署合约。流程:`beforeEach` 里部署完合约后调 `evm_snapshot` 保存状态,每个测试结束后 `evm_revert` 回到快照点。对部署耗时的大合约,能显著缩短测试时间。注意 `evm_revert` 后快照本身也被销毁,需要重新 `evm_snapshot`。 ### gas reporter 怎么用?能发现哪些问题? 安装 `hardhat-gas-reporter` 插件,`hardhat.config.ts` 里引入后正常运行 `npx hardhat test` 即可。测试结束后输出每个函数的 gas 消耗表。它帮你发现:哪个函数异常耗 gas(通常是循环写存储或 SSTORE 操作过多)、同一个逻辑的两种实现哪个更省、以及优化前后对比量化。只对 Hardhat Network 有效,不影响实际部署。 ## 写段代码 ```solidity import "hardhat/console.sol"; contract DebugExample { mapping(address => uint256) public balances; function transfer(address to, uint256 amount) external { console.log("From:", msg.sender, "To:", to, "Amount:", amount); require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; balances[to] += amount; } } ```
服务端5月28日 05:30
Hardhat 3 实战入门:以太坊智能合约开发环境搭建与核心功能## Hardhat 是什么? Hardhat 是以太坊智能合约开发的事实标准工具链之一,由 Nomic Foundation 维护。它把编译、测试、部署、调试这些每天要重复几十遍的操作串成一条流水线,让你专注于写合约本身。 2026 年发布的 Hardhat 3 是一次大版本重构——底层模拟器从 JavaScript 重写为 Rust(EDR),编译和测试速度提升了 2-5 倍,同时新增了 Solidity 测试和 OP Stack 本地模拟。如果你还在用 Hardhat 2,升级的体感差异很明显。 ## 核心功能拆解 ### 本地开发网络(Hardhat Network) Hardhat Network 是一个跑在本地的以太坊模拟器,每次运行测试时自动启动。你不需要连真实网络,不需要付费 Gas,合约部署和调用都是即时的。 实际开发中最有用的几个能力: - **即时挖矿**:每笔交易立刻出块,不用等出块时间 - **账户自动解锁**:内置 20 个预充值账户,直接拿来部署和交互 - **快照与回滚**:`evm_snapshot` / `evm_revert` 让测试之间互不干扰,一个 `beforeEach` 回滚就能重置状态 - **Hardhat 3 新增**:OP Stack 网络本地模拟,部署到 Optimism 的合约可以本地跑通完整流程 ```javascript // hardhat.config.js - 配置本地网络 module.exports = { solidity: "0.8.28", networks: { hardhat: { chainId: 31337, // Hardhat 3: 模拟 OP Stack 网络 opStack: true } } }; ``` ### 智能合约编译 Hardhat 的编译模块处理了大部分你不想手动管的事: - **多版本 Solidity 共存**:一个项目里可以同时用 0.6.x 和 0.8.x 的合约,Hardhat 按版本分别编译 - **依赖自动解析**:`node_modules` 里的 `@openzeppelin/contracts` 之类的库,直接 `import` 就行 - **TypeChain 集成**:编译后自动生成 TypeScript 类型绑定,合约交互时有完整的类型提示和自动补全 ```bash npx hardhat compile # 输出:Compiled 3 Solidity files successfully ``` 编译产物放在 `artifacts/` 目录,包含 ABI 和 bytecode。配合 `@nomicfoundation/hardhat-toolbox` 插件,TypeChain 绑定会自动生成到 `typechain-types/`。 ### 测试框架 Hardhat 内置的测试框架基于 Mocha + Chai + ethers.js,写测试的体验和写前端测试差不多。 **Hardhat 3 的重大变化**:除了 TypeScript 测试,现在也支持 Solidity 测试。纯逻辑的单元测试用 Solidity 写更快(省去 JS-EVM 通信开销),集成测试和涉及复杂交互的场景仍然用 TypeScript。 ```typescript // test/Token.ts - TypeScript 集成测试 import { expect } from "chai"; import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; describe("Token", function () { async function deployTokenFixture() { const [owner, addr1] = await hre.ethers.getSigners(); const Token = await hre.ethers.getContractFactory("Token"); const token = await Token.deploy(1000000); return { token, owner, addr1 }; } it("应该能转账", async function () { const { token, owner, addr1 } = await loadFixture(deployTokenFixture); await token.transfer(addr1.address, 100); expect(await token.balanceOf(addr1.address)).to.equal(100); }); }); ``` ```solidity // test/Token.t.sol - Solidity 单元测试 (Hardhat 3) import "forge-std/Test.sol"; import "../contracts/Token.sol"; contract TokenTest is Test { Token token; function setUp() public { token = new Token(1000000); } function testTransfer() public { address addr1 = makeAddr("addr1"); token.transfer(addr1, 100); assertEq(token.balanceOf(addr1), 100); } } ``` `loadFixture` 是测试性能的关键——它只在第一次调用时执行部署,后续测试复用快照,速度比每个 `it` 都重新部署快一个数量级。 ### 部署与验证 Hardhat Ignition 是 Hardhat 3 官方的部署方案。和手写部署脚本不同,Ignition 用声明式的方式定义部署流程,自动处理依赖顺序、重试和并发。 ```typescript // ignition/modules/TokenModule.ts import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("TokenModule", (m) => { const token = m.contract("Token", [1000000]); return { token }; }); ``` 部署到测试网并验证合约: ```bash npx hardhat ignition deploy ignition/modules/TokenModule.ts --network sepolia npx hardhat verify --network sepolia <CONTRACT_ADDRESS> 1000000 ``` Ignition 的一个实际好处:如果部署中途失败(比如 Gas 不够),它会记住已完成的步骤,重试时跳过不再需要重新执行的部分。 ### 调试工具 这是 Hardhat 区别于其他框架最明显的功能。 **Solidity 堆栈跟踪**:合约 revert 时,Hardhat 给出的错误信息包含完整的 Solidity 调用栈,而不只是一个 `revert` 地址。这在排查复杂合约交互时省了大量时间。 **console.log**:在合约里直接 `console.log`,和 JavaScript 一样用: ```solidity import "hardhat/console.sol"; function transfer(address to, uint256 amount) public { console.log("Transferring from", msg.sender, "to", to); console.log("Amount:", amount); // ... 转账逻辑 } ``` 部署到真实网络时,`console.sol` 的调用会被编译器自动移除,不消耗额外 Gas。 ### 插件生态 Hardhat 的插件系统是它最大的生态优势。几个常用插件: | 插件 | 用途 | |------|------| | `@nomicfoundation/hardhat-toolbox` | 一站式工具包,包含 ethers.js、TypeChain、测试工具等 | | `@nomicfoundation/hardhat-verify` | 在 Etherscan 等区块浏览器上验证合约源码 | | `@openzeppelin/hardhat-upgrades` | 支持可升级合约的部署和管理 | | `@nomicfoundation/hardhat-chai-matchers` | 提供 `revertedWith`、`emit` 等链上断言 | 安装插件只需 `npm install` 并在 `hardhat.config.js` 里 `require`,配置和扩展都很直观。 ## Hardhat 还是 Foundry? 2026 年选框架,这个问题绕不开。 **选 Hardhat 的场景**: - 团队主力是 JavaScript/TypeScript,前端和合约在同一仓库 - 需要 Etherscan 验证、可升级合约、多链部署等成熟插件 - 合约逻辑不复杂,更看重开发流程的整体顺滑度 **选 Foundry 的场景**: - 纯 Solidity 开发,不需要 JS 生态 - 追求极致编译和测试速度(Foundry 仍然比 Hardhat 3 快约 2 倍) - 需要 Fuzz 测试和 Invariant 测试 - 做协议层开发或安全审计 **实际选择**:不少团队两边都用——Foundry 负责合约开发和快速测试,Hardhat 负责部署脚本和前端集成。Hardhat 3 支持 Solidity 测试后,纯用 Hardhat 的门槛也在降低。 ## 快速开始 ```bash mkdir my-project && cd my-project npm init -y npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox npx hardhat init # 选择 TypeScript 项目 ``` 初始化后的项目结构: ``` my-project/ ├── contracts/ # Solidity 合约 ├── ignition/ # Ignition 部署模块 ├── test/ # 测试文件 ├── hardhat.config.ts # 配置文件 └── artifacts/ # 编译产物(gitignore) ``` 开发循环就是三个命令: ```bash npx hardhat compile # 编译 npx hardhat test # 测试 npx hardhat ignition deploy ignition/modules/Deploy.ts --network sepolia # 部署 ```
服务端5月28日 05:30
Hardhat 常用插件有哪些?各自解决什么问题?Hardhat 插件按用途分四类:**开发调试**(hardhat-ethers 做合约交互、hardhat-network-helpers 模拟链上状态、hardhat-chai-matchers 写测试断言)、**部署验证**(hardhat-verify 把源码验证到 Etherscan 和 Sourcify、hardhat-deploy 管理部署脚本和升级)、**质量分析**(hardhat-gas-reporter 看 Gas 消耗、solidity-coverage 跑覆盖率、hardhat-contract-sizer 查合约大小是否超限)、**安全审计**(hardhat-slither 跑静态漏洞扫描)。大多数项目直接装 `@nomicfoundation/hardhat-toolbox` 就够了——它把 ethers、network-helpers、chai-matchers、verify、coverage 全打包了。 ## 追问 ### hardhat-toolbox 和单独装插件哪个好? toolbox 是全家桶,新项目装一个就能跑测试、验证合约、看覆盖率。项目变复杂后可以拆掉它,按需装插件,减少依赖。没有性能差异,只是安装体积的区别。 ### hardhat-verify 和 hardhat-etherscan 是什么关系? `@nomiclabs/hardhat-etherscan` 是旧版,Nomic Labs 改组为 Nomic Foundation 后推出了 `@nomicfoundation/hardhat-verify`。新版除了 Etherscan 还支持 Sourcify 验证,API 也有调整。新项目必须用 hardhat-verify,旧项目建议迁移——etherscan 版已经不再维护了。 ### Gas 优化只靠 hardhat-gas-reporter 行吗? 不行。reporter 只是告诉你每个函数花了多少 Gas,是诊断工具不是优化工具。实际优化要用 `viaIR` 编译选项处理栈深度问题、减少 storage 写入次数、用 `calldata` 替代 `memory` 参数。正确的用法是优化前跑一次 reporter 记录基线,优化后再跑一次量化效果。 ### hardhat-deploy 和 Hardhat Ignition 怎么选? Ignition 是 Hardhat 3 官方内置的部署模块,声明式设计——你定义合约依赖关系,它自动编排部署顺序和并行执行。hardhat-deploy 是社区插件,基于脚本,强项是代理合约升级和部署历史回溯。新项目优先用 Ignition,老项目如果依赖 hardhat-deploy 的升级功能可以继续用,两者不冲突。 ### hardhat-slither 能替代安全审计吗? 替代不了。Slither 做静态分析,能扫出未初始化变量、重入模式、权限缺失这类模式化漏洞。但业务逻辑漏洞(比如错误的访问控制顺序、价格操控)它看不出来。项目上线前 Slither 做第一道快筛,正式审计必须靠人。 ### solidity-coverage 跑起来很慢怎么办? coverage 模式要插桩每行代码计算执行次数,比正常测试慢 5-10 倍是正常的。可以只在 CI 的特定阶段跑,开发时跳过。Hardhat 2.x 里 coverage 是独立任务,Hardhat 3 对此做了优化但差距仍然存在。 ## 写段代码 ```js // hardhat.config.js — 常用插件配置示例 require("@nomicfoundation/hardhat-toolbox"); require("hardhat-gas-reporter"); module.exports = { solidity: "0.8.24", gasReporter: { enabled: true, currency: "USD" } }; ```
服务端5月28日 05:24
Hardhat 配置文件有哪些核心配置项?Hardhat 配置文件 `hardhat.config.js`(或 `.ts`)导出一个配置对象,核心配置项按使用频率排列: **1. solidity** — 指定编译器版本和优化设置。简写 `"0.8.19"` 或对象形式开启 optimizer: ```javascript solidity: { version: "0.8.19", settings: { optimizer: { enabled: true, runs: 200 } } } ``` `runs` 权衡部署 Gas 和执行 Gas:runs 越高,执行越省 Gas 但部署越贵。库合约建议设 999999,一次性合约设 1。支持多版本编译,用 `overrides` 按路径指定: ```javascript solidity: { version: "0.8.19", overrides: { "contracts/legacy/": { version: "0.6.12" } } } ``` **2. networks** — 定义连接的区块链网络。`hardhat` 是内置本地网络,其他网络需配 RPC 和账户: ```javascript networks: { hardhat: { chainId: 31337 }, sepolia: { url: process.env.SEPOLIA_RPC_URL, accounts: [process.env.PRIVATE_KEY] } } ``` `accounts` 支持私钥数组或助记词对象。敏感信息必须走环境变量,不要硬编码私钥。 **3. defaultNetwork** — 不带 `--network` 参数时的默认网络,默认值 `"hardhat"`。 **4. paths** — 自定义目录结构(sources、tests、cache、artifacts),默认值够用,多仓库 monorepo 才需要改。 **5. etherscan** — 配置 API Key,部署后自动在 Etherscan 验证合约源码,省得手动提交。 **6. gasReporter** — 测试时输出 Gas 消耗报告,`currency: "USD"` 显示费用估算,上线前评估成本用。 **7. mocha** — 覆盖测试框架配置,常用 `timeout` 调整超时(合约测试默认 40000ms,复杂场景可能不够)。 插件通过 `require()` 引入,写在配置文件顶部,不算配置项但必须在这里加载。 ## 追问 ### optimizer runs 设成多少合适? 看合约调用频次。高频调用设 200-999,让编译器多优化执行路径;一次性部署设 1 省部署费。Uniswap V3 的池子合约 runs 设了几千,因为每笔交易都要执行。 ### hardhat 网络和 localhost 有什么区别? `hardhat` 是内存临时网络,每次 `npx hardhat test` 都重启,数据不持久。`localhost` 需要先 `npx hardhat node` 启动独立进程,数据跨命令保持,适合调试前端交互和合约状态持久化场景。 ### 配置文件用 JS 还是 TS? TS 更好——有类型提示,拼写错误编译期就能发现。Hardhat 3 已默认推荐 TS 配置。安装 `ts-node` 和 `@types/node`,文件改名为 `hardhat.config.ts` 即可,语法不变。 ### 怎么让配置文件不暴露私钥? 用 `dotenv` 包从 `.env` 文件读取环境变量,`.env` 加入 `.gitignore`。生产环境用密钥管理服务(如 AWS Secrets Manager)替代 `.env` 文件。CI/CD 里通过 GitHub Secrets 注入。
服务端5月27日 20:04
Hardhat 如何支持 TypeScript 和类型安全?Hardhat 对 TypeScript 的支持不是"能用"级别,而是"原生级"。初始化项目时直接选 TypeScript 模板,配置文件、部署脚本、测试文件全部 .ts,编译合约后还能自动生成类型定义——你调用合约方法时,编辑器会告诉你参数类型对不对、返回值是什么。 智能合约一旦部署上链就很难改,类型检查能在编译阶段把低级错误拦住,这个价值不需要多解释。下面从项目搭建到实际开发,把 Hardhat + TypeScript 的完整链路走一遍。 ## 项目初始化:选 TypeScript 模板 ```bash mkdir my-project && cd my-project npm init -y npx hardhat init # 选择 "Create a TypeScript project" ``` Hardhat 会自动生成 `hardhat.config.ts`、`tsconfig.json`,并安装必要的 TypeScript 依赖: ```bash npm install --save-dev ts-node typescript @types/node @types/mocha ``` 如果你用的是 `@nomicfoundation/hardhat-toolbox`(推荐),这些依赖已经包含在内,不用手动装。 一个默认生成的 `hardhat.config.ts` 长这样: ```typescript import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { solidity: "0.8.24", }; export default config; ``` `HardhatUserConfig` 这个类型会帮你检查配置项有没有写错——比如把 `solidity` 拼成 `solididy`,TypeScript 直接报红。 ## Hardhat Toolbox:一个包装搞定类型安全 `@nomicfoundation/hardhat-toolbox` 是 Hardhat 官方推荐的插件合集,包含了 TypeChain、Ethers.js、Chai 匹配器等,装一个包就把类型安全的环境搭好: ```bash npm install --save-dev @nomicfoundation/hardhat-toolbox ``` 在 `hardhat.config.ts` 中引入后,执行编译: ```bash npx hardhat compile ``` 你会看到类似输出: ``` Compiled 1 Solidity file successfully Generating typings for: 1 artifacts in dir: typechain-types for target: ethers-v6 Successfully generated 3 typings! ``` 这就是 TypeChain 在工作——它读取合约编译产出的 ABI,自动生成 TypeScript 类型定义文件,放在 `typechain-types/` 目录下。 ## 合约交互:从"盲调"到"类型安全" 假设你有一个简单的 Solidity 合约: ```solidity // contracts/Lock.sol // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.24; contract Lock { uint public unlockTime; address public owner; constructor(uint256 _unlockTime) payable { require(block.timestamp < _unlockTime, "Unlock time should be in the future"); unlockTime = _unlockTime; owner = msg.sender; } } ``` ### 没有 TypeChain 时 你用 JavaScript 或裸 TypeScript 调用合约方法,没有任何类型提示: ```typescript // 没有类型安全——参数类型、返回值全靠猜 const lock = await ethers.getContractAt("Lock", address); const time = await lock.unlockTime(); // 返回什么类型?不知道 ``` ### 有 TypeChain 后 ```typescript import { Lock } from "../typechain-types"; const LockFactory = await ethers.getContractFactory("Lock"); const lock = await LockFactory.deploy(futureTimestamp) as Lock; // 编辑器自动补全,参数类型和返回值都有提示 const time: bigint = await lock.unlockTime(); const owner: string = await lock.owner(); ``` 区别很明显:`unlockTime()` 返回 `bigint` 而不是 `any`,`owner()` 返回 `string`——如果后续代码把 `owner` 当数字用,编译阶段就能发现。 ## 测试中的类型安全 Hardhat 的 TypeScript 项目用 Mocha + Chai + Ethers.js 做测试,配合 TypeChain 生成的类型,测试代码也能享受完整的类型检查: ```typescript import { expect } from "chai"; import { ethers } from "hardhat"; import { Lock } from "../typechain-types"; import { time } from "@nomicfoundation/hardhat-network-helpers"; describe("Lock", function () { let lock: Lock; beforeEach(async function () { const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; const LockFactory = await ethers.getContractFactory("Lock"); lock = await LockFactory.deploy(unlockTime, { value: ethers.parseEther("1") }); }); it("should set the right unlockTime", async function () { const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; const expectedTime = BigInt(await time.latest()) + BigInt(ONE_YEAR_IN_SECS); expect(await lock.unlockTime()).to.be.closeTo(expectedTime, 2n); }); }); ``` 注意 `lock` 变量的类型是 `Lock`,不是 `any`——你在测试里调一个不存在的方法,TypeScript 会直接报错,不用等运行时才发现拼写错误。 跑测试时加 `--typecheck` 可以在执行前做一轮完整类型检查: ```bash npx hardhat test --typecheck ``` 建议在 CI 或 pre-commit hook 里加上这个标志,确保类型问题不会溜进代码库。 ## 配置文件的类型安全 `hardhat.config.ts` 本身就是类型安全的大本营。`HardhatUserConfig` 类型会约束你写正确的配置结构: ```typescript import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200, }, }, }, networks: { sepolia: { url: process.env.SEPOLIA_RPC_URL || "", accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], }, }, }; export default config; ``` 如果你把 `optimizer.enabled` 写成 `"yes"` 而不是 `true`,TypeScript 立刻报错。这种在配置层面的类型保护,避免了"部署到测试网怎么都不对,最后发现是配置拼错"的尴尬。 ### 环境变量的类型定义 在项目根目录创建 `hardhat.config.d.ts`,给环境变量加类型: ```typescript declare namespace NodeJS { interface ProcessEnv { SEPOLIA_RPC_URL: string; PRIVATE_KEY: string; ETHERSCAN_API_KEY: string; } } ``` 这样 `process.env.SEPOLIA_RPC_URL` 在编辑器里就不会被推断为 `string | undefined`,省去到处写 `!` 非空断言。 ## 部署脚本的类型安全 部署脚本是类型安全最容易出现缺口的地方。正确的做法是给部署脚本加上类型: ```typescript // scripts/deploy.ts import { ethers } from "hardhat"; import { Lock } from "../typechain-types"; async function main() { const unlockTime = Math.floor(Date.now() / 1000) + 60 * 60; // 1 小时后 const LockFactory = await ethers.getContractFactory("Lock"); const lock: Lock = await LockFactory.deploy(unlockTime, { value: ethers.parseEther("0.001"), }); await lock.waitForDeployment(); console.log("Lock deployed to:", await lock.getAddress()); } main().catch((error) => { console.error(error); process.exitCode = 1; }); ``` `waitForDeployment()` 是 ethers v6 的写法——v5 用的是 `deployed()`,已经在 v6 中被移除。如果你在网上抄到 v5 的代码直接用,TypeScript 会直接报方法不存在,这恰好是类型安全帮你挡住的一个常见坑。 ## 常见踩坑和解决方案 ### 1. 全局变量 vs 显式导入 JavaScript 项目里 Hardhat 会把 `ethers` 等对象注入全局作用域,但 TypeScript 项目必须显式导入: ```typescript // ✅ 正确 import { ethers } from "hardhat"; // ❌ TypeScript 中不存在全局 ethers ``` 如果你从 JS 项目迁移过来,记得把所有全局引用改成 import。 ### 2. ethers v5 和 v6 的写法差异 2024 年后 `hardhat-toolbox` 默认使用 ethers v6,关键差异: | 操作 | ethers v5 | ethers v6 | |------|-----------|-----------| | 等待部署完成 | `await contract.deployed()` | `await contract.waitForDeployment()` | | 获取合约地址 | `contract.address` | `await contract.getAddress()` | | 解析 ETH 单位 | `ethers.utils.parseEther("1")` | `ethers.parseEther("1")` | | BigInt 转换 | `value.toNumber()` | `Number(value)` 或直接用 `bigint` | 如果项目里混用了 v5 和 v6 的写法,TypeScript 的类型检查会帮你发现不兼容的调用——前提是你装的是 v6 版本的类型定义。 ### 3. TypeChain 生成文件要不要提交到 Git 建议把 `typechain-types/` 加入 `.gitignore`,让它在每次编译时重新生成。这样合约改动后类型定义总是最新的,不会出现代码和类型不同步的问题。 ### 4. 类型检查只在显式请求时执行 Hardhat 默认运行任务时不做类型检查(为了速度)。你可以在 `hardhat.config.ts` 中设置 `typechain.target` 确保生成正确版本,但类型检查需要手动触发: ```bash # 单独跑类型检查 npx hardhat compile && npx tsc --noEmit # 或在测试时加上 --typecheck npx hardhat test --typecheck ``` 在 CI 流水线里加一个 `tsc --noEmit` 步骤,能确保每次提交都不会引入类型错误。 ### 5. Hardhat Runtime Environment 的类型扩展 如果你装了第三方插件,`hre` 上可能缺少类型声明。可以通过模块扩展补上: ```typescript // hardhat.config.ts import "hardhat/types/runtime"; declare module "hardhat/types/runtime" { interface HardhatRuntimeEnvironment { myCustomPlugin: { doSomething: () => Promise<void>; }; } } ``` 但要注意,随意扩展类型容易和其他插件冲突,只在确实需要时才这么做。 ## TypeScript vs JavaScript:值不值得切? 简单对比一下在 Hardhat 项目中的实际体感: | 方面 | JavaScript | TypeScript | |------|-----------|------------| | 合约调用 | 返回 `any`,参数类型靠记忆 | 自动补全 + 类型检查 | | 配置错误 | 运行时才报错 | 编译时直接标红 | | 重构合约 | 全局搜索替换,容易遗漏 | 改一处,引用处全部报错 | | 团队协作 | 看注释或源码才知道参数含义 | 类型和注释一体化 | | 学习成本 | 低 | 需要理解类型系统,但 Hardhat 模板已配好 | 对于新项目,没有理由不用 TypeScript——Hardhat 的模板已经帮你把基础设施搭好了,额外成本几乎为零。老项目迁移需要一点工作量,主要是加 import 和类型声明,但迁移完成后维护体验明显提升。 总结一句话:Hardhat 的 TypeScript 支持不是锦上添花,是标配。从项目初始化到合约交互、测试、部署,全链路都有类型保护。唯一需要留意的是 ethers v5/v6 的 API 差异,以及显式导入 vs 全局变量的区别——搞清楚这两点,剩下的跟着模板走就行。
服务端5月27日 20:01
Hardhat Network 的特点和优势是什么?Hardhat Network 是 Hardhat 框架内置的本地以太坊开发网络,专为智能合约开发、测试和调试而设计。它让开发者无需部署到真实链上即可完成全流程开发验证,是以太坊开发工具链中的核心组件。 ## 核心特性 ### 1. 即时挖矿(Automining) Hardhat Network 默认启用自动挖矿模式——每笔交易提交后立即被打包进下一个区块,无需等待出块时间: ```javascript // hardhat.config.js module.exports = { networks: { hardhat: { mining: { auto: true, // 默认开启,交易即时确认 interval: 5000 // 也可设为定时出块(毫秒) } } } }; ``` 关闭自动挖矿后,交易会进入内存池(mempool),行为与 Geth 客户端一致,适合测试交易排序和 MEV 场景。 ### 2. 预置测试账户 启动时自动生成 20 个测试账户,每个账户预分配 10000 ETH: ```javascript const [deployer, user1, user2] = await hre.ethers.getSigners(); console.log("Deployer address:", deployer.address); console.log("Balance:", hre.ethers.formatEther(await hre.ethers.provider.getBalance(deployer.address))); // 输出: Balance: 10000.0 ``` 还可自定义账户配置: ```javascript networks: { hardhat: { accounts: { count: 5, // 只生成 5 个账户 accountsBalance: "100000000000000000000000" // 每个账户 100000 ETH } } } ``` ### 3. 状态快照与回滚 `evm_snapshot` 和 `evm_revert` 允许在测试中保存和恢复网络状态,避免每次测试都重新部署: ```javascript describe("Token 测试", function () { let snapshotId; before(async function () { // 部署合约 this.token = await Token.deploy(); // 保存初始状态快照 snapshotId = await hre.network.provider.send("evm_snapshot"); }); afterEach(async function () { // 每个测试用例后恢复快照 await hre.network.provider.send("evm_revert", [snapshotId]); snapshotId = await hre.network.provider.send("evm_snapshot"); }); it("转账测试", async function () { await this.token.transfer(user1.address, 100); // 测试结束后状态自动回滚 }); }); ``` ### 4. Solidity 调试工具 #### console.log 调试 在合约中直接输出调试信息,无需触发交易: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "hardhat/console.sol"; contract DebugExample { function calculate(uint256 a, uint256 b) public pure returns (uint256) { console.log("Input a:", a); console.log("Input b:", b); uint256 result = a * b + a; console.log("Result:", result); return result; } } ``` #### 堆栈追踪 交易失败时,Hardhat Network 提供组合 JavaScript 和 Solidity 的完整调用栈: ``` Error: VM Exception while processing transaction: reverted with reason string 'Insufficient balance' at Token.transfer (contracts/Token.sol:45) at process._tickCallback (internal/process/next_tick.js:68:7) ``` ### 5. 主网分叉(Mainnet Forking) 基于真实链上状态创建本地分叉,可直接与已部署的 DeFi 协议交互: ```javascript networks: { hardhat: { forking: { url: "https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY", enabled: true, // 可选:锁定到特定区块,确保测试可复现 blockNumber: 18500000 } } } ``` 分叉环境下的典型用法——与 Uniswap 交互测试: ```javascript it("在分叉网络上交换代币", async function () { // 模拟持有 USDC 的鲸鱼账户 await hre.network.provider.request({ method: "hardhat_impersonateAccount", params: ["0x55fe002..."], // USDC 鲸鱼地址 }); const whale = await hre.ethers.getSigner("0x55fe002..."); // 用鲸鱼账户执行交易 const usdc = await hre.ethers.getContractAt("IERC20", USDC_ADDRESS, whale); await usdc.transfer(user1.address, hre.ethers.parseUnits("1000", 6)); }); ``` ### 6. 时间操控与挖矿控制 在测试中灵活操控区块时间和出块: ```javascript // 前进指定时间 await hre.network.provider.send("evm_increaseTime", [3600]); // 快进 1 小时 await hre.network.provider.send("evm_mine"); // 挖一个新区块 // 设置到具体时间戳 await hre.network.provider.send("evm_setNextBlockTimestamp", [1700000000]); // 重置网络状态 await hre.network.provider.send("hardhat_reset"); ``` ### 7. Gas 报告 结合 `hardhat-gas-reporter` 插件,量化合约函数的 Gas 消耗: ```javascript // 安装:npm install hardhat-gas-reporter require("hardhat-gas-reporter"); module.exports = { gasReporter: { enabled: true, currency: "USD", gasPrice: 20 // Gwei } }; ``` 输出示例: ``` ·--------------------------------|---------------------------|-------------|-----------------------------| | Solc version: 0.8.20 · Gas price: 20 gwei · USD/ETH: 3800 | |--------------------------------|---------------------------|-------------|-----------------------------| | Method · Min · Max · Avg | |································|···························|·············|·····························| | transfer · 51654 · 61654 · 53987 | | approve · 46263 · 46263 · 46263 | |--------------------------------|---------------------------|-------------|-----------------------------| ``` ## 与其他本地网络的对比 | 特性 | Hardhat Network | Ganache | Anvil (Foundry) | |------|----------------|---------|-----------------| | 即时挖矿 | 默认开启 | 默认开启 | 默认开启 | | Solidity 堆栈追踪 | 完整支持 | 不支持 | 支持 | | console.log | 原生支持 | 不支持 | 支持 | | 主网分叉 | 原生内置 | 需配置 | 原生内置 | | 快照/回滚 | 支持 | 支持 | 支持 | | 账户模拟 | hardhat_impersonateAccount | 不直接支持 | vm.prank | | TypeScript 集成 | 深度集成 | 弱 | 弱 | | 运行性能 | 中等(EDR 加速) | 中等 | 极快(Rust 原生) | | 插件生态 | 最丰富 | 有限 | 增长中 | ## 典型使用场景 **单元测试与集成测试** ```javascript describe("MyContract", function () { it("应正确执行质押", async function () { const [owner, staker] = await hre.ethers.getSigners(); const token = await Token.deploy(); const staking = await Staking.deploy(token.target); await token.connect(staker).approve(staking.target, hre.ethers.parseEther("100")); await staking.connect(staker).stake(hre.ethers.parseEther("100")); expect(await staking.stakedBalance(staker.address)).to.equal(hre.ethers.parseEther("100")); }); }); ``` **DeFi 协议集成测试** 通过主网分叉测试与 Aave、Uniswap 等协议的交互逻辑,无需在测试网排队或消耗真实 Gas。 **时间依赖逻辑验证** 利用 `evm_increaseTime` 和 `evm_mine` 测试锁仓期、投票截止时间等时间相关功能。 **Gas 优化迭代** 通过 Gas 报告对比不同实现的 Gas 消耗,持续优化合约效率。 ## 常见配置参考 ```javascript // hardhat.config.js — 完整开发配置 require("@nomicfoundation/hardhat-toolbox"); require("hardhat-gas-reporter"); module.exports = { solidity: "0.8.24", networks: { hardhat: { chainId: 31337, mining: { auto: true }, forking: process.env.FORK_URL ? { url: process.env.FORK_URL, blockNumber: 18500000 } : undefined, accounts: { count: 20, accountsBalance: "10000000000000000000000" // 10000 ETH }, allowBlocksWithSameTimestamp: true, throwOnTransactionFailures: true, throwOnCallFailures: true } }, gasReporter: { enabled: process.env.REPORT_GAS === "true" } }; ``` ## 总结 Hardhat Network 的核心优势在于将开发调试效率最大化:即时确认交易、Solidity 级堆栈追踪、主网状态分叉、灵活的时间与账户操控,这些能力让开发者能在本地完成绝大部分验证工作,显著减少对测试网的依赖。虽然 Foundry/Anvil 在执行速度上有优势,但 Hardhat Network 凭借 TypeScript 深度集成和丰富的插件生态,仍是复杂 DApp 开发场景下的首选本地网络。