关于 web3-testing
面向 Web3 智能合约测试的指导技能,涵盖 Hardhat 和 Foundry。内容包括单元与集成测试、模糊测试、Gas 检查、主网分叉、覆盖率和合约验证;所提及的辅助文件并未随附。
主要功能
- 01Hardhat 与 Foundry 测试指导
- 02单元和集成测试模式
- 03模糊测试与边界用例
- 04Gas 与覆盖率检查
- 05主网分叉测试
- 06合约验证指导
适用场景
- 01编写智能合约单元测试
- 02检查 Gas 用量和测试覆盖率
- 03基于主网分叉状态测试
- 04准备合约验证步骤
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill web3-testing安装后在 Claude Code 中输入/web3-testing确认已加载。
详细介绍
支持材料可用性
本目录副本包含以下内联指南,但不包含以下支持文件路径。这些是外部前置条件,并非随附文件:
<support-root>/references/hardhat-setup.md<support-root>/references/foundry-setup.md<support-root>/references/test-patterns.md<support-root>/references/mainnet-forking.md<support-root>/references/contract-verification.md<support-root>/assets/hardhat-config.js<support-root>/assets/test-suite.js<support-root>/assets/foundry.toml<support-root>/scripts/test-contract.sh
使用依赖这些路径的步骤前,请索取原始支持材料或现有安装;本目录未提供经验证的下载来源。请根据该安装解析每个路径,检查其内容,并核实适用的许可和执行前置条件。所需文件不存在时,不要运行相关命令,也不要声称已遵循引用的流程。不要伪造缺失的脚本,也不要将名称相同但无关的文件视为等效文件。本文档中已完整说明的独立步骤,可在用户授权范围内继续执行;依赖这些文件的步骤则应报告为不可用。
Web3 智能合约测试
掌握使用 Hardhat、Foundry 和高级测试模式进行智能合约全面测试的策略。
以下情况请勿使用此技能
- 任务与 Web3 智能合约测试无关
- 需要使用此范围之外的其他领域或工具
说明
- 明确目标、约束和所需输入。
- 应用相关最佳实践并验证结果。
- 提供可执行的步骤和验证方法。
以下情况请使用此技能
- 为智能合约编写单元测试
- 设置集成测试套件
- 执行 Gas 优化测试
- 对边缘情况进行模糊测试
- 分叉主网以进行逼真测试
- 自动化测试覆盖率报告
- 在 Etherscan 上验证合约
Hardhat 测试环境设置
CommonJS 代码片段展示的是旧版 Hardhat 2 / ethers 5 工具链。请根据项目锁文件固定兼容且经过审核的版本;未经迁移,不要将它们与 Hardhat 3 / ethers 6 组合使用。合约 fixture 和接口是项目特定的前置条件,并非随附的可部署代码。仅使用本地开发链和一次性密钥。分叉状态仅存在于本地;切勿为图省事而在有资金的网络上广播交易或部署。远程源码验证是单独的发布操作。
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
require("hardhat-gas-reporter");
require("solidity-coverage");
module.exports = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
hardhat: {
forking: {
url: process.env.MAINNET_RPC_URL,
blockNumber: 15000000,
},
},
},
gasReporter: {
enabled: true,
currency: "USD",
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
单元测试模式
const { expect } = require("chai");
const { ethers, network } = 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, owner, 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, addr1 } = await loadFixture(deployTokenFixture);
const tx = await token.transfer(addr1.address, 100);
const receipt = await tx.wait();
expect(receipt.gasUsed).to.be.lessThan(50000);
});
});
});
Foundry 测试(Forge)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Token.sol";
interface IERC20MetadataForTest {
function symbol() external view returns (string memory);
}
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 testTransferInsufficientBalance() public {
vm.expectRevert(); // Prefer the exact expected error from the tested contract.
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(vm.envString("MAINNET_RPC_URL"), 15000000);
// Interact with mainnet contracts
address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
assertEq(IERC20MetadataForTest(dai).symbol(), "DAI");
}
}
高级测试模式
快照与还原
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
});
});
主网分叉
describe("Mainnet Fork Tests", function () {
let uniswapRouter, dai, usdc;
before(async function () {
await network.provider.request({
method: "hardhat_reset",
params: [
{
forking: {
jsonRpcUrl: process.env.MAINNET_RPC_URL,
blockNumber: 15000000,
},
},
],
});
// Connect to existing mainnet contracts
uniswapRouter = await ethers.getContractAt(
"IUniswapV2Router",
"0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
);
dai = await ethers.getContractAt(
"IERC20",
"0x6B175474E89094C44Da98b954EedeAC495271d0F",
);
});
it("Should swap on Uniswap", async function () {
// Test with real Uniswap contracts
});
});
模拟账户身份
it("Should impersonate whale account", async function () {
const whaleAddress = "0x...";
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [whaleAddress],
});
const whale = await ethers.getSigner(whaleAddress);
// Use whale's tokens
await dai
.connect(whale)
.transfer(addr1.address, ethers.utils.parseEther("1000"));
});
Gas 优化测试
const { expect } = require("chai");
describe("Gas Optimization", function () {
it("Compare gas usage between implementations", async function () {
const Implementation1 =
await ethers.getContractFactory("OptimizedContract");
const Implementation2 = await ethers.getContractFactory(
"UnoptimizedContract",
);
const contract1 = await Implementation1.deploy();
const contract2 = await Implementation2.deploy();
const tx1 = await contract1.doSomething();
const receipt1 = await tx1.wait();
const tx2 = await contract2.doSomething();
const receipt2 = await tx2.wait();
console.log("Optimized gas:", receipt1.gasUsed.toString());
console.log("Unoptimized gas:", receipt2.gasUsed.toString());
expect(receipt1.gasUsed).to.be.lessThan(receipt2.gasUsed);
});
});
覆盖率报告
# Generate coverage report
npx hardhat coverage
# Output shows:
# File | % Stmts | % Branch | % Funcs | % Lines |
# -------------------|---------|----------|---------|---------|
# contracts/Token.sol | 100 | 90 | 100 | 95 |
合约验证
// Verify on Etherscan
await hre.run("verify:verify", {
address: contractAddress,
constructorArguments: [arg1, arg2],
});
# Or via CLI
npx hardhat verify --network mainnet CONTRACT_ADDRESS "Constructor arg1" "arg2"
CI/CD 集成
使用仓库支持的 Node 版本和经过审核、固定到提交的 Actions。从已提交的锁文件安装依赖,编译,运行本地链测试并收集覆盖率。将 RPC 凭证保存在平台的机密存储中;不要向不受信任的 fork 任务暴露机密。向外部上传覆盖率数据是可选的;披露任何源码或覆盖率数据都需要授权。仅有良好的覆盖率百分比并不等于安全审计。
资源
- references/hardhat-setup.md:Hardhat 配置指南
- references/foundry-setup.md:Foundry 测试框架
- references/test-patterns.md:测试最佳实践
- references/mainnet-forking.md:分叉测试策略
- references/contract-verification.md:Etherscan 验证
- assets/hardhat-config.js:完整的 Hardhat 配置
- assets/test-suite.js:全面的测试示例
- assets/foundry.toml:Foundry 配置
- scripts/test-contract.sh:自动化测试脚本
最佳实践
- 测试覆盖率:目标是达到 >90% 的覆盖率
- 边界情况:测试边界条件
- Gas 限制:确认函数不会触及区块 Gas 限制
- 重入攻击:测试是否存在重入漏洞
- 访问控制:测试未经授权的访问尝试
- 事件:确认事件已触发
- Fixtures:使用 fixtures 避免代码重复
- 主网分叉:使用真实合约进行测试
- 模糊测试:使用基于属性的测试
- CI/CD:在每次提交时自动运行测试
限制
- 仅当任务明确符合上述范围时,才使用此技能。
- 不要将输出视为特定环境验证、测试或专家审查的替代品。
- 如果缺少必要的输入、权限、安全边界或成功标准,请暂停并请求澄清。