智能合约测试完全指南 - 从入门到精通
版本: v3.0
最后更新: 2026-03-14
目标: 让你看完后直接精通智能合约测试
📚 目录
第一部分:基础理论
第二部分:实战测试
第三部分:高级技术
第四部分:精通之路
第一部分:基础理论
1. EVM原理深度解析
1.1 EVM是什么?
Ethereum Virtual Machine (EVM) 是以太坊的核心执行引擎,是一个:
- ✅ 图灵完备的虚拟机
- ✅ 基于栈的执行模型
- ✅ 确定性的状态机
- ✅ 分布式计算环境
1.2 EVM架构
┌─────────────────────────────────────────┐
│ Smart Contract Code │
│ (Solidity/Vyper Source) │
└────────────────┬────────────────────────┘
│ 编译
▼
┌─────────────────────────────────────────┐
│ Bytecode (字节码) │
│ (部署在区块链上的机器码) │
└────────────────┬────────────────────────┘
│ 执行
▼
┌─────────────────────────────────────────┐
│ EVM Execution Engine │
│ ┌────────┐ ┌──────┐ ┌────────────┐ │
│ │ Stack │ │Memory│ │ Storage │ │
│ │(栈) │ │(内存)│ │ (存储) │ │
│ └────────┘ └──────┘ └────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Gas Mechanism │ │
│ │ (Gas消耗计算) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
│ 状态改变
▼
┌─────────────────────────────────────────┐
│ World State (世界状态) │
│ - Account Balance (账户余额) │
│ - Contract Storage (合约存储) │
│ - Nonce (交易计数) │
└─────────────────────────────────────────┘
1.3 EVM数据结构
1.3.1 Stack (栈)
特性:
- 最大深度: 1024
- 每个元素: 256位 (32字节)
- 操作: PUSH, POP, DUP, SWAP
示例操作:
PUSH1 0x05 // 将5压入栈
PUSH1 0x03 // 将3压入栈
ADD // 弹出两个值,相加,结果压入栈
栈状态变化:
[] // 初始
[5] // PUSH1 0x05
[5, 3] // PUSH1 0x03
[8] // ADD (5+3=8)
1.3.2 Memory (内存)
特性:
- 线性字节数组
- 可动态扩展
- 按32字节访问
- Gas成本随使用增长
示例操作:
MSTORE(offset, value) // 在offset位置存储32字节
MLOAD(offset) // 从offset读取32字节
Memory布局:
0x00-0x3f: 保留区域 (用于哈希方法)
0x40-0x5f: 空闲内存指针
0x60-...: 用户数据
1.3.3 Storage (存储)
特性:
- 持久化存储 (写入区块链)
- Key-Value映射 (32字节 -> 32字节)
- 成本最高 (20,000 gas写入)
- 支持合约间持久化
示例操作:
SSTORE(key, value) // 存储
SLOAD(key) // 读取
Storage布局示例:
Slot 0: owner地址
Slot 1: totalSupply
Slot 2: balances映射的根
Slot 3: allowances映射的根
1.4 Gas机制详解
Gas计算公式
总Gas消耗 = Σ(操作码Gas成本) + 内存扩展成本 + 存储成本
常见操作Gas成本:
- ADD/SUB/MUL: 3 gas
- DIV/MOD: 5 gas
- SHA3: 30 gas + 6 gas/word
- SLOAD: 2100 gas (冷) / 100 gas (热)
- SSTORE: 20000 gas (新) / 5000 gas (修改) / 2900 gas (删除返还)
- CALL: 700 gas + 9000 gas (转账)
- CREATE: 32000 gas
Gas优化示例
// ❌ 不优化 - 多次SLOAD
function badExample() public view returns (uint) {
uint total = 0;
for (uint i = 0; i < count; i++) { // 每次循环读取count
total += items[i];
}
return total;
}
// Gas: ~2100 * count + ...
// ✅ 优化 - 缓存到内存
function goodExample() public view returns (uint) {
uint total = 0;
uint length = count; // 只读取一次
for (uint i = 0; i < length; i++) {
total += items[i];
}
return total;
}
// Gas: 2100 + 3 * count + ...
1.5 EVM执行流程
1. 交易提交
↓
2. 语法验证 (签名、nonce、gas limit)
↓
3. 从发送者扣除 gas limit * gas price
↓
4. EVM开始执行字节码
↓
5. 逐条执行操作码
- 消耗Gas
- 修改状态
- 触发事件
↓
6. 执行完成或revert
↓
7. 返还剩余Gas
↓
8. 状态提交到区块链
1.6 关键EVM概念
1.6.1 调用类型
// 1. CALL - 普通外部调用
targetContract.someFunction();
// - 切换执行上下文
// - msg.sender变为调用者
// - 有独立的gas限制
// 2. DELEGATECALL - 委托调用
address(targetContract).delegatecall(data);
// - 使用调用者的存储
// - msg.sender保持不变
// - 用于库和代理模式
// 3. STATICCALL - 静态调用
address(targetContract).staticcall(data);
// - 只读调用
// - 不能修改状态
// - 用于view/pure函数
1.6.2 事件和日志
event Transfer(address indexed from, address indexed to, uint256 value);
emit Transfer(sender, recipient, amount);
// 底层实现: LOG操作码
// LOG0, LOG1, LOG2, LOG3, LOG4
// 数字表示indexed参数数量
// 日志存储在交易收据中,不在状态树
// Gas成本: 375 + 375 * topics + 8 * data_size
2. 智能合约测试方法论
2.1 测试金字塔
┌──────────────┐
│ E2E Tests │ ← 少量,覆盖关键流程
│ (端到端) │
└──────────────┘
┌────────────────┐
│Integration Tests│ ← 中等数量,测试组件交互
│ (集成测试) │
└────────────────┘
┌──────────────────────┐
│ Unit Tests │ ← 大量,测试单个函数
│ (单元测试) │
└──────────────────────┘
2.2 测试分类
2.2.1 功能测试
目标: 验证合约按预期工作
describe("Token Transfer", function() {
it("should transfer tokens correctly", async function() {
// Arrange (准备)
const [owner, user1, user2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
const token = await Token.deploy();
await token.mint(user1.address, 1000);
// Act (执行)
await token.connect(user1).transfer(user2.address, 100);
// Assert (断言)
expect(await token.balanceOf(user2.address)).to.equal(100);
expect(await token.balanceOf(user1.address)).to.equal(900);
});
});
2.2.2 安全测试
目标: 发现漏洞和攻击向量
describe("Reentrancy Attack Prevention", function() {
it("should prevent reentrancy attack", async function() {
const Attacker = await ethers.getContractFactory("ReentrancyAttacker");
const attacker = await Attacker.deploy(vault.address);
// 尝试重入攻击
await expect(
attacker.attack({value: ethers.parseEther("1")})
).to.be.revertedWith("ReentrancyGuard: reentrant call");
});
});
2.2.3 边界条件测试
目标: 测试极端情况
describe("Edge Cases", function() {
it("should handle zero transfer", async function() {
await token.transfer(user.address, 0);
// 验证没有状态改变
});
it("should handle max uint256", async function() {
const maxUint = ethers.MaxUint256;
await token.mint(user.address, maxUint);
// 验证边界值
});
it("should revert on insufficient balance", async function() {
await expect(
token.transfer(user.address, 1000000)
).to.be.revertedWith("Insufficient balance");
});
});
2.2.4 状态机测试
目标: 验证状态转换正确
describe("State Transitions", function() {
it("should follow correct state flow", async function() {
// Initial State
expect(await contract.state()).to.equal(State.Pending);
// Transition to Active
await contract.activate();
expect(await contract.state()).to.equal(State.Active);
// Transition to Paused
await contract.pause();
expect(await contract.state()).to.equal(State.Paused);
// Cannot go directly from Paused to Ended
await expect(contract.end()).to.be.reverted;
});
});
2.3 测试覆盖率目标
功能覆盖率目标:
├── 语句覆盖率 (Statement): 95%+
├── 分支覆盖率 (Branch): 90%+
├── 函数覆盖率 (Function): 100%
└── 行覆盖率 (Line): 95%+
安全测试覆盖:
├── OWASP Top 10 漏洞
├── SWC Registry 常见漏洞
├── 历史攻击案例
└── 特定协议风险
3. 测试工具生态
3.1 Hardhat (推荐)
优势:
- ✅ 完整的开发环境
- ✅ 强大的调试功能
- ✅ 丰富的插件生态
- ✅ 内置本地网络
安装和配置:
# 初始化项目
npm init -y
npm install --save-dev hardhat
# 创建Hardhat项目
npx hardhat
# 安装依赖
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install --save-dev @nomicfoundation/hardhat-chai-matchers
npm install --save-dev chai ethers
hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 31337,
forking: {
url: process.env.MAINNET_RPC_URL, // 用于Fork测试
blockNumber: 19000000
}
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
}
},
gasReporter: {
enabled: true,
currency: 'USD',
coinmarketcap: process.env.COINMARKETCAP_API_KEY
}
};
3.2 Foundry (高级用户)
优势:
- ⚡ 极快的测试速度
- ✅ 用Solidity写测试
- ✅ 内置Fuzzing
- ✅ 强大的调试工具
安装:
curl -L https://foundry.paradigm.xyz | bash
foundryup
测试示例:
// test/Token.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Token.sol";
contract TokenTest is Test {
Token token;
address user1;
address user2;
function setUp() public {
token = new Token();
user1 = address(0x1);
user2 = address(0x2);
token.mint(user1, 1000);
}
function testTransfer() public {
vm.prank(user1); // 模拟user1调用
token.transfer(user2, 100);
assertEq(token.balanceOf(user2), 100);
assertEq(token.balanceOf(user1), 900);
}
function testFuzzTransfer(uint256 amount) public {
vm.assume(amount <= 1000); // 设置假设条件
vm.prank(user1);
token.transfer(user2, amount);
assertEq(token.balanceOf(user2), amount);
assertEq(token.balanceOf(user1), 1000 - amount);
}
}
运行测试:
# 运行所有测试
forge test
# 显示详细输出
forge test -vvv
# 测试覆盖率
forge coverage
# Gas报告
forge test --gas-report
3.3 测试工具对比
| 特性 | Hardhat | Foundry | Truffle |
|---|---|---|---|
| 测试语言 | JavaScript | Solidity | JavaScript |
| 速度 | 中等 | 极快 | 慢 |
| 调试 | 优秀 | 优秀 | 一般 |
| Fuzzing | 需插件 | 内置 | 不支持 |
| 学习曲线 | 平缓 | 陡峭 | 平缓 |
| 生态 | 丰富 | 增长中 | 成熟 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
4. 功能测试实战
4.1 ERC20代币完整测试
4.1.1 合约代码
// contracts/MyToken.sol
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
uint256 public constant MAX_SUPPLY = 1000000 * 10**18;
constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {}
function mint(address to, uint256 amount) public onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
4.1.2 完整测试套件
// test/MyToken.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function() {
let token;
let owner;
let user1;
let user2;
beforeEach(async function() {
[owner, user1, user2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("MyToken");
token = await Token.deploy();
await token.waitForDeployment();
});
describe("Deployment", function() {
it("Should set the right name and symbol", async function() {
expect(await token.name()).to.equal("MyToken");
expect(await token.symbol()).to.equal("MTK");
});
it("Should set the right owner", async function() {
expect(await token.owner()).to.equal(owner.address);
});
it("Should have correct decimals", async function() {
expect(await token.decimals()).to.equal(18);
});
it("Should start with zero total supply", async function() {
expect(await token.totalSupply()).to.equal(0);
});
});
describe("Minting", function() {
it("Should mint tokens to address", async function() {
const amount = ethers.parseEther("1000");
await token.mint(user1.address, amount);
expect(await token.balanceOf(user1.address)).to.equal(amount);
expect(await token.totalSupply()).to.equal(amount);
});
it("Should emit Transfer event on mint", async function() {
const amount = ethers.parseEther("1000");
await expect(token.mint(user1.address, amount))
.to.emit(token, "Transfer")
.withArgs(ethers.ZeroAddress, user1.address, amount);
});
it("Should revert if non-owner tries to mint", async function() {
const amount = ethers.parseEther("1000");
await expect(
token.connect(user1).mint(user2.address, amount)
).to.be.revertedWithCustomError(token, "OwnableUnauthorizedAccount");
});
it("Should revert if minting exceeds max supply", async function() {
const maxSupply = await token.MAX_SUPPLY();
const excessAmount = maxSupply + 1n;
await expect(
token.mint(user1.address, excessAmount)
).to.be.revertedWith("Exceeds max supply");
});
it("Should allow minting up to max supply", async function() {
const maxSupply = await token.MAX_SUPPLY();
await token.mint(user1.address, maxSupply);
expect(await token.totalSupply()).to.equal(maxSupply);
});
});
describe("Transfer", function() {
beforeEach(async function() {
await token.mint(user1.address, ethers.parseEther("1000"));
});
it("Should transfer tokens between accounts", async function() {
const amount = ethers.parseEther("100");
await token.connect(user1).transfer(user2.address, amount);
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseEther("900")
);
expect(await token.balanceOf(user2.address)).to.equal(amount);
});
it("Should emit Transfer event", async function() {
const amount = ethers.parseEther("100");
await expect(
token.connect(user1).transfer(user2.address, amount)
).to.emit(token, "Transfer")
.withArgs(user1.address, user2.address, amount);
});
it("Should revert on insufficient balance", async function() {
const amount = ethers.parseEther("2000");
await expect(
token.connect(user1).transfer(user2.address, amount)
).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance");
});
it("Should allow zero transfer", async function() {
await token.connect(user1).transfer(user2.address, 0);
// 验证没有错误
});
it("Should revert transfer to zero address", async function() {
await expect(
token.connect(user1).transfer(ethers.ZeroAddress, 100)
).to.be.revertedWithCustomError(token, "ERC20InvalidReceiver");
});
});
describe("Approval and TransferFrom", function() {
beforeEach(async function() {
await token.mint(user1.address, ethers.parseEther("1000"));
});
it("Should approve tokens for delegated transfer", async function() {
const amount = ethers.parseEther("100");
await token.connect(user1).approve(user2.address, amount);
expect(await token.allowance(user1.address, user2.address))
.to.equal(amount);
});
it("Should emit Approval event", async function() {
const amount = ethers.parseEther("100");
await expect(
token.connect(user1).approve(user2.address, amount)
).to.emit(token, "Approval")
.withArgs(user1.address, user2.address, amount);
});
it("Should allow transferFrom with approval", async function() {
const amount = ethers.parseEther("100");
await token.connect(user1).approve(user2.address, amount);
await token.connect(user2).transferFrom(
user1.address,
user2.address,
amount
);
expect(await token.balanceOf(user2.address)).to.equal(amount);
expect(await token.allowance(user1.address, user2.address))
.to.equal(0);
});
it("Should revert transferFrom without approval", async function() {
const amount = ethers.parseEther("100");
await expect(
token.connect(user2).transferFrom(
user1.address,
user2.address,
amount
)
).to.be.revertedWithCustomError(token, "ERC20InsufficientAllowance");
});
it("Should revert transferFrom exceeding allowance", async function() {
await token.connect(user1).approve(
user2.address,
ethers.parseEther("50")
);
await expect(
token.connect(user2).transferFrom(
user1.address,
user2.address,
ethers.parseEther("100")
)
).to.be.revertedWithCustomError(token, "ERC20InsufficientAllowance");
});
});
describe("Burning", function() {
beforeEach(async function() {
await token.mint(user1.address, ethers.parseEther("1000"));
});
it("Should burn tokens", async function() {
const burnAmount = ethers.parseEther("100");
await token.connect(user1).burn(burnAmount);
expect(await token.balanceOf(user1.address)).to.equal(
ethers.parseEther("900")
);
expect(await token.totalSupply()).to.equal(
ethers.parseEther("900")
);
});
it("Should emit Transfer event to zero address", async function() {
const burnAmount = ethers.parseEther("100");
await expect(token.connect(user1).burn(burnAmount))
.to.emit(token, "Transfer")
.withArgs(user1.address, ethers.ZeroAddress, burnAmount);
});
it("Should revert burn with insufficient balance", async function() {
await expect(
token.connect(user1).burn(ethers.parseEther("2000"))
).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance");
});
});
describe("Edge Cases", function() {
it("Should handle max uint256 approval", async function() {
await token.connect(user1).approve(user2.address, ethers.MaxUint256);
expect(await token.allowance(user1.address, user2.address))
.to.equal(ethers.MaxUint256);
});
it("Should not decrease infinite allowance", async function() {
await token.mint(user1.address, ethers.parseEther("1000"));
await token.connect(user1).approve(user2.address, ethers.MaxUint256);
await token.connect(user2).transferFrom(
user1.address,
user2.address,
ethers.parseEther("100")
);
// 无限授权不应减少
expect(await token.allowance(user1.address, user2.address))
.to.equal(ethers.MaxUint256);
});
});
describe("Gas Optimization Tests", function() {
it("Should compare gas costs for different operations", async function() {
const amount = ethers.parseEther("100");
// Mint gas cost
const mintTx = await token.mint(user1.address, amount);
const mintReceipt = await mintTx.wait();
console.log("Mint gas:", mintReceipt.gasUsed.toString());
// Transfer gas cost
const transferTx = await token.connect(user1).transfer(
user2.address,
amount
);
const transferReceipt = await transferTx.wait();
console.log("Transfer gas:", transferReceipt.gasUsed.toString());
});
});
});
4.2 运行测试
# 运行所有测试
npx hardhat test
# 运行特定测试文件
npx hardhat test test/MyToken.test.js
# 显示gas报告
REPORT_GAS=true npx hardhat test
# 测试覆盖率
npx hardhat coverage
5. 安全测试实战
5.1 重入攻击测试
5.1.1 漏洞合约
// contracts/VulnerableBank.sol
pragma solidity ^0.8.20;
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// ❌ 漏洞: 先转账后更新状态
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// 危险: 外部调用在状态更新之前
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount; // 状态更新太晚
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
5.1.2 攻击合约
// contracts/ReentrancyAttacker.sol
pragma solidity ^0.8.20;
interface IVulnerableBank {
function deposit() external payable;
function withdraw(uint256 amount) external;
}
contract ReentrancyAttacker {
IVulnerableBank public bank;
uint256 public attackAmount;
uint256 public attackCount;
constructor(address _bankAddress) {
bank = IVulnerableBank(_bankAddress);
}
function attack() public payable {
attackAmount = msg.value;
attackCount = 0;
// 存入
bank.deposit{value: attackAmount}();
// 发起攻击
bank.withdraw(attackAmount);
}
// 接收ETH时重入
receive() external payable {
attackCount++;
// 重入攻击
if (address(bank).balance >= attackAmount && attackCount < 5) {
bank.withdraw(attackAmount);
}
}
function getBalance() public view returns (uint256) {
return address(this).balance;
}
}
5.1.3 重入攻击测试
// test/ReentrancyAttack.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Reentrancy Attack Test", function() {
let bank;
let attacker;
let owner;
let user;
beforeEach(async function() {
[owner, user] = await ethers.getSigners();
// 部署漏洞合约
const VulnerableBank = await ethers.getContractFactory("VulnerableBank");
bank = await VulnerableBank.deploy();
// 正常用户存入5 ETH
await bank.connect(user).deposit({value: ethers.parseEther("5")});
// 部署攻击合约
const ReentrancyAttacker = await ethers.getContractFactory(
"ReentrancyAttacker"
);
attacker = await ReentrancyAttacker.deploy(await bank.getAddress());
});
it("Should demonstrate reentrancy attack", async function() {
const bankInitialBalance = await ethers.provider.getBalance(
await bank.getAddress()
);
console.log("Bank initial balance:", ethers.formatEther(bankInitialBalance));
// 攻击者只存入1 ETH
const attackAmount = ethers.parseEther("1");
// 执行攻击
await attacker.attack({value: attackAmount});
// 检查结果
const bankFinalBalance = await ethers.provider.getBalance(
await bank.getAddress()
);
const attackerBalance = await attacker.getBalance();
console.log("Bank final balance:", ethers.formatEther(bankFinalBalance));
console.log("Attacker balance:", ethers.formatEther(attackerBalance));
console.log("Stolen amount:", ethers.formatEther(
attackAmount - bankFinalBalance
));
// 验证攻击成功
expect(attackerBalance).to.be.gt(attackAmount);
expect(bankFinalBalance).to.be.lt(bankInitialBalance);
});
});
5.1.4 安全修复版本
// contracts/SecureBank.sol
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// ✅ 修复1: 使用 ReentrancyGuard
function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
// ✅ 修复2: 先更新状态
balances[msg.sender] -= amount;
// ✅ 修复3: 最后外部调用
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// ✅ 修复4: 使用 Pull Payment 模式
mapping(address => uint256) public pendingWithdrawals;
function requestWithdrawal(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
pendingWithdrawals[msg.sender] += amount;
}
function executeWithdrawal() public nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No pending withdrawal");
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
5.1.5 测试修复效果
describe("Secure Bank Test", function() {
it("Should prevent reentrancy attack", async function() {
const SecureBank = await ethers.getContractFactory("SecureBank");
const secureBank = await SecureBank.deploy();
await secureBank.connect(user).deposit({value: ethers.parseEther("5")});
const SecureAttacker = await ethers.getContractFactory(
"ReentrancyAttacker"
);
const secureAttacker = await SecureAttacker.deploy(
await secureBank.getAddress()
);
// 攻击应该失败
await expect(
secureAttacker.attack({value: ethers.parseEther("1")})
).to.be.reverted;
});
});
5.2 整数溢出测试
5.2.1 漏洞示例 (Solidity < 0.8.0)
// contracts/VulnerableToken.sol
pragma solidity ^0.7.6; // 旧版本,没有自动溢出检查
contract VulnerableToken {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// ❌ 漏洞: 可能整数下溢
function withdraw(uint256 amount) public {
balances[msg.sender] -= amount; // 没有检查余额
payable(msg.sender).transfer(amount);
}
}
5.2.2 溢出攻击测试
describe("Integer Overflow Attack", function() {
it("Should demonstrate underflow attack (Solidity < 0.8)", async function() {
// 注意: 需要使用 Solidity 0.7.x 编译
const amount = ethers.parseEther("1");
// 攻击者余额为0
expect(await vulnerableToken.balances(attacker.address)).to.equal(0);
// 尝试提取1 ETH (余额不足)
// 在 0.7.x 中会下溢变成超大数
await vulnerableToken.connect(attacker).withdraw(amount);
// 余额下溢
const balance = await vulnerableToken.balances(attacker.address);
expect(balance).to.be.gt(ethers.MaxUint256 / 2n);
});
});
5.2.3 安全版本 (Solidity 0.8+)
// contracts/SecureToken.sol
pragma solidity ^0.8.20; // ✅ 自动溢出检查
contract SecureToken {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value; // ✅ 自动检查溢出
}
function withdraw(uint256 amount) public {
// ✅ 会自动revert如果余额不足
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
5.3 访问控制测试
5.3.1 测试所有权限
describe("Access Control", function() {
let contract;
let owner;
let admin;
let user;
beforeEach(async function() {
[owner, admin, user] = await ethers.getSigners();
const Contract = await ethers.getContractFactory("MyContract");
contract = await Contract.deploy();
// 设置admin角色
await contract.grantRole(await contract.ADMIN_ROLE(), admin.address);
});
describe("Owner Functions", function() {
it("Should allow owner to call owner-only functions", async function() {
await expect(contract.connect(owner).ownerFunction())
.to.not.be.reverted;
});
it("Should reject non-owner calls", async function() {
await expect(contract.connect(user).ownerFunction())
.to.be.revertedWithCustomError(contract, "OwnableUnauthorizedAccount");
});
it("Should transfer ownership", async function() {
await contract.transferOwnership(admin.address);
expect(await contract.owner()).to.equal(admin.address);
});
});
describe("Role-Based Access", function() {
it("Should allow admin to call admin functions", async function() {
await expect(contract.connect(admin).adminFunction())
.to.not.be.reverted;
});
it("Should reject non-admin calls", async function() {
await expect(contract.connect(user).adminFunction())
.to.be.revertedWith("Missing ADMIN_ROLE");
});
it("Should grant and revoke roles", async function() {
// 授予角色
await contract.grantRole(await contract.ADMIN_ROLE(), user.address);
expect(await contract.hasRole(await contract.ADMIN_ROLE(), user.address))
.to.be.true;
// 撤销角色
await contract.revokeRole(await contract.ADMIN_ROLE(), user.address);
expect(await contract.hasRole(await contract.ADMIN_ROLE(), user.address))
.to.be.false;
});
});
});
5.4 前端运行攻击 (Front-Running)
5.4.1 漏洞场景
// contracts/VulnerableAuction.sol
pragma solidity ^0.8.20;
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
// ❌ 漏洞: 可被前端运行
function bid() public payable {
require(msg.value > highestBid, "Bid too low");
// 退还前一个最高出价
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
5.4.2 前端运行测试
describe("Front-Running Attack", function() {
it("Should demonstrate front-running vulnerability", async function() {
const [user1, user2] = await ethers.getSigners();
// User1 出价 1 ETH
await auction.connect(user1).bid({value: ethers.parseEther("1")});
// User2 看到 User1 的交易,立即出更高价
// 并设置更高的 gasPrice 让交易先执行
await auction.connect(user2).bid({
value: ethers.parseEther("1.01"),
gasPrice: ethers.parseUnits("100", "gwei") // 更高gas
});
// User2 通过前端运行成为最高出价者
expect(await auction.highestBidder()).to.equal(user2.address);
});
});
5.4.3 防护方案:承诺-揭示模式
// contracts/SecureAuction.sol
pragma solidity ^0.8.20;
contract SecureAuction {
mapping(address => bytes32) public commitments;
mapping(address => uint256) public bids;
uint256 public commitPhaseEnd;
uint256 public revealPhaseEnd;
constructor(uint256 commitDuration, uint256 revealDuration) {
commitPhaseEnd = block.timestamp + commitDuration;
revealPhaseEnd = commitPhaseEnd + revealDuration;
}
// 阶段1: 提交承诺 (隐藏出价)
function commit(bytes32 commitment) public {
require(block.timestamp < commitPhaseEnd, "Commit phase ended");
commitments[msg.sender] = commitment;
}
// 阶段2: 揭示出价
function reveal(uint256 amount, bytes32 nonce) public payable {
require(block.timestamp >= commitPhaseEnd, "Commit phase not ended");
require(block.timestamp < revealPhaseEnd, "Reveal phase ended");
require(msg.value == amount, "Amount mismatch");
// 验证承诺
bytes32 commitment = keccak256(abi.encodePacked(amount, nonce));
require(commitments[msg.sender] == commitment, "Invalid commitment");
bids[msg.sender] = amount;
}
}
6. Gas优化测试
6.1 Gas测试基准
describe("Gas Optimization", function() {
it("Should compare gas costs of different implementations", async function() {
// 方法1: 多次 SLOAD
const tx1 = await contract.methodWithMultipleSLOAD();
const receipt1 = await tx1.wait();
console.log("Multiple SLOAD gas:", receipt1.gasUsed.toString());
// 方法2: 缓存到内存
const tx2 = await contract.methodWithCaching();
const receipt2 = await tx2.wait();
console.log("With caching gas:", receipt2.gasUsed.toString());
// 计算节省
const saved = receipt1.gasUsed - receipt2.gasUsed;
console.log("Gas saved:", saved.toString());
expect(receipt2.gasUsed).to.be.lt(receipt1.gasUsed);
});
});
6.2 常见Gas优化技巧
6.2.1 缓存存储变量
// ❌ 不优化
function sumArray() public view returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < array.length; i++) { // 每次循环读取 length
total += array[i];
}
return total;
}
// Gas: ~2100 * iterations
// ✅ 优化
function sumArrayOptimized() public view returns (uint256) {
uint256 total = 0;
uint256 length = array.length; // 缓存到内存
for (uint256 i = 0; i < length; i++) {
total += array[i];
}
return total;
}
// Gas: ~2100 + 3 * iterations
6.2.2 使用 uint256 而不是小类型
// ❌ Gas浪费
uint8 a = 1;
uint8 b = 2;
uint8 c = a + b; // EVM需要额外转换
// ✅ Gas优化
uint256 a = 1;
uint256 b = 2;
uint256 c = a + b; // EVM原生支持
6.2.3 短路求值
// ✅ 将便宜的检查放前面
require(amount > 0 && balances[msg.sender] >= amount, "Invalid");
// ❌ 昂贵的操作在前
require(balances[msg.sender] >= amount && amount > 0, "Invalid");
6.2.4 批量操作
// ❌ 单独操作
function transferMultiple(address[] memory recipients, uint256[] memory amounts)
public
{
for (uint256 i = 0; i < recipients.length; i++) {
transfer(recipients[i], amounts[i]); // 每次都emit event
}
}
// ✅ 批量操作
function batchTransfer(address[] memory recipients, uint256[] memory amounts)
public
{
uint256 totalAmount = 0;
for (uint256 i = 0; i < recipients.length; i++) {
totalAmount += amounts[i];
_balances[recipients[i]] += amounts[i];
}
_balances[msg.sender] -= totalAmount;
emit BatchTransfer(recipients, amounts); // 只emit一次
}
7. 常见漏洞测试
7.1 漏洞检查清单
## 🔴 高危漏洞
- [ ] 重入攻击 (Reentrancy)
- [ ] 整数溢出/下溢 (Integer Overflow/Underflow)
- [ ] 访问控制缺陷 (Access Control)
- [ ] 未检查的外部调用 (Unchecked External Calls)
- [ ] 委托调用漏洞 (Delegatecall Vulnerability)
- [ ] 签名重放 (Signature Replay)
- [ ] 前端运行 (Front-Running)
## ⚠️ 中危漏洞
- [ ] DoS攻击 (Denial of Service)
- [ ] 时间戳依赖 (Timestamp Dependence)
- [ ] 随机数可预测 (Weak Randomness)
- [ ] 交易顺序依赖 (Transaction Ordering Dependence)
- [ ] Gas Limit DoS
- [ ] 未初始化的存储指针
## ℹ️ 低危问题
- [ ] 未检查的返回值
- [ ] 浮点和精度问题
- [ ] 废弃函数使用
- [ ] Gas优化问题
- [ ] 事件缺失
7.2 时间戳依赖测试
// contracts/VulnerableTimelock.sol
contract VulnerableTimelock {
uint256 public unlockTime;
constructor(uint256 duration) {
// ❌ 依赖 block.timestamp (可被矿工操纵)
unlockTime = block.timestamp + duration;
}
function withdraw() public {
require(block.timestamp >= unlockTime, "Still locked");
// 提取资金
}
}
// test/TimestampDependence.test.js
describe("Timestamp Dependence", function() {
it("Should demonstrate timestamp manipulation", async function() {
const duration = 3600; // 1小时
const timelock = await Timelock.deploy(duration);
// 获取当前时间
const block = await ethers.provider.getBlock("latest");
console.log("Current timestamp:", block.timestamp);
// 时间旅行 (Hardhat功能)
await ethers.provider.send("evm_increaseTime", [duration + 1]);
await ethers.provider.send("evm_mine");
// 现在可以提取
await expect(timelock.withdraw()).to.not.be.reverted;
});
it("Should test timestamp manipulation vulnerability", async function() {
// 矿工可以在一定范围内操纵时间戳 (通常±15秒)
await ethers.provider.send("evm_setNextBlockTimestamp", [
block.timestamp + 15
]);
await ethers.provider.send("evm_mine");
// 测试在时间戳被操纵的情况下的行为
});
});
7.3 DoS攻击测试
// contracts/VulnerableAuction.sol
contract VulnerableAuction {
address public highestBidder;
uint256 public highestBid;
function bid() public payable {
require(msg.value > highestBid);
// ❌ 漏洞: 如果前一个出价者的合约revert,整个拍卖就会DoS
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid); // 可能失败
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
// contracts/MaliciousBidder.sol
contract MaliciousBidder {
VulnerableAuction auction;
constructor(address _auction) {
auction = VulnerableAuction(_auction);
}
function attack() public payable {
auction.bid{value: msg.value}();
}
// ❌ 拒绝接收ETH,导致拍卖DoS
receive() external payable {
revert("I refuse to accept refund");
}
}
// test/DoS.test.js
describe("DoS Attack", function() {
it("Should demonstrate DoS vulnerability", async function() {
// 恶意出价者出价
await maliciousBidder.attack({value: ethers.parseEther("1")});
// 正常用户尝试出更高价,但会失败
await expect(
auction.connect(normalUser).bid({value: ethers.parseEther("2")})
).to.be.reverted; // 因为无法退款给恶意合约
console.log("拍卖被DoS攻击锁定");
});
});
7.3.1 安全修复:Pull Payment模式
// contracts/SecureAuction.sol
contract SecureAuction {
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public pendingReturns; // ✅ Pull模式
function bid() public payable {
require(msg.value > highestBid);
if (highestBidder != address(0)) {
// ✅ 不立即转账,而是记录待领取金额
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
// ✅ 用户主动领取退款
function withdrawRefund() public {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0);
pendingReturns[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
if (!success) {
pendingReturns[msg.sender] = amount; // 失败则恢复
}
}
}
8. 高级测试技术
8.1 Fuzzing测试 (模糊测试)
8.1.1 使用Foundry Fuzzing
// test/FuzzTest.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Token.sol";
contract FuzzTest is Test {
Token token;
function setUp() public {
token = new Token();
}
// Foundry会自动生成随机输入
function testFuzzTransfer(address to, uint256 amount) public {
// 设置假设条件
vm.assume(to != address(0));
vm.assume(amount <= type(uint256).max / 2);
token.mint(address(this), amount);
token.transfer(to, amount);
assertEq(token.balanceOf(to), amount);
}
// 测试边界条件
function testFuzzNoOverflow(uint256 a, uint256 b) public {
vm.assume(a <= type(uint256).max - b); // 防止溢出
uint256 result = token.add(a, b);
assertEq(result, a + b);
}
}
# 运行fuzzing测试
forge test --match-test testFuzz
# 增加测试次数
forge test --match-test testFuzz --fuzz-runs 10000
8.1.2 Echidna Fuzzing
安装Echidna:
docker pull trailofbits/eth-security-toolbox
Echidna配置:
# echidna.yaml
testMode: assertion
testLimit: 50000
timeout: 300
coverage: true
corpusDir: corpus
测试合约:
// contracts/EchidnaTest.sol
pragma solidity ^0.8.20;
import "./Token.sol";
contract EchidnaTest {
Token token;
constructor() {
token = new Token();
}
// Echidna会尝试让这个断言失败
function echidna_balance_never_negative() public view returns (bool) {
return token.balanceOf(address(this)) >= 0;
}
function echidna_total_supply_conservation() public view returns (bool) {
// 总供应量应该等于所有余额之和
return token.totalSupply() == token.balanceOf(address(this));
}
}
# 运行Echidna
echidna-test contracts/EchidnaTest.sol --config echidna.yaml
8.2 形式化验证
8.2.1 SMT Solver验证
// contracts/VerifiedContract.sol
pragma solidity ^0.8.20;
contract VerifiedContract {
uint256 public value;
// SMTChecker规范
/// @custom:smtchecker assert-verified
function increment() public {
uint256 oldValue = value;
value++;
// 形式化验证条件
assert(value == oldValue + 1);
assert(value > oldValue);
}
}
编译时启用SMTChecker:
// hardhat.config.js
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: { enabled: true },
modelChecker: {
engine: "chc", // 使用SMT solver
targets: ["assert", "underflow", "overflow"],
}
}
}
};
8.3 静态分析
8.3.1 Slither
# 安装
pip3 install slither-analyzer
# 运行分析
slither contracts/MyToken.sol
# 生成报告
slither contracts/MyToken.sol --json report.json
# 检查特定问题
slither contracts/MyToken.sol --detect reentrancy-eth
8.3.2 Mythril
# 安装
pip3 install mythril
# 分析合约
myth analyze contracts/MyToken.sol
# 深度分析
myth analyze contracts/MyToken.sol --execution-timeout 900
8.4 符号执行
# 使用Manticore进行符号执行
pip3 install manticore
# 分析合约
manticore contracts/MyToken.sol --contract MyToken
9. DeFi协议测试
9.1 AMM (自动做市商) 测试
// contracts/SimpleAMM.sol
pragma solidity ^0.8.20;
contract SimpleAMM {
uint256 public reserveA;
uint256 public reserveB;
function addLiquidity(uint256 amountA, uint256 amountB) public {
reserveA += amountA;
reserveB += amountB;
}
// x * y = k
function swap(uint256 amountIn, bool swapAForB) public returns (uint256) {
uint256 amountOut;
if (swapAForB) {
amountOut = (reserveB * amountIn) / (reserveA + amountIn);
reserveA += amountIn;
reserveB -= amountOut;
} else {
amountOut = (reserveA * amountIn) / (reserveB + amountIn);
reserveB += amountIn;
reserveA -= amountOut;
}
return amountOut;
}
function getSpotPrice() public view returns (uint256) {
return (reserveB * 1e18) / reserveA;
}
}
// test/AMM.test.js
describe("AMM Tests", function() {
let amm;
beforeEach(async function() {
const AMM = await ethers.getContractFactory("SimpleAMM");
amm = await AMM.deploy();
// 初始流动性: 1000 tokenA, 1000 tokenB
await amm.addLiquidity(1000, 1000);
});
it("Should calculate correct swap amount", async function() {
// 用100 tokenA换取tokenB
const amountOut = await amm.swap.staticCall(100, true);
// x * y = k
// (1000 + 100) * (1000 - amountOut) = 1000 * 1000
// amountOut ≈ 90.9
expect(amountOut).to.be.closeTo(90, 1);
});
it("Should maintain constant product", async function() {
const k = (await amm.reserveA()) * (await amm.reserveB());
await amm.swap(100, true);
const newK = (await amm.reserveA()) * (await amm.reserveB());
expect(newK).to.be.gte(k); // k只增不减 (因为手续费)
});
it("Should have correct price impact", async function() {
const priceBefore = await amm.getSpotPrice();
// 大额交易会造成价格滑点
await amm.swap(500, true);
const priceAfter = await amm.getSpotPrice();
// tokenB价格应该上升 (相对于tokenA)
expect(priceAfter).to.be.gt(priceBefore);
});
describe("Flash Loan Attack Protection", function() {
it("Should prevent price manipulation via flash loan", async function() {
// 模拟闪电贷攻击
const largeAmount = 10000;
// 攻击者借大量tokenA
const priceBefore = await amm.getSpotPrice();
// 大量买入tokenB,操纵价格
await amm.swap(largeAmount, true);
const priceManipulated = await amm.getSpotPrice();
// 在其他协议利用被操纵的价格
// 卖出tokenB,还原价格
const tokenBAmount = await amm.reserveB();
await amm.swap(tokenBAmount / 2, false);
const priceAfter = await amm.getSpotPrice();
// 验证是否有防护机制
// (实际应该使用TWAP或其他预言机)
});
});
});
9.2 借贷协议测试
describe("Lending Protocol", function() {
describe("Collateral Management", function() {
it("Should calculate correct health factor", async function() {
// 健康因子 = (抵押物价值 * 清算阈值) / 借款价值
const collateralValue = ethers.parseEther("100");
const borrowedValue = ethers.parseEther("50");
const liquidationThreshold = 80; // 80%
await lending.deposit(collateralValue);
await lending.borrow(borrowedValue);
const healthFactor = await lending.getHealthFactor(user.address);
// (100 * 0.8) / 50 = 1.6
expect(healthFactor).to.equal(ethers.parseEther("1.6"));
});
it("Should liquidate under-collateralized position", async function() {
await lending.deposit(ethers.parseEther("100"));
await lending.borrow(ethers.parseEther("80"));
// 模拟价格下跌
await oracle.setPrice(ethers.parseEther("0.5"));
// 健康因子 < 1,可以清算
const healthFactor = await lending.getHealthFactor(user.address);
expect(healthFactor).to.be.lt(ethers.parseEther("1"));
// 清算
await expect(
lending.connect(liquidator).liquidate(user.address)
).to.not.be.reverted;
});
});
});
10. NFT合约测试
10.1 ERC721测试
describe("NFT Contract", function() {
describe("Minting", function() {
it("Should mint NFT correctly", async function() {
await nft.mint(user.address, 1);
expect(await nft.ownerOf(1)).to.equal(user.address);
expect(await nft.balanceOf(user.address)).to.equal(1);
});
it("Should increment token ID", async function() {
await nft.mint(user1.address, 1);
await nft.mint(user2.address, 2);
expect(await nft.totalSupply()).to.equal(2);
});
it("Should revert double mint", async function() {
await nft.mint(user.address, 1);
await expect(
nft.mint(user.address, 1)
).to.be.revertedWith("Token already minted");
});
});
describe("Metadata", function() {
it("Should return correct token URI", async function() {
await nft.mint(user.address, 1);
const uri = await nft.tokenURI(1);
expect(uri).to.equal("https://api.example.com/metadata/1");
});
it("Should support ERC721Metadata interface", async function() {
// ERC165 interface check
const interfaceId = "0x5b5e139f"; // ERC721Metadata
expect(await nft.supportsInterface(interfaceId)).to.be.true;
});
});
describe("Royalties (ERC2981)", function() {
it("Should return correct royalty info", async function() {
await nft.mint(creator.address, 1);
const salePrice = ethers.parseEther("1");
const [receiver, royaltyAmount] = await nft.royaltyInfo(1, salePrice);
expect(receiver).to.equal(creator.address);
expect(royaltyAmount).to.equal(salePrice * 5n / 100n); // 5%
});
});
});
11. 测试最佳实践
11.1 测试组织结构
test/
├── unit/ # 单元测试
│ ├── Token.test.js
│ ├── Vault.test.js
│ └── Governance.test.js
├── integration/ # 集成测试
│ ├── TokenVault.test.js
│ └── FullProtocol.test.js
├── security/ # 安全测试
│ ├── Reentrancy.test.js
│ ├── AccessControl.test.js
│ └── FrontRunning.test.js
├── fuzzing/ # 模糊测试
│ └── TokenFuzz.t.sol
└── gas/ # Gas测试
└── GasOptimization.test.js
11.2 测试命名规范
describe("合约名", function() {
describe("功能模块", function() {
it("should [期望行为] when [条件]", async function() {
// 测试代码
});
it("should revert when [错误条件]", async function() {
// 测试错误情况
});
});
});
// 示例:
describe("Token", function() {
describe("Transfer", function() {
it("should transfer tokens when sender has sufficient balance", async function() {
// ...
});
it("should revert when sender has insufficient balance", async function() {
// ...
});
});
});
11.3 持续集成 (CI)
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Run tests
run: npx hardhat test
- name: Run coverage
run: npx hardhat coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage.json
- name: Run Slither
uses: crytic/slither-action@v0.3.0
with:
target: contracts/
12. 精通路线图
12.1 初级阶段 (1-2个月)
学习目标:
- ✅ 理解EVM基础原理
- ✅ 掌握Solidity语法
- ✅ 会写基础单元测试
- ✅ 理解常见漏洞
实战项目:
- ERC20代币合约 + 完整测试
- 简单的多签钱包
- NFT铸造合约
学习资源:
- Solidity官方文档
- CryptoZombies教程
- OpenZeppelin合约库
12.2 中级阶段 (3-6个月)
学习目标:
- ✅ 精通测试框架 (Hardhat/Foundry)
- ✅ 掌握集成测试
- ✅ 会使用Fuzzing
- ✅ 理解DeFi协议原理
实战项目:
- AMM DEX实现 + 测试
- 借贷协议
- Staking合约
- 复现历史攻击案例
学习资源:
- Damn Vulnerable DeFi挑战
- Ethernaut CTF
- Trail of Bits安全指南
12.3 高级阶段 (6-12个月)
学习目标:
- ✅ 掌握形式化验证
- ✅ 能进行安全审计
- ✅ 精通Gas优化
- ✅ 理解MEV和前端运行
实战项目:
- 完整的DeFi协议
- DAO治理系统
- 跨链桥
- 参与真实项目审计
学习资源:
- Secureum Bootcamp
- 阅读审计报告 (Trail of Bits, OpenZeppelin, etc.)
- Code4rena竞赛
12.4 专家阶段 (持续学习)
目标:
- 🎯 成为安全审计专家
- 🎯 发现0day漏洞
- 🎯 设计安全协议
- 🎯 贡献开源工具
13. 真实案例分析
13.1 The DAO攻击 (2016)
漏洞: 重入攻击
// 简化的漏洞代码
function withdraw(uint amount) {
if (balances[msg.sender] >= amount) {
// ❌ 先转账
msg.sender.call.value(amount)();
// ❌ 后更新状态
balances[msg.sender] -= amount;
}
}
损失: 360万ETH (~$60M)
教训:
- 总是遵循 Checks-Effects-Interactions 模式
- 使用 ReentrancyGuard
- 在状态更新前不要进行外部调用
13.2 Poly Network攻击 (2021)
漏洞: 权限控制缺陷
损失: $611M
教训:
- 严格的权限管理
- 多重签名
- 时间锁
13.3 测试这些漏洞
describe("Historical Attack Reproductions", function() {
describe("The DAO Attack", function() {
it("Should demonstrate reentrancy attack", async function() {
// 复现The DAO攻击逻辑
// ...
});
it("Should show how fix prevents attack", async function() {
// 测试修复后的版本
// ...
});
});
});
总结:精通智能合约测试的关键
✅ 核心知识
-
深入理解EVM
- 执行模型
- Gas机制
- 存储结构
-
掌握测试方法论
- 单元测试
- 集成测试
- 安全测试
- Fuzzing
-
熟悉工具生态
- Hardhat/Foundry
- Slither/Mythril
- Echidna
-
了解常见漏洞
- 重入攻击
- 整数溢出
- 访问控制
- 前端运行
✅ 实践路径
- 读代码: 阅读100+优秀合约
- 写测试: 为50+合约写完整测试
- 找漏洞: 完成所有CTF挑战
- 做审计: 参与真实项目审计
✅ 持续学习
- 📚 每周阅读审计报告
- 🔍 关注最新攻击事件
- 💻 参与开源项目
- 🏆 参加安全竞赛
附录:速查表
A. 常用测试命令
# Hardhat
npx hardhat test
npx hardhat test --grep "specific test"
npx hardhat coverage
REPORT_GAS=true npx hardhat test
# Foundry
forge test
forge test -vvv
forge test --match-test testFuzz
forge coverage
forge snapshot
# 静态分析
slither contracts/
myth analyze contracts/MyContract.sol
B. 断言速查
// Chai断言
expect(value).to.equal(expected);
expect(value).to.be.gt(other);
expect(value).to.be.lt(other);
expect(await promise).to.be.reverted;
expect(await promise).to.be.revertedWith("Error message");
expect(await promise).to.emit(contract, "Event");
// Foundry断言
assertEq(a, b);
assertGt(a, b);
assertLt(a, b);
vm.expectRevert();
vm.expectEmit(true, true, false, true);
C. 有用的资源
- 📖 Solidity文档
- 🔐 SWC Registry
- 🛡️ Smart Contract Security Best Practices
- 🎓 Secureum Bootcamp
- 💰 Code4rena
- 🏆 Damn Vulnerable DeFi
最后更新: 2026-03-14
作者: Smart Contract Security Team
版权: MIT License
看完这份文档,配合大量实践,你就能精通智能合约测试!
评论区