Hardhat 与 Foundry 开发工具链

Solidity 专业开发环境对比:Hardhat(JavaScript 任务流)与 Foundry(Rust 原生测试)。覆盖本地网络、分叉测试、Gas 快照、部署脚本、CI/CD 集成与完整实战示例。

导语:从 Remix 到生产级开发

Remix IDE 是学习 Solidity 的好地方,但生产级开发需要更强大的工具链:自动化测试、本地 EVM 网络、主网分叉、Gas 优化分析、部署流水线。

当前最主流的两大工具链:

  • Hardhat(前身 Buidler):JavaScript/TypeScript 生态,插件丰富、社区庞大
  • Foundry:Rust 编写,原生测试用 Solidity 写、执行速度极快

一句话总结:Hardhat 适合 JS 全栈团队快速搭建,Foundry 适合合约开发者追求极致性能和原生测试体验。


1. Hardhat 环境搭建

1.1 项目初始化

# 创建项目
mkdir defi-project && cd defi-project
npm init -y
npm install --save-dev hardhat

# 初始化 Hardhat
npx hardhat init
# → 选择 Create a TypeScript project
# → 自动安装:@nomicfoundation/hardhat-toolbox

# 目录结构
contracts/        # Solidity 合约
scripts/          # 部署脚本
test/             # 测试用例
hardhat.config.ts # 配置文件

1.2 hardhat.config.ts

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";

dotenv.config();

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
    },
  },
  networks: {
    hardhat: {
      forking: {
        url: process.env.ALCHEMY_MAINNET_URL || "",
        blockNumber: 18000000,  // 固定区块号,保证测试确定性
      },
    },
    goerli: {
      url: process.env.ALCHEMY_GOERLI_URL,
      accounts: [process.env.PRIVATE_KEY!],
    },
    mainnet: {
      url: process.env.ALCHEMY_MAINNET_URL,
      accounts: [process.env.PRIVATE_KEY!],
    },
  },
  gasReporter: {
    enabled: true,
    currency: "USD",
    coinmarketcap: process.env.CMC_API_KEY,
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
};

export default config;

1.3 编写与运行测试

// test/Token.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";
import { MyToken } from "../typechain-types";

describe("MyToken", function () {
  let token: MyToken;
  let owner: any, addr1: any, addr2: any;

  beforeEach(async function () {
    [owner, addr1, addr2] = await ethers.getSigners();
    
    const Token = await ethers.getContractFactory("MyToken");
    token = await Token.deploy("TestToken", "TTK", 1000000);
    await token.waitForDeployment();
  });

  it("should assign total supply to owner", async function () {
    const ownerBalance = await token.balanceOf(owner.address);
    expect(await token.totalSupply()).to.equal(ownerBalance);
  });

  it("should transfer tokens between accounts", async function () {
    await token.transfer(addr1.address, 100);
    expect(await token.balanceOf(addr1.address)).to.equal(100);
    
    await token.connect(addr1).transfer(addr2.address, 50);
    expect(await token.balanceOf(addr2.address)).to.equal(50);
  });

  it("should fail if sender has insufficient balance", async function () {
    await expect(
      token.connect(addr1).transfer(owner.address, 1)
    ).to.be.revertedWith("ERC20: insufficient balance");
  });
});
# 运行测试(自动编译合约)
npx hardhat test

# 运行指定测试文件
npx hardhat test test/Token.test.ts

# 生成 Gas 报告
REPORT_GAS=true npx hardhat test

# 覆盖范围报告
npx hardhat coverage

1.4 部署脚本

// scripts/deploy.ts
import { ethers } from "hardhat";

async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying with account:", deployer.address);

  const Token = await ethers.getContractFactory("MyToken");
  const token = await Token.deploy("MyToken", "MTK", 1000000);
  await token.waitForDeployment();

  console.log("Token deployed to:", await token.getAddress());
  
  // 验证合约(可选)
  await run("verify:verify", {
    address: await token.getAddress(),
    constructorArguments: ["MyToken", "MTK", 1000000],
  });
}

main().catch(console.error);
# 部署到本地网络
npx hardhat run scripts/deploy.ts

# 部署到测试网
npx hardhat run scripts/deploy.ts --network goerli

一句话总结:Hardhat 通过 TypeScript 测试 + Ethers.js + 插件生态,提供了 Web3 开发最成熟的工具链。


2. Foundry 环境搭建

2.1 安装与项目初始化

# 安装 foundryup
curl -L https://foundry.paradigm.xyz | bash
foundryup

# 创建项目
forge init foundry-project
cd foundry-project

# 目录结构
src/          # 合约源码
test/         # Solidity 测试(!)
script/       # 部署脚本
lib/          # Git 子模块依赖
foundry.toml  # 配置文件

2.2 foundry.toml

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.20"
optimizer = true
optimizer_runs = 200

# 主网分叉测试
[profile.default.fuzz]
runs = 256

[rpc_endpoints]
mainnet = "${ALCHEMY_MAINNET_URL}"
goerli = "${ALCHEMY_GOERLI_URL}"

[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }

2.3 Solidity 中写测试(Foundry 的独特优势)

// test/MyToken.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "../src/MyToken.sol";

