智能合约漏洞通常不是盯出小数点:用不变量和反例把账本打穿

4次阅读
没有评论

很多 DeFi 事件最后会被概括成“舍入错了几个 wei”“小数精度有问题”。发现者通常不是靠肉眼盯着小数位碰运气。更常见的路径是:

先定义协议永远不能违反的业务规则
        ↓
让工具生成大量极端金额和调用顺序
        ↓
找到能够破坏规则的最小反例
        ↓
分析误差能否重复、放大并转化为真实资产
        ↓
只在本地分叉、测试网或授权环境中验证

真正要找的不是“哪里少了一个小数”,而是**哪个边界状态会让协议的账不再守恒**。

本文只做公开安全方法科普,不提供针对真实协议的攻击参数,也不鼓励未经授权的链上探测。

为什么 DeFi 特别容易出现精度问题

EVM 主要用整数运算。代币显示的“小数”本质仍是整数单位:

1 ETH  = 10^18 wei
1 USDC = 10^6 个最小单位

协议又要在资产、份额、价格、利率和不同 decimals 之间反复换算,于是常见:

  • 整数除法直接丢弃余数
  • 向上/向下舍入方向选错
  • 先乘后除可能溢出,先除后乘损失精度
  • 多种代币 decimals 不一致
  • 单次 1 wei 误差被循环调用累积
  • 份额价格被误差改变后,下一次继续基于错误价格
  • 总供应量归零,但缓存、虚拟余额或累计值未同步重置

所以有些事件表面是小数问题,根因可能是会计模型、状态生命周期或旧版整数安全机制。

第一步:先写出必须守住的不变量

不变量是无论谁调用、输入多极端、顺序多奇怪,都必须成立的业务规则。常见包括:

  1. **资产守恒**:没有收益来源时,协议不能凭空增加可提取资产。
  2. **偿付能力**:真实持有资产不能低于对用户承诺的可赎回价值。
  3. **份额一致性**:用户份额、总供应量、内部缓存和真实余额必须对应。
  4. **往返不获利**:立即“存入—赎回”或“A—B—A”后,不应仅靠舍入获得额外价值。
  5. **误差有界**:多次调用的误差必须有设计上限,不能单向累积。
  6. **生命周期完整**:池创建、首次存款、清空、供应量归零和重新初始化时,派生状态同步变化。

最简单的检查可以是:

初始资产 = 100
存入 → 获得份额 → 立即赎回
最终资产必须 ≤ 100

若工具找到最终资产为 101 的输入,即使只多一个最小单位,也已证明存在反例。接着再问:差额能否循环、扩大或影响份额价格。

第二步:优先看这些代码位置

  • assetsshares 的双向换算
  • mulDivceilDivroundUproundDown
  • 固定精度常量与 decimals 归一化
  • 先乘后除、先除后乘、连续除法
  • Solidity 0.8 以前的算术、unchecked
  • 总供应量为零时的特殊分支
  • 首次存款、最后一次提款、池余额接近零
  • 缓存、虚拟余额、累计器、价格快照的创建与清理
  • 多个公开函数共享同一组会计状态的地方

静态分析能定位危险语法,但“存在除法”不等于漏洞。最终仍要回到业务不变量,证明公开操作能让账本失真。

第三步:专门生成反常输入和调用顺序

普通功能测试常用 10、100、1000;安全测试更应覆盖:

0
1 wei
2 wei
舍入边界两侧
最大整数附近
池子只剩极小余额
总供应量刚好归零
首次存款和最后一次提款
相同操作连续多次
多个公开函数交替调用
不同 decimals 的代币组合

很多问题出在组合状态,而不是单个函数,例如:

存款 → 兑换 → 提款
清空池子 → 再次初始化
捐赠资产 → 铸造份额 → 赎回
多次小额兑换 → 一次大额赎回

第四步:把失败序列缩成最小反例

模糊测试和不变量测试会生成大量金额、账户和调用顺序。概念上类似:

随机执行若干次公开的存款、兑换和提款

每轮结束后检查:
协议真实余额 ≥ 用户可赎回总额
用户净资产没有无来源增长
份额总价值与真实资产差额处于允许范围

