精通 Hardhat 与 Foundry 的智能合约全面测试策略
掌握基于 Hardhat 和 Foundry 的完整测试工作流,涵盖单元测试、集成测试、Gas 优化验证、主网分叉仿真及 CI/CD 自动化部署流程,确保智能合约安全上线。
为什么需要这个技能
智能合约一旦部署到主网,修改成本极高且不可逆转。在发布前,仅仅通过静态代码审查无法发现逻辑漏洞或边界条件错误。专业的测试策略能够模拟真实用户操作、极端 Gas 费用环境以及潜在的攻击向量。本技能指导开发者利用 Hardhat 和 Foundry 生态,构建从单元测试到主网分叉仿真的完整验证体系,显著降低资金损失风险。
适用场景
- 智能合约开发与审计:编写单元测试,确保逻辑符合预期,并验证复杂的状态变更。
- Gas 费用优化:通过对比不同实现的 Gas 消耗,寻找优化空间,减少用户调用成本。
- 极端场景验证:利用 Fuzzing 和边界条件测试,发现常规用例难以触发的异常路径。
- 真实环境仿真:分叉主网数据,在不消耗真实资金的情况下模拟真实的交易环境和网络延迟。
- CI/CD 流水线集成:将测试用例自动接入 GitHub Actions,每次提交即触发全量验证。
核心工作流
1. 基础配置与工具链选择
根据团队偏好选择 Hardhat(JS/TS,生态丰富)或 Foundry(Rust,速度快)。配置好 hardhat.config.js 或 foundry.toml,接入 Etherscan 接口以验证合约,并开启 Gas 报告功能。
2. 单元测试与 Fixture 模式
利用 loadFixture 避免重复部署代码。在 describe 块中编写针对部署、转账失败、事件触发等场景的测试。
const { expect } = require("chai");
const { ethers } = require("hardhat");
const {
loadFixture,
time,
} = require("@nomicfoundation/hardhat-network-helpers");
describe("Token Contract", function () {
// Fixture for test setup
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy();
return { token, owner, addr1, addr2 };
}
describe("Deployment", function () {
it("Should set the right owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
expect(await token.owner()).to.equal(owner.address);
});
it("Should assign total supply to owner", async function () {
const { token, owner } = await loadFixture(deployTokenFixture);
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
await expect(token.transfer(addr1.address, 50)).to.changeTokenBalances(
token,
[owner, addr1],
[-50, 50],
);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const { token, addr1 } = await loadFixture(deployTokenFixture);
const initialBalance = await token.balanceOf(addr1.address);
await expect(
token.connect(addr1).transfer(owner.address, 1),
).to.be.revertedWith("Insufficient balance");
});
it("Should emit Transfer event", async function () {
const { token, owner, addr1 } = await loadFixture(deployTokenFixture);
await expect(token.transfer(addr1.address, 50))
.to.emit(token, "Transfer")
.withArgs(owner.address, addr1.address, 50);
});
});
describe("Time-based tests", function () {
it("Should handle time-locked operations", async function () {
const { token } = await loadFixture(deployTokenFixture);
// Increase time by 1 day
await time.increase(86400);
// Test time-dependent functionality
});
});
describe("Gas optimization", function () {
it("Should use gas efficiently", async function () {
const { token } = await loadFixture(deployTokenFixture);
const tx = await token.transfer(addr1.address, 100);
const receipt = await tx.wait();
expect(receipt.gasUsed).to.be.lessThan(50000);
});
});
});
3. Foundry 进阶测试与主网分叉
利用 Foundry 的 vm cheatcodes 进行作弊码测试,如模拟账户、增加余额。结合 vm.createSelectFork 分叉主网,连接真实的 Uniswap 等合约进行集成测试。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Token.sol";
contract TokenTest is Test {
Token token;
address owner = address(1);
address user1 = address(2);
address user2 = address(3);
function setUp() public {
vm.prank(owner);
token = new Token();
}
function testInitialSupply() public {
assertEq(token.totalSupply(), 1000000 * 10**18);
}
function testTransfer() public {
vm.prank(owner);
token.transfer(user1, 100);
assertEq(token.balanceOf(user1), 100);
assertEq(token.balanceOf(owner), token.totalSupply() - 100);
}
function testFailTransferInsufficientBalance() public {
vm.prank(user1);
token.transfer(user2, 100); // Should fail
}
function testCannotTransferToZeroAddress() public {
vm.prank(owner);
vm.expectRevert("Invalid recipient");
token.transfer(address(0), 100);
}
// Fuzzing test
function testFuzzTransfer(uint256 amount) public {
vm.assume(amount > 0 && amount <= token.totalSupply());
vm.prank(owner);
token.transfer(user1, amount);
assertEq(token.balanceOf(user1), amount);
}
// Test with cheatcodes
function testDealAndPrank() public {
// Give ETH to address
vm.deal(user1, 10 ether);
// Impersonate address
vm.prank(user1);
// Test functionality
assertEq(user1.balance, 10 ether);
}
// Mainnet fork test
function testForkMainnet() public {
vm.createSelectFork("https://eth-mainnet.alchemyapi.io/v2/...");
// Interact with mainnet contracts
address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
assertEq(IERC20(dai).symbol(), "DAI");
}
}
4. 快照机制与状态恢复
利用 Hardhat 的 evm_snapshot 和 evm_revert 实现复杂状态变更测试。在 beforeEach 保存快照,afterEach 恢复状态,确保各测试用例独立干净,无需手动清理。
describe("Complex State Changes", function () {
let snapshotId;
beforeEach(async function () {
snapshotId = await network.provider.send("evm_snapshot");
});
afterEach(async function () {
await network.provider.send("evm_revert", [snapshotId]);
});
it("Test 1", async function () {
// Make state changes
});
it("Test 2", async function () {
// State reverted, clean slate
});
});
5. 自动化测试报告与 CI/CD
生成覆盖报告并集成到 GitHub Actions。配置 Workflow 在每次 push 或 PR 时自动编译、运行测试并上传结果到 Codecov,确保代码质量可控。
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16"
- run: npm install
- run: npx hardhat compile
- run: npx hardhat test
- run: npx hardhat coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
下载和安装
解压后将目录放入你的 AI 工具 skills 文件夹,重启工具后即可使用。
你可能还需要
暂无推荐