contract MyTokenTest is Test {
    MyToken token;
    address owner = address(this);
    address alice = makeAddr("alice");
    address bob = makeAddr("bob");

    function setUp() public {
        token = new MyToken("TestToken", "TTK", 1_000_000);
        // 给测试账户发代币
        token.transfer(alice, 10_000);
        token.transfer(bob, 10_000);
    }

    function test_InitialSupply() public {
        assertEq(token.totalSupply(), 1_000_000 * 10**18);
        assertEq(token.balanceOf(owner), 1_000_000 * 10**18 - 20_000);
    }

    function test_Transfer() public {
        vm.prank(alice);  // 模拟 alice 调用
        token.transfer(bob, 1000);
        
        assertEq(token.balanceOf(alice), 9000);
        assertEq(token.balanceOf(bob), 11_000);
    }

    function test_RevertOnInsufficientBalance() public {
        vm.prank(alice);
        vm.expectRevert("ERC20: insufficient balance");
        token.transfer(bob, 100_000);  // alice 只有 10_000
    }

    function testFuzz_Transfer(uint256 amount) public {
        amount = bound(amount, 0, token.balanceOf(owner));
        
        token.transfer(alice, amount);
        assertEq(token.balanceOf(alice), 10_000 + amount);
    }

    // 使用主网分叉测试
    function testFork_RealTokenBalance() public {
        vm.createSelectFork("mainnet", 18000000);
        
        address usdcHolder = 0x...;  // 真实主网地址
        address usdc = 0xA0b86a33E6449...;  // USDC 合约地址
        
        uint256 balance = IERC20(usdc).balanceOf(usdcHolder);
        assertGt(balance, 0);
    }
}
# 运行测试
forge test

# 详细输出
forge test -vvv

# 运行指定测试
forge test --match-test test_Transfer

# Gas 快照(生成 Gas 使用基线)
forge snapshot

# 生成 Gas 差异报告
forge snapshot --diff

# 格式化代码
forge fmt

# 静态分析
forge build

2.4 部署脚本

// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Script.sol";
import "../src/MyToken.sol";

contract DeployScript is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        
        vm.startBroadcast(deployerPrivateKey);
        
        MyToken token = new MyToken("MyToken", "MTK", 1_000_000);
        
        vm.stopBroadcast();
    }
}
# 模拟部署(dry run)
forge script script/Deploy.s.sol --rpc-url goerli --broadcast --verify -vvvv

# 实际部署
forge script script/Deploy.s.sol --rpc-url mainnet --broadcast --verify

一句话总结:Foundry 最大的创新是用 Solidity 写测试——测试者站在合约内部视角,可以使用作弊码(cheatcodes)操纵时间、区块号、余额,测试效率极高。


3. Hardhat vs Foundry 对比

维度HardhatFoundry
语言TypeScript/JavaScriptRust(工具)+ Solidity(测试)
测试写法JS/TS(外部调用合约)Solidity(从合约内部测试)
执行速度中等(JS VM)极快(Rust EVM)
分叉测试hardhat_impersonateAccountvm.createSelectFork
Gas 报告内置插件forge snapshot
调试console.logforge test -vvv
类型生成typechain(自动生成)手动或 cast
社区规模更大快速增长
学习曲线低(JS 开发者友好)中(需学习 Solidity 测试模式)
最佳场景全栈 DApp 开发、团队以 JS 为主合约审计、复杂协议开发、速度优先

一句话总结:Hardhat 生态成熟插件众多,Foundry 原生测试极致高效——许多团队采用"Hardhat 做前端集成 + Foundry 做合约测试"的混合方案。


4. CI/CD 集成

GitHub Actions + Foundry

# .github/workflows/test.yml
name: test

on: [push, pull_request]

jobs:
  check:
    name: Foundry project
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1

      - name: Run tests
        run: forge test -vvv

      - name: Run snapshot
        run: forge snapshot --check
        # 如果 Gas 使用量超出基线会失败

GitHub Actions + Hardhat

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx hardhat test
      - run: npx hardhat coverage

一句话总结:将合约测试集成到 CI 流水线是安全开发的基本要求——任何人提交代码时自动运行全套测试,防止回归问题。


5. 调试技巧与常见问题

开发过程中经常遇到以下问题,掌握排查方法能大幅提升效率:

问题现象根因解决
out of gas合约函数消耗超过区块 gas 上限优化循环、减少 storage 写入、分批处理
revert 无原因Solidity 0.8+ 的 require 未提供消息字符串使用 revert CustomError() 或带消息的 require
本地测试通过,部署失败不同网络的 gas limit、EIP 支持度不同使用相同 Solidity 版本,在测试网上复测
Metamask 估算 gas 偏低复杂逻辑(如循环依赖 calldata 大小)手动调高 gas limit
事件未触发topics 不匹配或地址错误用 Etherscan 直接查看原始日志

Hardhat 调试利器:

# 打印合约内部 console.log(需安装 hardhat/console.sol)
npx hardhat test --logs

# 查看具体交易的 trace
npx hardhat node
# 然后在另一个终端
npx hardhat run scripts/debug.ts --network localhost

Foundry 调试利器:

# 详细的执行 trace
forge test -vvv --match-test testName

# 生成测试覆盖热力图
forge coverage --report lcov
# 用 VS Code 的 Coverage Gutters 插件可视化

# 分析合约存储布局
forge inspect ContractName storage-layout

一句话总结:调试智能合约需要熟悉 trace、日志和存储分析工具,Hardhat 的 console.log 和 Foundry 的 -vvv 是最常用的两把利器。


5. 总结

工具核心优势推荐场景
HardhatTS 生态、丰富插件、大社区DApp 全栈开发、团队主力 JS
Foundry原生 Solidity 测试、极快协议开发、审计、Gas 敏感项目
Ethers.js最成熟的前端库DApp 前端与合约交互
viem轻量类型安全版 ethers新项目、TypeScript 优先
Wagmi/RainbowKitReact hooks + 钱包连接前端集成

延伸阅读:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「blockchain」更多文章

  1. 智能合约安全审计与常见漏洞
  2. DeFi 核心协议与流动性挖矿
  3. ERC-20 与 ERC-721 标准详解