尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Solidity 模糊测试失败后:从最小反例定位合约边界

Solidity 模糊测试失败后:从最小反例定位合约边界 Solidity 模糊测试失败后从最小反例定位合约边界模糊测试失败的价值在于它给出了一条可缩减的输入。不要把随机失败包装成故障故事先固定种子缩小反例再检查不变量和权限边界。本文的输入仅用于说明复现流程。1. 证据链定位架构从 Trace 到攻击路径重构当 Foundry 或 Echidna 在长路径测试中触发Counterexample Found时测试引擎会返回一段冗长的 EVM 操作码跟踪日志。如果只看最终revert的位置往往会误判根因。需要构建一条完整的定位证据链状态前置条件捕获提取触发失败之前的全局存储状态Storage Slot。调用树递归还原Call Tree Tracking还原多合约之间的 Call/DelegateCall 序列。存储状态差异对比Storage Delta Engine定位哪一次指令写入违反了不变性断言Invariant Constraint。2. 工程代码漏洞场景与安全审计测试下面展示一个存在隐蔽“跨函数重入”风险的质押池合约以及用于捕获该问题的 Foundry 审计测试代码。2.1 缺陷合约与修复实现 (StakingVault.sol)// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import openzeppelin/contracts/utils/ReentrancyGuard.sol; import openzeppelin/contracts/token/ERC20/IERC20.sol; import openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol; /** * title 质押池合约 - 展示重入防护与状态校验 */ contract StakingVault is ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable stakingToken; mapping(address uint256) private _balances; uint256 private _totalStaked; event Deposited(address indexed user, uint256 amount); event Withdrawn(address indexed user, uint256 amount); event EmergencyLiquidated(address indexed user, uint256 amount); constructor(address _token) { require(_token ! address(0), INVALID_TOKEN); stakingToken IERC20(_token); } function balanceOf(address account) external view returns (uint256) { return _balances[account]; } function totalStaked() external view returns (uint256) { return _totalStaked; } /** * 存款操作遵循 CEI 模式 */ function deposit(uint256 amount) external nonReentrant { require(amount 0, ZERO_AMOUNT); // 1. Checks Effects _balances[msg.sender] amount; _totalStaked amount; // 2. Interactions stakingToken.safeTransferFrom(msg.sender, address(this), amount); emit Deposited(msg.sender, amount); } /** * 取款操作存在潜在漏洞的原始写法逻辑 (已修复版本) */ function withdraw(uint256 amount) external nonReentrant { require(amount 0, ZERO_AMOUNT); require(_balances[msg.sender] amount, INSUFFICIENT_BALANCE); // 严格遵循 CEI 模式先扣减状态再对外转账 _balances[msg.sender] - amount; _totalStaked - amount; stakingToken.safeTransfer(msg.sender, amount); emit Withdrawn(msg.sender, amount); } /** * 紧急清算接口 - 此接口若未加 nonReentrant 且未更新 _totalStaked会导致全局不变性崩溃 */ function emergencyLiquidate() external nonReentrant { uint256 userBalance _balances[msg.sender]; require(userBalance 0, NO_BALANCE); // 先清理状态 _balances[msg.sender] 0; _totalStaked - userBalance; // 转移代币 stakingToken.safeTransfer(msg.sender, userBalance); emit EmergencyLiquidated(msg.sender, userBalance); } }2.2 捕获隐蔽问题的 Foundry Invariant 测试套件 (AuditStakingVault.t.sol)普通的单元测试很难发现多个函数交替调用时的全局不变量损坏。以下是用 Foundry 编写的不变性模糊测试套件。// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import forge-std/Test.sol; import ../src/StakingVault.sol; import openzeppelin/contracts/token/ERC20/ERC20.sol; class MockERC20 is ERC20 { constructor() ERC20(Mock Token, MTK) { _mint(msg.sender, 10_000_000 * 10**18); } } contract VaultHandler is Test { StakingVault public vault; MockERC20 public token; uint256 public ghostTotalDeposits; constructor(StakingVault _vault, MockERC20 _token) { vault _vault; token _token; } function deposit(uint256 amount) public { amount bound(amount, 1, 100_000 * 10**18); token.mint(address(this), amount); token.approve(address(vault), amount); vault.deposit(amount); ghostTotalDeposits amount; } function withdraw(uint256 amount) public { uint256 userBal vault.balanceOf(address(this)); if (userBal 0) return; amount bound(amount, 1, userBal); vault.withdraw(amount); ghostTotalDeposits - amount; } } contract AuditStakingVaultTest is Test { StakingVault public vault; MockERC20 public token; VaultHandler public handler; function setUp() public { token new MockERC20(); vault new StakingVault(address(token)); handler new VaultHandler(vault, token); // 限制 Foundry 仅调用 Handler 的特定函数 targetContract(address(handler)); } /** * 核心审计不变性断言Vault 的代币余额必须始终等于内部 totalStaked */ function invariant_SolvencyAndStateConsistency() public view { uint256 actualTokenBalance token.balanceOf(address(vault)); uint256 reportedTotalStaked vault.totalStaked(); assertEq( actualTokenBalance, reportedTotalStaked, CRITICAL: Token balance does not match internal totalStaked! ); } }3. 从实验失败到防御演练的规则提炼安全审计需要基于可复现的证据定位漏洞。经历模糊测试失败后团队可将有效的检查方式整理为以下防护规则拒绝依靠经验主义做 Code Review跨函数的状态依赖关系在代码量超过 2000 行后人类视觉容易遗漏。必须依赖 Invariant Testing 强制定义系统“不可破坏的不变量”。严格审计 ERC20 钩子函数ERC-777 / ERC-1155许多代币在transfer时会自动回调接收方的tokensReceived接口。如果合约未配置nonReentrant或未严格执行 Checks-Effects-Interactions即便核心逻辑看起来再完整攻击者依然可以插队执行。保留 VM Trace 证据不要在采集前重置节点。cast run tx_hash --debug可逐指令查看 EVM 状态还应结合合约源码、编译产物、交易回执和节点版本交叉验证。一次失败的实验只要能提炼出完整的虚拟机证据链其价值远大于十次顺利通过的 Demo 部署。
返回列表