ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

合约评审怎样提前发现风险

合约评审怎样提前发现风险 合约评审怎样提前发现风险在 Solidity 代码评审Code Review现场最容易让人放下戒备的往往是那些排版整齐、跑通了基本单元测试的代码。大部分工程团队都配备了 Slither、Mythril 等自动化静态扫描工具跑一遍 CI 流程拿出一份全是 Green Checkmark 的报告就算完成了第一轮审计。但现实往往很残酷。近几年被黑客卷走千万美元的 DeFi 协议绝大部分在上线前都通过了自动化工具扫描。静态工具擅长捕捉重入攻击的简单形式、语法过时或未使用的变量但面对业务逻辑漏洞、跨函数状态错位、隐性重入以及存储冲突自动化工具常常束手无策。真正的隐性风险必须依赖审计者在评审时建立一套严密的工程思维与破坏性视角。自动化扫描盲区里的隐形炸弹审计智能合约不能只顺着开发者的思路读代码而要时刻盘算“怎样用最怪异的操作打破合约的状态平衡”。在审查代码时有三个极易被忽视的隐形风险点。1. 跨函数与跨合约重入Cross-Function / Cross-Contract Reentrancy普通的单函数重入大家都有防备加个nonReentrant修饰符看似万事大吉。但在复杂协议中攻击者通过转账回调如onERC721Received或原生的 Ether 接收函数调用的不是提现函数本身而是另一个依赖该提现函数中间状态的辅助合约比如借贷算力的计价合约。此时单函数的重入锁形同虚设跨合约的状态读取差错直接导致资金被空套。2. 存储槽重叠与升级覆盖Storage Collision采用可升级代理Upgradeable Proxy架构的项目最怕开发者在更新 Implementation 合约时修改了继承顺序或在中间插入了新变量。这种错误在编译期不会报错在简单测试中也不一定能暴露但一旦部署上链代理合约读取的地址或数值就会与实际意图偏差万丈引发灾难性资产锁定。3. 精度截断与计算顺序颠倒Solidity 缺乏浮点数支持所有除法都会向下取整。开发者写(a * b) / c还是a * (b / c)在数学上等价在链上逻辑里却天差地别。如果乘法放在除法后面很可能在中间步骤就被截断为零。攻击者常利用微小数额高频调用让合约在计算份额或利息时不断遭遇精度丢失最终积少成多吃干抹净资金池。工程级安全防范代码实践要从根本上消除跨函数重入与状态未及时更新的隐患除了遵守 Checks-Effects-Interactions检查-效果-交互模式外在 Solidity 0.8.24 及更高版本中可以利用 EIP-1153 提供的暂态存储Transient Storage来构建开销更低、控制粒度更细的全局状态保护门禁。下面的工程示例代码演示了如何构建一个兼顾临时状态锁定与只读重入Read-Only Reentrancy防护的安全合约基类。// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /** * title AdvancedSecurityGuard * notice 防范只读重入与跨函数重入的高级安全基类 */ abstract contract AdvancedSecurityGuard { // 使用 EIP-1153 暂态存储槽地址通过 keccak256 计算避免冲突 bytes32 private constant GLOBAL_LOCK_SLOT keccak256(security.guard.global.lock); bytes32 private constant READ_ONLY_LOCK_SLOT keccak256(security.guard.readonly.lock); error ReentrancyGuardReentrantCall(); error ReadOnlyReentrancyDetected(); modifier nonReentrantGlobal() { bytes32 slot GLOBAL_LOCK_SLOT; uint256 locked; assembly { locked : tload(slot) } if (locked 1) { revert ReentrancyGuardReentrantCall(); } assembly { tstore(slot, 1) tstore(READ_ONLY_LOCK_SLOT, 1) // 同时开启只读重入防线 } _; assembly { tstore(slot, 0) tstore(READ_ONLY_LOCK_SLOT, 0) } } /** * notice 给外部只读查询接口使用的校验修饰符 * dev 如果有任何写操作正在进行且未落盘禁止外部进行关键计价查询 */ modifier checkReadOnlyState() { bytes32 slot READ_ONLY_LOCK_SLOT; uint256 readLocked; assembly { readLocked : tload(slot) } if (readLocked 1) { revert ReadOnlyReentrancyDetected(); } _; } } contract SecureVault is AdvancedSecurityGuard { mapping(address uint256) private _balances; bool private _isInitialized; event Withdrawn(address indexed user, uint256 amount); function deposit() external payable { require(msg.value 0, Zero deposit); _balances[msg.sender] msg.value; } /** * notice 提现函数遵循 CEI 模式并开启暂态全局重入锁 */ function withdraw(uint256 amount) external nonReentrantGlobal { uint256 userBalance _balances[msg.sender]; require(userBalance amount, Insufficient balance); // 1. Checks Effects 先更新状态 unchecked { _balances[msg.sender] userBalance - amount; } // 2. Interactions 外部交互 (bool success, ) msg.sender.call{value: amount}(); require(success, ETH transfer failed); emit Withdrawn(msg.sender, amount); } /** * notice 计价或份额查询接口使用 checkReadOnlyState 杜绝只读重入攻击 */ function getExchangeRate() external view checkReadOnlyState returns (uint256) { // 假设这里依赖合约内的 ETH 总余额与份额比率 uint256 totalVaultEth address(this).balance; if (totalVaultEth 0) return 1e18; return totalVaultEth; } function balanceOf(address account) external view returns (uint256) { return _balances[account]; } }评审现场实战 Checklist在审阅团队提交的 Pull Request 时不建议凭感觉一行行看必须对照硬性 Checkbox 逐一过筛状态变更顺序检查所有变量更新是否 完整 发生在call、delegatecall或外部代币转账之前只读接口安全性对外暴露的getRate、getPrice等 View 函数在有外部交互发生期间是否有可能被第三方合约调用并拿到中间错误数据代币兼容性盲点代码是否默认所有 ERC20 转账失败都会revert是否处理了 USDT 这类无返回值代币或者收取手续费Fee-on-transfer的特殊代币控制权与特权函数带有onlyOwner或多签限制的函数其关键参数如修改预言机地址、设置手续费率是否有合理的时间锁Timelock和上限范围约束Solidity 编译器版本与特性是否使用了固定版本如pragma solidity 0.8.25;而非浮动版本编译配置中的via-ir选项是否在 Foundry 测试和主网部署中保持一致审智能合约代码就像在雷区散步稍微打个盹就会踩爆隐藏的陷阱。自动化工具只能帮你清扫地表上的杂草真正决定项目生死存亡的永远是评审现场对每一行指令、每一次外部调用背后逻辑严密度的苛刻考究。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表