DeFi 常见风险速查(重入 / 溢出&精度 / MEV)
这份文档面向 DApp/DeFi 开发、测试、产品,目标是“一目了然 + 可落地执行”。每个风险按如下结构说明:
- 定义:它是什么
- 触发条件:通常怎么发生
- 影响:会造成什么损失/故障
- 典型场景:在 DeFi 里常见在哪些模块出现
- 如何发现:测试/审计时怎么快速定位
- 防护建议:工程上怎么做更安全
1) 重入(Reentrancy)
定义
重入是指合约在一次外部调用(call / transfer / 调用外部合约函数)过程中,被对方合约“回调”回来,再次进入当前合约的某个函数(通常是同一个或相关的资金函数),导致状态尚未更新或校验被绕过,从而产生 重复提款、重复记账、绕过额度 等问题。
触发条件(常见)
- 你在函数中 先对外部地址/合约转账或调用,再更新内部状态(余额、份额、索引等)。
- 外部调用使用了
call(或可执行任意逻辑的外部合约调用),对方合约在 fallback/receive 中回调你的合约函数。 - 资金相关逻辑没有“重入锁”或逻辑上允许在未完成状态下重复进入。
影响
- 重复提现/盗取资金:同一份余额被多次取出。
- 状态错乱:份额、利息索引、会计账本被多次更新或未一致更新。
- 绕过限制:如额度检查在第一次通过后,第二次重入时绕过。
典型场景(DeFi 高频)
- Vault / Bank:
deposit()/withdraw()/redeem()。 - Staking:
unstake()/claimRewards()(奖励发放先转账后更新)。 - AMM/Swap Router:某些回调式设计(尤其是多合约交互、flash swap)。
- 借贷协议:
borrow()/repay()/liquidate()中的外部调用、代币转账、回调。
如何发现(测试/审计抓手)
- 代码扫描:
- 查找“外部调用”后才更新状态的模式:
- 先
token.transfer(...)/call{value:...},再balances[msg.sender] -= amount。
- 先
- 查找可重入入口:任何 public/external 且会改变余额/份额的函数。
- 查找“外部调用”后才更新状态的模式:
- 单测/模糊测试:
- 用恶意合约(attacker)在 fallback 中重入目标函数,验证能否重复提款或破坏不变量。
- 不变量检查(Invariants):
- “合约资产总额 = 用户份额总和”;
- “单用户余额不会负数/不会在一次操作内增加两次”;
- “每次提款后余额必然减少”。
防护建议(工程可落地)
- CEI 模式(Checks-Effects-Interactions):
- 先做校验(Checks),再更新状态(Effects),最后外部交互(Interactions)。
- 重入锁(Reentrancy Guard):
- 对关键函数加
nonReentrant,并避免可重入的内部/外部调用链造成“锁绕开”。
- 对关键函数加
- 拉取式支付(Pull over Push):
- 先记账,用户自己来
claim();避免在核心流程里直接对外部地址转账。
- 先记账,用户自己来
- 最小外部调用:
- 尽量减少可执行任意逻辑的外部调用;必要时把外部调用集中在最后一步。
2) 溢出 / 下溢(Overflow/Underflow)与“精度/舍入”风险
说明:Solidity 0.8+ 已默认检查整型溢出并自动 revert,但现实中更常见的“溢出类事故”来自:
- 使用旧版本/
unchecked/自定义数学库- 精度处理错误、舍入误差、单位换算错误、顺序错误(这些会导致资产错算,效果与“溢出”一样严重)
定义
- 溢出/下溢:数值超过类型上限/下限后回绕(或在 0.8+ 触发 revert)。
- 精度/舍入风险:由于整数除法、
decimals差异、固定点计算误差导致:- 计算出的金额偏大/偏小;
- 利息、份额、价格、兑换比率错误;
- 长期累积后出现“系统性亏损/被套利”。
触发条件(常见)
- 在
unchecked {}中做加减乘,或依赖外部库/老版本合约。 amount * price / 1e18的计算顺序不当,先除导致大量精度损失。- 不同代币
decimals不同(6/8/18),单位换算遗漏。 - 份额(share)/价格(pricePerShare)/利息指数(index)更新时,舍入方向导致可被“反复存取套利”。
影响
- 用户资产被错误铸造/销毁:多给或少给。
- 可被套利:利用舍入误差反复操作赚取差额(常见于 share 计算、兑换比率)。
- 系统性坏账:借贷协议利息/抵押计算错误导致清算阈值失效。
典型场景(DeFi 高频)
- Vault / Share 模型:
shares = amount * totalShares / totalAssets;amount = shares * totalAssets / totalShares。
- 借贷利息/指数:
- 利息指数累乘、累加带来精度问题。
- AMM 报价/滑点:
amountOutMin计算错误,导致用户被过度滑点或交易失败。
- 抵押品估值:
- 价格喂价单位(8 位/18 位)换算出错,直接影响清算。
如何发现(测试/审计抓手)
- 边界值测试:
- 极小金额(1 wei / 1 最小单位);
- 极大金额(接近
uint256上限); - decimals 不同的组合(6↔18、8↔18)。
- 舍入方向测试:
- 连续执行
deposit -> withdraw -> deposit -> withdraw,看是否能凭舍入赚钱。
- 连续执行
- 属性/不变量测试:
- “总资产变化 = 用户资产变化之和(含手续费/利息)”;
- “shares 不能凭空增发”;
- “同一价格下可赎回金额不应超过总资产”。
防护建议(工程可落地)
- 统一精度(WAD/RAY):
- 约定内部统一用 1e18(WAD)或 1e27(RAY),对外再换算。
- 使用成熟数学库:
mulDiv(512 位乘法再除法)减少溢出与精度损失(例如 OpenZeppelin/PRBMath 等思路)。
- 明确舍入策略:
- 关键处选择向下取整(防止多发钱)或向上(防止少收手续费)并在文档中写清楚。
- 避免
unchecked:- 除非明确证明安全,并写测试覆盖边界。
3) MEV(Maximal/Maximum Extractable Value)
定义
MEV 是指出块者/排序者(或通过搜寻者 Searcher 与他们合作)利用“交易排序权”从用户交易中获取额外收益的行为。它不是传统意义的“合约漏洞”,但会让用户在链上交易时产生 更差成交价、被夹击、被抢跑、被清算抢占 等损失。
触发条件(常见)
- 交易进入公共 mempool,可被观察。
- 交易包含可预测的盈利机会:
- 大额 swap(滑点设置过宽);
- 套利路径明显;
- 清算、预言机更新窗口;
- 铸造/赎回比例变化等。
影响
- 夹子攻击(Sandwich):用户成交价显著变差(被前置买入抬价,后置卖出砸回)。
- 抢跑(Front-run):别人先一步执行同类交易,吃掉机会(如套利/清算)。
- 后跑(Back-run):在你交易后立刻套利,间接提高你的成本(尤其是 AMM)。
- 交易失败/被 DoS:你的交易因价格变化或 Gas 竞争失败,浪费时间和机会(有的链会消耗手续费)。
典型场景(DeFi 高频)
- Swap/聚合器交易:大额 swap、滑点宽、路径公开。
- 清算:谁先清算谁赚清算奖励。
- 铸造/赎回窗口:价格更新、指数更新前后存在可套利的短窗口。
- NFT/IDO:抢跑铸造、本质也是 MEV。
如何发现(测试/审计抓手)
- 看交易参数:
amountOutMin是否设置过低(滑点过大);deadline是否过长;- 是否暴露可预测的套利路径。
- 回放链上案例:
- 在区块浏览器/MEV 仪表盘上查看同区块内是否出现“前置/后置”两笔围住用户交易的模式。
- 仿真测试:
- 在本地 fork 环境里模拟:攻击者在你交易前后插入 swap,比较用户实际损失。
防护建议(工程可落地)
- 合理滑点与期限:
- 默认给出保守滑点;对大额交易提示用户风险;
deadline设短,降低被长时间观察的窗口。
- 使用私有交易通道(视链而定):
- 例如通过私有 mempool / 交易包(bundle)提交,减少被公开观察和夹击的概率。
- 抗 MEV 设计:
- 对易被夹的操作拆分、引入 TWAP/预言机、限制单笔交易价格影响;
- 对清算采用拍卖/批量处理(减少抢跑空间)。
- 用户侧提示:
- 交易确认前展示“预计滑点损失范围”“价格影响”;
- 大额交易建议分批或走聚合器、私有通道。
一页测试/审计清单(建议直接用)
重入
- 是否所有资金函数满足 CEI?
- 是否对关键入口加了重入锁?
- 是否有外部调用(尤其是
call)在状态更新之前? - 是否有 attacker 合约重入单测?
溢出/精度
- 是否统一内部精度(WAD/RAY)并清晰换算 decimals?
- 是否覆盖极小/极大金额、不同 decimals 的边界测试?
- 是否明确舍入方向并验证“反复存取套利”不可行?
MEV
- 默认滑点是否过宽?deadline 是否过长?
- 大额交易是否给出价格影响提示?
- 是否考虑私有通道或抗 MEV 机制(尤其是清算、重大 swap)?
评论区