重入攻击
简单来说:在合约状态还没发生改变前,不停的进行对合约进行套利操作。
存在场景:
1、单函数重入(Single-Function Reentrancy)
简单来说:在一个函数中进行多次获取钱财操作。一般发生在先给他人转账,再进行修改状态。
流程
• 攻击路径:Victim.funcA → Attacker.receive/fallback → Victim.funcA → …
• 经典案例:The DAO(2016)。
• 关键不变量:funcA 在每次外部调用前必须先完成「余额扣减或状态迁移」。
解决方案:
先修改状态,再进行转账。
2、跨函数重入(Cross-Function Reentrancy)
简单来说:A和B函数共享一个Mapping数据,A进行执行了操作,但是还没有更新Mapping中的数据。攻击者立即调用B函数进行再次操作获利。A和B函数在同一个合约中。
流程:
• 攻击路径:Victim.funcA → Attacker.receive → Victim.funcB。
• 共享状态:funcA 与 funcB 操作同一个 storage 变量(如全局余额 mapping)。
• 案例:Uniswap v1 的 ERC-777 重入(2020)。
解决方案:
- CEI 顺序
先调整状态,再进行转账
function withdrawAll() external {
uint bal = balanceOf[msg.sender];
require(bal > 0);
balanceOf[msg.sender] = 0; // ① 先清零
(bool ok,) = msg.sender.call{value: bal}(""); // ② 再转账
require(ok);
}
- 互斥锁
contract Victim is ReentrancyGuard {
...
function withdrawAll() external nonReentrant { ... }
}
- 状态机拆分
如果业务必须在外部调用后更新状态,可引入「提款申请 → 延迟期 → 最终提取」两阶段模型,将重入面缩小到可控范围。
3、跨合约重入(Cross-Contract Reentrancy)
简单来说:两个合约共用同一份余额记录,A 合约还没扣账就把钱打出去,B 合约立刻利用未扣账的余额再把这笔钱提一次。
流程:
• 攻击路径:VictimA.funcA → Attacker.receive → VictimB.funcB。
• 共享状态:VictimA 与 VictimB 通过外部合约或 delegatecall 共享同一状态槽(如代理-实现模式)。
• 案例:Siren Protocol 抵押品计算漏洞(2021)。
4、只读重入(Read-Only Reentrancy)
流程:
• 攻击路径:Victim.funcA(尚未退出)→ Attacker.view → 读取中间状态 → 套利。
• 特点:攻击者并不修改状态,而是利用 view 函数返回的「脏数据」在外部协议中获利。
• 案例:Curve read-only reentrancy(2022),导致多个借贷协议价格预言机被操纵。
闪电贷治理攻击
用户进行闪电贷,使得自己所占份额变大在这一刻。那么如果 这一刻进行投票,该用户会获得大量选票,做一些恶意事件。
为什么说是这一刻,因为可能这个用户无法支付闪电贷的全部费用。