教程区块链区块链技术ch1313.1 开发框架对比:Hardhat、Foundry 与 Truffle

本页目录

智能合约开发框架已经从"可选工具"变成"基础设施"。2024 年的生态中,Foundry 是协议工程师的首选,Hardhat 是 DApp 全栈开发的标准,Truffle 已退出历史舞台


13.1.1 三大框架的历史与现状

框架语言测试性能定位2024 状态
TruffleNode.jsMocha + Chai教学/遗产已弃用
HardhatNode.js/TSEthers.js + ChaiDApp 全栈主流
FoundryRust内建 fuzzing极快协议/审计增长最快

核心差异的架构根源

graph LR
    subgraph Truffle[Truffle Architecture]
        TC[Truffle Config] --> TB[Build Pipeline]
        TB --> TM[Migrations JS]
        TM --> TE[Test: Mocha/Chai]
        TE --> TG[Gas Reporter]
    end
    subgraph Hardhat[Hardhat Architecture]
        HH[Hardhat Runtime] --> HP[Plugin System]
        HP --> HE[Ethers.js Provider]
        HP --> HT[Hardhat Network]
        HT --> HN[npx hardhat test]
    end
    subgraph Foundry[Foundry Architecture]
        F[Forge: Rust Binary] --> FC[Compile]
        FC --> FT[Test in Solidity]
        FT --> FF[Fuzzing Engine]
        F --> FCa[Cast CLI]
        FCa --> FA[Anvil Local Node]
        FA --> FCH[Chisel REPL]
    end
    
    style Truffle fill:#ffebee
    style Hardhat fill:#fff3e0
    style Foundry fill:#e8f5e9

13.1.2 Hardhat:灵活与生态

核心组件

  • Hardhat Runtime Environment (HRE):脚本和任务的全局对象
  • Hardhat Network:内置本地 EVM,支持快照、时间操纵、impersonate
  • 插件:ethers, waffle, verify, deploy, gas-reporter 等无缝集成
typescript
/**
 * Hardhat 风格的部署与测试脚本(TypeScript)
 */
interface HardhatRuntime {
  network: { name: string; config: object };
  ethers: {
    getSigners(): Promise<{ address: string; provider: any }[]>;
    getContractFactory(name: string): Promise<any>;
  };
  run(task: string, args: object): Promise<any>;
}

// 模拟 hardhat run 的部署任务
async function deployToken(hre: HardhatRuntime) {
  const [deployer] = await hre.ethers.getSigners();
  console.log("Deploying with:", deployer.address);
  
  const Token = await hre.ethers.getContractFactory("ERC20Token");
  const token = await Token.deploy("MockToken", "MTK", 1000000n);
  // 在 Hardhat 网络中,部署即挖到区块
  console.log("Token deployed to:", token.address);
  return token;
}

// 测试结构:beforeEach + fixture 模式
class HardhatTestSuite {
  private async fixture(): Promise<{ token: MockToken; owner: string; user: string }> {
    const token = await deployToken(this.hre);
    const [owner, user] = (await this.hre.ethers.getSigners()).map(s => s.address);
    return { token, owner, user };
  }
  
  async testTransfer() {
    const { token, owner, user } = await this.fixture();
    const tx = await token.transfer(user, 100n);
    const receipt = await tx.wait();
    
    const balance = await token.balanceOf(user);
    console.assert(balance === 100n, "Transfer failed");
    console.log("Gas used:", receipt.gasUsed);
  }
}

// 模拟 HRE
const hre: HardhatRuntime = {
  network: { name: "hardhat", config: {} },
  ethers: {
    async getSigners() {
      return [
        { address: "0xDeployer...", provider: null },
        { address: "0xUser1...", provider: null },
        { address: "0xUser2...", provider: null },
      ];
    },
    async getContractFactory(name: string) {
      return {
        async deploy(...args: any[]) {
          return { address: "0xToken...", async transfer(to: string, amt: bigint) {
            return { wait: async () => ({ gasUsed: 52000n }) };
          }, async balanceOf(addr: string) { return 100n; }};
        }
      };
    }
  },
  async run(task, args) { return; }
};

new HardhatTestSuite(hre).testTransfer().then(() => console.log("Hardhat test passed"));

13.1.3 Foundry:Rust 驱动的极速工具链