一旦失败,工具往往还能把上百步压成几步,例如:

存入 17 个最小单位
执行两次兑换
赎回得到 18 个最小单位

这个最小反例才是人工分析的起点:沿着 trace 看哪一次除法、状态写入或缓存读取首次打破不变量。

第五步:1 wei 差额不等于真实可利用漏洞

还要回答:

  • 差额能否重复,还是只能发生一次
  • Gas、手续费、滑点是否高于收益
  • 本金变大时误差是否同步变大
  • 是否改变份额价格,使后续偏差加速
  • 能否在一笔原子交易中完成
  • 闪电贷只提供临时本金时,归还后是否仍有净差额
  • 是否有单笔限额、资金流速限制或自动暂停
  • 实际池余额与市场流动性是否足以转化理论差额

若每轮只赚 1 wei 且成本更高,更像低影响缺陷;若 1 wei 改变价格或份额分母,风险完全不同。

第六步:在隔离环境复现

单元测试用的是简化状态。真实协议还受代币精度、池余额、外部协议和历史状态影响。更稳妥的是主网分叉:

复制某个历史区块的合约代码与状态
        ↓
在本机启动临时链
        ↓
只向本地节点发送模拟交易
        ↓
对比交易前后的余额、份额和内部状态

主网分叉不改真实链、不动真实用户资金,适合验证公开事件和回归修复。**未经授权,不要向真实协议发送“确认漏洞”的探测交易。**

公开事件里常见的研究入口(复盘视角)

| 案例 | 合理审计入口 | 要验证的不变量 |

| — | — | — |

| Bunni | 提款舍入方向、极小池余额、多次流动性调整 | 提取价值不能超过被销毁流动性对应价值 |

| Balancer V2 | 批量兑换、固定精度换算、极小余额与误差累计 | 循环兑换不能在无市场收益时持续抽干池子 |

| Yearn yETH | 总供应量归零后的缓存与首次铸造分支 | 归零后旧虚拟余额不能参与新一轮铸造 |

| Truebit | 旧版大数乘法与联合曲线 | 接近零成本时不能铸造巨额市值代币 |

这是从公开复盘反推的合理入口,不代表首名发现者完整私有流程。

开发团队可用的检查顺序

  1. 画出真实资产、内部余额、份额供应、价格和缓存关系。
  2. 为守恒、偿付、往返不获利和误差上限写可执行不变量。
  3. 覆盖 0、1 wei、极小余额、最大整数和供应量归零。
  4. 用状态机模糊测试跨函数序列,而不是只测单函数。
  5. 对每种除法明确舍入方向和误差承担方。
  6. 检查创建、清空、再初始化与缓存失效的全生命周期。
  7. 在本地主网分叉中回归真实 decimals 与外部组合。
  8. 上线后监控份额价格突变、异常铸币、极小余额循环和大额短时流出。
  9. 为高风险协议设单笔/单区块/时间窗口限损和自动暂停。
  10. 把公开事件的最小反例收成永久回归测试。

学习边界

可以学习、实现和分享:

  • 通用不变量与安全测试方法
  • 本地教学合约
  • 已公开事件在历史区块的本地分叉复现
  • 已修复并公开披露的根因
  • Foundry、Echidna、Slither、Halmos 等工具

需要停止并走授权流程的情况:

  • 怀疑仍在运行的真实协议存在未公开漏洞
  • 准备向真实合约发送边界探测交易
  • 测试可能改变真实资金、价格或用户状态
  • 复现材料包含可直接作用于未修复目标的完整调用参数

安全研究的正确闭环是:保留证据、最小化验证、私下报告并等待修复——不是在真实链上用资金证明猜想。

公开学习入口

来源笔记:10-生活/币圈/04-链上/合约漏洞如何被发现.md

若你还没有可用的 Web3 钱包入口,可走 OKX 官方邀请页(VITALK),再回到本文继续「智能合约漏洞通常不是盯出小数点」。

打开 OKX Web3 邀请页(VITALK)

披露:上方为邀请链接,作者可能因此获得返佣或奖励。不构成投资建议,请注意钓鱼站与私钥安全。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-07-31发表,共计2889字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)