智能合约安全 - 重入攻击 - 常见漏洞(第一篇)

重入攻击

简单来说:在合约状态还没发生改变前,不停的进行对合约进行套利操作。

存在场景:

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)。

解决方案:

  1. 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);
}
  1. 互斥锁

    contract Victim is ReentrancyGuard {
    ...
    function withdrawAll() external nonReentrant { ... }
    }

  2. 状态机拆分
    如果业务必须在外部调用后更新状态,可引入「提款申请 → 延迟期 → 最终提取」两阶段模型,将重入面缩小到可控范围。

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),导致多个借贷协议价格预言机被操纵。

闪电贷治理攻击

用户进行闪电贷,使得自己所占份额变大在这一刻。那么如果 这一刻进行投票,该用户会获得大量选票,做一些恶意事件。

为什么说是这一刻,因为可能这个用户无法支付闪电贷的全部费用。