四大工具

  • Forge:编译 + 测试 + Gas 快照。Solidity 中直接写 invariantfuzz 测试。
  • Cast:CLI 与链交互的工具,类似 curl 对于 HTTP。
  • Anvil:本地 EVM 节点,支持 fork 主网。
  • Chisel:Solidity 交互式 REPL(即时尝试)。

Foundry 测试的革命性:在 Solidity 中测试 Solidity

solidity
// Foundry 测试示例(Solidity 语法)
contract FoundryTest is Test {
    MockToken token;
    
    function setUp() public {
        token = new MockToken(1000000e18);
    }
    
    // 模糊测试:Foundry 自动生成 256 个随机值
    function testFuzzTransfer(address to, uint256 amount) public {
        vm.assume(to != address(0)); // 过滤无效输入
        vm.assume(amount <= token.balanceOf(address(this)));
        
        token.transfer(to, amount);
        assertEq(token.balanceOf(to), amount);
    }
    
    // 不变式测试
    function invariantTotalSupply() public {
        assertEq(token.totalSupply(), 1000000e18);
    }
}
typescript
/**
 * 模拟 Foundry 模糊测试引擎的核心逻辑
 */
interface FuzzCase {
  inputs: (bigint | string | boolean)[];
  failed: boolean;
  error?: string;
}

class FuzzEngine {
  private seed: number;
  
  constructor(seed: number = 42) { this.seed = seed; }
  
  private randomBigInt(max: bigint = 2n**256n - 1n): bigint {
    this.seed = (this.seed * 16807 + 0) % 2147483647;
    return BigInt(this.seed) % max;
  }
  
  private randomAddress(): string {
    return "0x" + Array(40).fill(0).map(() => 
      (this.randomBigInt(16n) as unknown as number).toString(16)
    ).join("");
  }
  
  fuzz<E>(
    cases: number,
    testFn: (inputs: (bigint | string)[]) => void,
    invariants: { filter: (inputs: (bigint | string)[]) => boolean }[],
  ): FuzzCase[] {
    const results: FuzzCase[] = [];
    for (let i = 0; i < cases; i++) {
      const inputs: (bigint | string)[] = [
        this.randomAddress(),
        this.randomBigInt(1000000n),
      ];
      
      // 应用不变式过滤器
      const valid = invariants.every(inv => inv.filter(inputs));
      if (!valid) { i--; continue; }
      
      try {
        testFn(inputs);
        results.push({ inputs, failed: false });
      } catch (e) {
        results.push({ inputs, failed: true, error: String(e) });
      }
    }
    return results;
  }
  
  // 最小化反例(shrinking)
  shrink(failure: FuzzCase, testFn: any, invariants: any): FuzzCase {
    // 尝试减小输入值,寻找最小失败案例
    let [addr, amount] = failure.inputs;
    let shrunk = [addr, (amount as bigint) / 2n];
    try {
      testFn(shrunk);
      return failure; // 无法缩小
    } catch {
      return { ...failure, inputs: shrunk };
    }
  }
}

// 模拟 fuzzing ERC20 transfer
const fuzzer = new FuzzEngine(42);
const results = fuzzer.fuzz(
  100,
  ([to, amount]) => {
    if (to === "0x" + "0".repeat(40)) throw new Error("Zero address not allowed");
    if ((amount as bigint) > 1000000n) throw new Error("Exceeds balance");
    // 正常逻辑
  },
  [{ filter: ([to, amount]) => (amount as bigint) <= 1000000n }],
);

const failures = results.filter(r => r.failed);
console.log(`Fuzzed 100 cases, ${failures.length} failures`);
if (failures.length > 0) {
  console.log("First failure:", failures[0].error, "inputs:", failures[0].inputs);
}

13.1.4 选择建议

场景推荐框架原因
DApp 全栈(前端 + 合约)HardhatJS 生态无缝,插件丰富
协议开发 / 审计Foundry模糊测试、速度、Solidity-native
快速 PoC / 教学Hardhat文档最完整,社区最大
遗留项目维护Truffle——— 已弃用,建议迁移
需要 CI 流水线两者皆可Foundry 更快,Hardhat 更灵活

> ← 上一章 ch12 总结 | 前往 → 13.2 本地开发与主网分叉 |*

评论

0

评论加载中…

发表评论

0/2000