本文深入探讨了无状态模糊测试和有状态模糊测试在智能合约安全中的作用。无状态模糊测试将每个测试案例视为独立事件,适用于快速发现输入验证和简单功能中的漏洞。有状态模糊测试则追踪系统状态,模拟真实攻击行为,擅长检测多步骤交互和状态依赖性漏洞,如重入攻击和经济漏洞。文章还介绍了混合模糊测试策略、工具和实践,强调了在DeFi安全中综合运用两种方法的重要性。
在 领英 上关注我,获取更多区块链开发内容。
想象一下,你部署了一个 DeFi 协议,它通过了所有单独的功能测试,却眼睁睁地看着黑客通过一系列看似无害的复杂交易窃取了数百万美元。这种噩梦般的场景突显了无状态模糊测试和有状态模糊测试之间的关键区别——这种区别已经让区块链行业损失了超过 38 亿美元的漏洞利用。在分析了数十起主要的 DeFi 黑客事件,并在多个协议中实施了这两种模糊测试方法之后,我发现理解这两种测试方法不仅仅是重要的,对于智能合约的安全性来说,更是绝对关键的。🚀 这份全面的指南将使你掌握选择正确的模糊测试策略的知识,并保护你的协议免受复杂的、依赖状态的攻击。🛡️
模糊测试是安全研究人员武器库中最强大的武器之一。其核心在于,模糊测试涉及用随机、半随机或变异的输入来轰炸系统,以发现隐藏的漏洞、崩溃和意外行为,而这些问题是传统测试可能错过的。
是什么让模糊测试如此有效:
🎯 探索边缘情况:发现开发者从未考虑过的场景
⚡ 自动化发现:无需人工干预即可发现 Bug
🔄 高覆盖率:快速测试数千种输入组合
💥 真实世界模拟:模仿攻击者的行为模式
区块链行业已经接受了模糊测试,因为智能合约在对抗性环境中运行,任何可利用的 Bug 都可能导致直接的经济损失。与传统软件中 Bug 可能导致崩溃或数据损坏不同,智能合约漏洞会直接转化为被盗资金。
无状态模糊测试将每个测试用例视为完全独立的事件。每个输入都是独立生成和执行的,系统在每次测试运行之间都重置为干净状态。
// 示例:简单 Vault 的无状态模糊测试
contract SimpleVault {
mapping(address => uint256) public balances;
function deposit() external payable {
require(msg.value > 0, "Must deposit something");
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
无状态模糊测试示例:
// 使用随机输入进行无状态模糊测试
const { ethers } = require("hardhat");
describe("Stateless Vault Fuzzing", function() {
let vault;
beforeEach(async function() {
// 每次测试都部署新的合约
const Vault = await ethers.getContractFactory("SimpleVault");
vault = await Vault.deploy();
});
it("should fuzz deposit function", async function() {
for (let i = 0; i < 1000; i++) {
// 生成随机存款金额
const randomAmount = Math.floor(Math.random() * 1000000);
try {
await vault.deposit({ value: randomAmount });
console.log(`✅ Deposit succeeded: ${randomAmount}`);
} catch (error) {
console.log(`❌ Deposit failed: ${error.message}`);
}
}
});
it("should fuzz withdraw function", async function() {
// 预先为账户充值
await vault.deposit({ value: ethers.utils.parseEther("10") });
for (let i = 0; i < 1000; i++) {
const randomAmount = Math.floor(Math.random() * 1000000);
try {
await vault.withdraw(randomAmount);
console.log(`✅ Withdrawal succeeded: ${randomAmount}`);
} catch (error) {
console.log(`❌ Withdrawal failed: ${error.message}`);
}
}
});
});
✅ 无状态模糊测试的优点:
🚀 简单实现:设置和运行都很简单
⚡ 快速执行:没有状态跟踪开销
🎯 输入验证重点:非常适合查找基本验证错误
🔄 高吞吐量:可以快速执行数千次测试
📊 易于分析:清晰的因果关系
真实的无状态成功案例:
// 通过无状态模糊测试发现的Bug
function processPayment(uint256 amount, address recipient) external {
require(amount > 0, "Amount must be positive");
// BUG:没有检查零地址!
require(recipient != address(0), "Invalid recipient");
// Transfer logic...
}
无状态模糊测试快速发现将 address(0)
作为接收者传递会导致意外行为,从而在部署前进行了关键修复。
❌ 无状态模糊测试的缺点:
🚫 没有状态演变:无法测试复杂的交互序列
🔍 浅层 Bug 检测:遗漏了深层的、基于逻辑的漏洞
⚠️ 有限的真实场景:无法模拟实际的用户工作流程
🎭 缺少攻击向量:无法检测重入或状态操纵攻击
无状态模糊测试遗漏的关键漏洞:
// 需要有状态测试的重入漏洞
contract VulnerableBank {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
// 漏洞:状态更新之前的外部调用
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // 外部调用后更新状态
}
}
无状态模糊测试会单独测试 withdraw
函数,但永远不会发现重入漏洞,该漏洞需要恶意合约在执行期间回调 withdraw
。
有状态模糊测试将系统视为状态机,跨多个交互跟踪内部状态,并根据先前的系统响应和当前状态条件生成输入。
// 需要有状态测试的复杂 DeFi 协议
contract LendingProtocol {
mapping(address => uint256) public deposits;
mapping(address => uint256) public borrows;
uint256 public totalDeposits;
uint256 public totalBorrows;
uint256 public constant COLLATERAL_RATIO = 150; // 150%
function deposit() external payable {
deposits[msg.sender] += msg.value;
totalDeposits += msg.value;
}
function borrow(uint256 amount) external {
uint256 maxBorrow = (deposits[msg.sender] * 100) / COLLATERAL_RATIO;
require(borrows[msg.sender] + amount <= maxBorrow, "Insufficient collateral");
borrows[msg.sender] += amount;
totalBorrows += amount;
payable(msg.sender).transfer(amount);
}
function repay() external payable {
require(borrows[msg.sender] >= msg.value, "Repaying too much");
borrows[msg.sender] -= msg.value;
totalBorrows -= msg.value;
}
function liquidate(address user) external {
uint256 requiredCollateral = (borrows[user] * COLLATERAL_RATIO) / 100;
require(deposits[user] < requiredCollateral, "User not liquidatable");
// Liquidation logic...
}
}
有状态模糊测试的实现:
// 使用状态跟踪进行有状态模糊测试
describe("Stateful Lending Protocol Fuzzing", function() {
let protocol;
let users;
let protocolState;
before(async function() {
const Protocol = await ethers.getContractFactory("LendingProtocol");
protocol = await Protocol.deploy();
users = await ethers.getSigners();
// 初始化状态跟踪
protocolState = {
userDeposits: new Map(),
userBorrows: new Map(),
totalDeposits: 0,
totalBorrows: 0
};
});
it("should perform stateful fuzzing across multiple interactions", async function() {
for (let i = 0; i < 10000; i++) {
const action = selectNextAction(protocolState);
const user = selectRandomUser(users);
try {
await executeAction(action, user, protocolState);
updateStateTracking(action, user, protocolState);
validateInvariants(protocolState);
} catch (error) {
console.log(`🔍 Potential bug found: ${error.message}`);
logSystemState(protocolState);
}
}
});
function selectNextAction(state) {
const actions = ['deposit', 'borrow', 'repay', 'liquidate'];
const weights = calculateActionWeights(state);
return weightedRandomSelection(actions, weights);
}
function calculateActionWeights(state) {
// 基于当前状态的智能操作选择
return {
deposit: state.totalDeposits < 1000 ? 0.4 : 0.2,
borrow: state.totalDeposits > 0 ? 0.3 : 0.1,
repay: state.totalBorrows > 0 ? 0.2 : 0.1,
liquidate: hasLiquidatableUsers(state) ? 0.3 : 0.1
};
}
async function executeAction(action, user, state) {
switch (action) {
case 'deposit':
const depositAmount = generateRandomAmount(0.1, 10);
await protocol.connect(user).deposit({
value: ethers.utils.parseEther(depositAmount.toString())
});
break;
case 'borrow':
const maxBorrow = calculateMaxBorrow(user, state);
if (maxBorrow > 0) {
const borrowAmount = generateRandomAmount(0.01, maxBorrow);
await protocol.connect(user).borrow(
ethers.utils.parseEther(borrowAmount.toString())
);
}
break;
case 'repay':
const userBorrow = state.userBorrows.get(user.address) || 0;
if (userBorrow > 0) {
const repayAmount = generateRandomAmount(0.01, userBorrow);
await protocol.connect(user).repay({
value: ethers.utils.parseEther(repayAmount.toString())
});
}
break;
case 'liquidate':
const liquidatableUser = findLiquidatableUser(state);
if (liquidatableUser) {
await protocol.connect(user).liquidate(liquidatableUser);
}
break;
}
}
function validateInvariants(state) {
// 必须保持的关键协议不变量
assert(state.totalDeposits >= state.totalBorrows, "Protocol insolvent!");
// 检查个人用户的不变量
for (let [user, deposits] of state.userDeposits) {
const borrows = state.userBorrows.get(user) || 0;
const requiredCollateral = (borrows * 150) / 100;
if (borrows > 0) {
assert(deposits >= requiredCollateral, `User ${user} under-collateralized!`);
}
}
}
});
✅ 有状态模糊测试的优点:
🔍 复杂 Bug 检测:发现复杂的、多步骤的漏洞
🎭 真实攻击模拟:模拟实际的攻击者行为模式
📈 状态演变测试:验证协议在状态变化过程中的行为
🛡️ 不变量检查:确保关键的系统属性始终保持不变
🔄 交互覆盖:测试真实的用户工作流程序列
真实的、有状态的成功案例:
// 通过有状态模糊测试发现的闪电贷攻击
contract FlashLoanExploit {
function executeExploit(address target) external {
// 步骤 1:获取闪电贷
uint256 loanAmount = 1000000 ether;
flashLoan(loanAmount);
}
function onFlashLoan(uint256 amount) external {
// 步骤 2:操纵协议状态
LendingProtocol(target).deposit{value: amount}();
// 步骤 3:利用状态不一致性
LendingProtocol(target).borrow(amount * 2); // 过度借贷!
// 步骤 4:偿还闪电贷
repayFlashLoan(amount);
// 步骤 5:从漏洞中获利
// 攻击者保留额外借入的资金
}
}
只有有状态模糊测试才能通过模拟操纵协议状态所需的复杂交互序列来发现这种多步骤的攻击。
❌ 有状态模糊测试的缺点:
🐌 计算开销:需要大量的处理能力和时间
🧩 复杂实现:设置和维护更加困难
📊 状态爆炸:跟踪所有可能的状态变得笨拙
🔍 难以调试:更难隔离失败的根本原因
⚖️ 资源密集型:消耗更多的内存和存储
输入验证测试:
function processAmount(uint256 amount) external {
require(amount > 0 && amount <= MAX_AMOUNT, "Invalid amount");
// 简单的处理逻辑...
}
单函数安全性:
function calculateInterest(uint256 principal, uint256 rate) external pure returns (uint256) {
require(rate <= 10000, "Rate too high"); // 100% max
return (principal * rate) / 10000;
}
基本访问控制:
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
DeFi 协议测试:
// 需要有状态测试的复杂交互
function swapAndStake(address tokenA, address tokenB, uint256 amount) external {
// 需要状态跟踪的多步骤操作
uint256 swapped = dex.swap(tokenA, tokenB, amount);
stakingPool.stake(tokenB, swapped);
updateUserRewards(msg.sender);
}
重入漏洞检测:
function criticalOperation() external nonReentrant {
// 必须使用复杂调用序列进行测试的操作
externalContract.riskyCall();
updateCriticalState();
}
经济漏洞预防:
function liquidationMechanism(address user) external {
// 需要多步骤测试的复杂经济逻辑
uint256 collateralValue = getCollateralValue(user);
uint256 debtValue = getDebtValue(user);
if (collateralValue < debtValue * LIQUIDATION_THRESHOLD) {
performLiquidation(user);
}
}
// 混合模糊测试策略
class HybridFuzzer {
constructor(contract) {
this.contract = contract;
this.statelessPhase = true;
this.statefulPhase = false;
this.stateHistory = [];
}
async runHybridFuzzing() {
// 阶段 1:无状态模糊测试以快速取胜
console.log("🎯 Starting stateless phase...");
const statelessBugs = await this.runStatelessPhase();
// 阶段 2:有状态模糊测试以发现深层 Bug
console.log("🔄 Starting stateful phase...");
const statefulBugs = await this.runStatefulPhase();
return {
statelessBugs,
statefulBugs,
totalBugs: statelessBugs.length + statefulBugs.length
};
}
async runStatelessPhase() {
const bugs = [];
for (let i = 0; i < 5000; i++) {
const randomInput = this.generateRandomInput();
try {
await this.executeIsolatedTest(randomInput);
} catch (error) {
bugs.push({
type: 'stateless',
input: randomInput,
error: error.message
});
}
}
return bugs;
}
async runStatefulPhase() {
const bugs = [];
let currentState = this.initializeState();
for (let i = 0; i < 10000; i++) {
const action = this.selectStateBasedAction(currentState);
try {
const result = await this.executeStatefulTest(action, currentState);
currentState = this.updateState(currentState, result);
this.validateInvariants(currentState);
} catch (error) {
bugs.push({
type: 'stateful',
sequence: this.stateHistory.slice(-10), // 最后 10 个操作
currentState: JSON.stringify(currentState),
error: error.message
});
}
}
return bugs;
}
}
// 高级基于属性的模糊测试
const fc = require('fast-check');
describe("Property-Based Fuzzing", function() {
it("should maintain invariants across all operations", async function() {
await fc.assert(
fc.asyncProperty(
fc.array(fc.record({
action: fc.constantFrom('deposit', 'withdraw', 'transfer'),
amount: fc.integer(1, 1000),
user: fc.constantFrom(...users)
}), 10, 100),
async (actionSequence) => {
// 执行一系列操作
let totalBalance = 0;
for (const action of actionSequence) {
await executeAction(action);
totalBalance = await getTotalBalance();
// 属性:总余额永远不应为负数
expect(totalBalance).to.be.gte(0);
// 属性:个人余额永远不应超过总额
const userBalance = await getUserBalance(action.user);
expect(userBalance).to.be.lte(totalBalance);
}
}
),
{ numRuns: 1000 }
);
});
});
2016 年臭名昭著的 DAO 黑客事件表明了为什么有状态模糊测试对于智能合约的安全性至关重要:
// 简化的 DAO 漏洞
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
// 漏洞:状态更新之前的外部调用
(bool success,) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // 太迟了!
}
为什么无状态模糊测试失败:
withdraw
调用看起来是安全的有状态模糊测试本可以如何捕获它:
// 可以捕获重入的有状态测试
it("should detect reentrancy vulnerability", async function() {
const maliciousContract = await deployMaliciousContract();
// 步骤 1:存入资金
await dao.connect(maliciousContract).deposit({ value: ether("10") });
// 步骤 2:发起提款(触发重入)
await expect(
dao.connect(maliciousContract).withdraw(ether("10"))
).to.revert(); // 由于 Bug,应该 revert 但没有
});
现代 DeFi 协议面临着复杂的攻击,这些攻击会跨多个协议操纵状态:
// 多协议闪电贷攻击
contract ComplexExploit {
function executeMultiStepExploit() external {
// 步骤 1:闪电贷 10M 代币
flashLoan(10_000_000e18);
}
function onFlashLoan(uint256 amount) external {
// 步骤 2:存入协议 A
protocolA.deposit(amount);
// 步骤 3:使用抵押品从协议 B 借款
protocolB.borrow(amount * 2); // 由于 Oracle 操纵而过度借款
// 步骤 4:通过 DEX 交易操纵 Oracle
dex.massiveTrade(protocolB.governanceToken(), amount / 2);
// 步骤 5:以操纵的价格清算头寸
protocolB.liquidate(targetVictim);
// 步骤 6:获利并偿还闪电贷
repayFlashLoan(amount);
}
}
只有具有跨协议状态跟踪的有状态模糊测试才能发现如此复杂的利用路径。
## echidna.yaml configuration
testMode: "property"
testLimit: 50000
seqLen: 100
deployer: "0x30000"
sender: ["0x10000", "0x20000", "0x30000"]
psender: "0x10000"
coverage: true
corpusDir: "corpus"
// Echidna 属性测试
contract PropertyTests {
MyProtocol internal protocol;
constructor() {
protocol = new MyProtocol();
}
// 属性:总供应量永远不应意外减少
function echidna_supply_never_decreases() public view returns (bool) {
return protocol.totalSupply() >= protocol.previousTotalSupply();
}
// 属性:用户余额永远不应超过总供应量
function echidna_balance_bounded() public view returns (bool) {
return protocol.balanceOf(msg.sender) <= protocol.totalSupply();
}
}
// Foundry 模糊测试
contract ProtocolFuzzTest is Test {
MyProtocol protocol;
function setUp() public {
protocol = new MyProtocol();
}
function testFuzz_deposit(uint256 amount) public {
vm.assume(amount > 0 && amount <= type(uint128).max);
uint256 balanceBefore = protocol.totalDeposits();
protocol.deposit{value: amount}();
uint256 balanceAfter = protocol.totalDeposits();
assertEq(balanceAfter, balanceBefore + amount);
}
// 具有不变量的有状态模糊测试
function invariant_totalSupplyMatchesBalances() public {
uint256 totalSupply = protocol.totalSupply();
uint256 sumOfBalances = 0;
address[] memory users = protocol.getAllUsers();
for (uint i = 0; i < users.length; i++) {
sumOfBalances += protocol.balanceOf(users[i]);
}
assertEq(totalSupply, sumOfBalances);
}
}
// 进展策略
class FuzzingStrategy {
async implementProgression() {
// 级别 1:基本输入验证
await this.runBasicStatelessFuzzing();
// 级别 2:函数级别的安全性
await this.runAdvancedStatelessFuzzing();
// 级别 3:简单的状态转换
await this.runLimitedStatefulFuzzing();
// 级别 4:复杂的协议交互
await this.runFullStatefulFuzzing();
// 级别 5:跨协议模糊测试
await this.runEcosystemFuzzing();
}
}
// 关键协议不变量
contract ProtocolInvariants {
// 经济不变量
function invariant_protocolSolvent() external view returns (bool) {
return totalAssets() >= totalLiabilities();
}
// 安全不变量
function invariant_noUnauthorizedMinting() external view returns (bool) {
return totalSupply() <= maxAllowedSupply();
}
// 功能不变量
function invariant_balancesSum() external view returns (bool) {
return sumOfAllBalances() == totalSupply();
}
}
// 高级日志记录系统
class FuzzingLogger {
constructor() {
this.logs = [];
this.bugDatabase = new Map();
}
logExecution(test) {
this.logs.push({
timestamp: Date.now(),
testType: test.type,
inputs: test.inputs,
gasUsed: test.gasUsed,
stateHash: test.stateHash,
result: test.result
});
}
analyzePatternsAndGenerateReport() {
// 用于更好测试生成的模式分析
const patterns = this.identifyFailurePatterns();
const coverage = this.calculateCodeCoverage();
const performance = this.analyzePerformanceMetrics();
return {
patterns,
coverage,
performance,
recommendations: this.generateRecommendations()
};
}
}
无状态模糊测试覆盖率:
有状态模糊测试覆盖率:
无状态模糊测试资源:
有状态模糊测试资源:
// 完整的模糊测试框架
class UltimateFuzzingFramework {
constructor(protocol) {
this.protocol = protocol;
this.statelessEngine = new StatelessFuzzingEngine();
this.statefulEngine = new StatefulFuzzingEngine();
this.reporter = new SecurityReporter();
}
async executeComprehensiveFuzzing() {
console.log("🚀 Starting comprehensive fuzzing campaign...");
// 阶段 1:通过无状态模糊测试快速取胜
const quickBugs = await this.statelessEngine.run({
iterations: 10000,
timeout: 30 * 60, // 30 分钟
functions: this.protocol.getAllFunctions()
});
console.log(`⚡ Stateless phase complete: ${quickBugs.length} bugs found`);
// 阶段 2:通过有状态模糊测试进行深入分析
const deepBugs = await this.statefulEngine.run({
iterations: 50000,
timeout: 6 * 60 * 60, // 6 小时
stateDepth: 100,
invariants: this.protocol.getInvariants()
});
console.log(`🔍 Stateful phase complete: ${deepBugs.length} bugs found`);
// 阶段 3:生成全面报告
return this.reporter.generateReport({
statelessResults: quickBugs,
statefulResults: deepBugs,
recommendations: this.generateRecommendations(),
coverage: this.calculateTotalCoverage()
});
}
generateRecommendations() {
return {
criticalFixes: this.identifyCriticalIssues(),
securityEnhancements: this.suggestSecurityImprovements(),
testingStrategy: this.recommendTestingApproach(),
monitoringSetup: this.proposeMonitoringSystem()
};
}
}
无状态和有状态模糊测试之间的斗争不是选择赢家,而是战略性地使用这两种武器:
✅ 无状态模糊测试精通:
✅ 有状态模糊测试卓越:
✅ 混合方法至上:
- 原文链接: coinsbench.com/stateful-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!