很多 DeFi 事件最后会被概括成“舍入错了几个 wei”“小数精度有问题”。发现者通常不是靠肉眼盯着小数位碰运气。更常见的路径是:
先定义协议永远不能违反的业务规则
↓
让工具生成大量极端金额和调用顺序
↓
找到能够破坏规则的最小反例
↓
分析误差能否重复、放大并转化为真实资产
↓
只在本地分叉、测试网或授权环境中验证
真正要找的不是“哪里少了一个小数”,而是**哪个边界状态会让协议的账不再守恒**。
本文只做公开安全方法科普,不提供针对真实协议的攻击参数,也不鼓励未经授权的链上探测。
为什么 DeFi 特别容易出现精度问题
EVM 主要用整数运算。代币显示的“小数”本质仍是整数单位:
1 ETH = 10^18 wei
1 USDC = 10^6 个最小单位
协议又要在资产、份额、价格、利率和不同 decimals 之间反复换算,于是常见:
- 整数除法直接丢弃余数
- 向上/向下舍入方向选错
- 先乘后除可能溢出,先除后乘损失精度
- 多种代币 decimals 不一致
- 单次 1 wei 误差被循环调用累积
- 份额价格被误差改变后,下一次继续基于错误价格
- 总供应量归零,但缓存、虚拟余额或累计值未同步重置
所以有些事件表面是小数问题,根因可能是会计模型、状态生命周期或旧版整数安全机制。
第一步:先写出必须守住的不变量
不变量是无论谁调用、输入多极端、顺序多奇怪,都必须成立的业务规则。常见包括:
- **资产守恒**:没有收益来源时,协议不能凭空增加可提取资产。
- **偿付能力**:真实持有资产不能低于对用户承诺的可赎回价值。
- **份额一致性**:用户份额、总供应量、内部缓存和真实余额必须对应。
- **往返不获利**:立即“存入—赎回”或“A—B—A”后,不应仅靠舍入获得额外价值。
- **误差有界**:多次调用的误差必须有设计上限,不能单向累积。
- **生命周期完整**:池创建、首次存款、清空、供应量归零和重新初始化时,派生状态同步变化。
最简单的检查可以是:
初始资产 = 100
存入 → 获得份额 → 立即赎回
最终资产必须 ≤ 100
若工具找到最终资产为 101 的输入,即使只多一个最小单位,也已证明存在反例。接着再问:差额能否循环、扩大或影响份额价格。
第二步:优先看这些代码位置
assets与shares的双向换算mulDiv、ceilDiv、roundUp、roundDown- 固定精度常量与 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 | 旧版大数乘法与联合曲线 | 接近零成本时不能铸造巨额市值代币 |
这是从公开复盘反推的合理入口,不代表首名发现者完整私有流程。
开发团队可用的检查顺序
- 画出真实资产、内部余额、份额供应、价格和缓存关系。
- 为守恒、偿付、往返不获利和误差上限写可执行不变量。
- 覆盖 0、1 wei、极小余额、最大整数和供应量归零。
- 用状态机模糊测试跨函数序列,而不是只测单函数。
- 对每种除法明确舍入方向和误差承担方。
- 检查创建、清空、再初始化与缓存失效的全生命周期。
- 在本地主网分叉中回归真实 decimals 与外部组合。
- 上线后监控份额价格突变、异常铸币、极小余额循环和大额短时流出。
- 为高风险协议设单笔/单区块/时间窗口限损和自动暂停。
- 把公开事件的最小反例收成永久回归测试。
学习边界
可以学习、实现和分享:
- 通用不变量与安全测试方法
- 本地教学合约
- 已公开事件在历史区块的本地分叉复现
- 已修复并公开披露的根因
- Foundry、Echidna、Slither、Halmos 等工具
需要停止并走授权流程的情况:
- 怀疑仍在运行的真实协议存在未公开漏洞
- 准备向真实合约发送边界探测交易
- 测试可能改变真实资金、价格或用户状态
- 复现材料包含可直接作用于未修复目标的完整调用参数
安全研究的正确闭环是:保留证据、最小化验证、私下报告并等待修复——不是在真实链上用资金证明猜想。
公开学习入口
—
来源笔记:10-生活/币圈/04-链上/合约漏洞如何被发现.md
若你还没有可用的 Web3 钱包入口,可走 OKX 官方邀请页(VITALK),再回到本文继续「智能合约漏洞通常不是盯出小数点」。
披露:上方为邀请链接,作者可能因此获得返佣或奖励。不构成投资建议,请注意钓鱼站与私钥安全。




