Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

授权竞争:旧额度与新额度如何都被消费

ERC-20 的 approve(spender, value) 覆盖当前额度,不表示“从最初授权起累计最多花 value”。当旧额度已存在,Spender 可以先花旧额度,再在覆盖交易生效后使用新额度。ERC-20 规范

这里每一笔交易仍然具有原子性。竞争发生在多笔交易的排序之间,不能把等待确认解释为交易“非原子”。攻击者需要已经拥有有效额度,或能利用已被授权 Spender 的业务漏洞;旁观内存池本身不会获得 transferFrom 权限。

1. 完整的排序实例

假设 Alice 有 200 个代币,Spender 已获 100 的额度。Alice 准备把剩余额度改成 50:

顺序已执行操作剩余额度Spender 累计转走
初始approve(spender, 100)1000
抢先消费Spender transferFrom(..., 100)0100
覆盖交易Alice approve(spender, 50)50100
再次消费Spender transferFrom(..., 50)0150

150 来自旧额度 100 加新额度 50。approve(50) 不会检查此前已花了多少,也不会把历史转账回滚。

公开内存池会增加提前观察交易的机会,但实际排序由区块构建过程决定。提高 Gas 费用或使用私有交易通道可能改变观察和排序条件,不能保证消除额度模型本身的风险。

2. 先归零的作用与局限

approve(spender, 0) 确认后,旧剩余额度不能再用;确认前仍可能被消费。清零无法追回已经转走的代币。

如果随后不检查期间发生的转账,直接重新授权 50,同样可能形成“旧额度已经消费,再给一份新额度”的结果。用户或应用应在清零确认后重新核对余额、消费和真实业务需求,再决定是否设置新额度。

对于始终不可信的 Spender,重新给额度仍允许其按规则转账;这种权限风险也不能由清零步骤解决。

3. 增量调整与 permit

某些代币提供 increaseAllowance / decreaseAllowance,按执行时的余额增减额度,避免简单覆盖重新补足旧额度。减少值大于当时剩余额度时通常回滚;并不代表所有待确认交易都不能被抢先排序。

这两个接口不是 ERC-20 必需接口,OpenZeppelin 5.x 基础 ERC20 已不提供。应按代币实现调用,不能假设所有代币都支持,也不应把它们的移除推导为“permit 永久解决竞态”。

EIP-2612 允许签名设置额度。单独提交 permit 仍然覆盖 allowance;在 deadline 前任何人可提交许可。把许可和消费放进同一业务交易可以缩小操作窗口,但原有 allowance、Spender 权限和业务身份仍要分析。

ERC-777 的 operator、ERC-1155 的全局授权采用不同权限模型,不是对 ERC-20 覆盖额度风险的通用补丁。

4. 运行一个可复现的状态序列

仓库 examples/ethereum-signatures 中的 testApprovalRaceSpendsOldAndNewAllowance 用 Foundry 分别模拟 Owner 与 Spender,按上表顺序调用。

npm ci --ignore-scripts
forge test --match-test testApprovalRace -vv

预期累计收款为 150 个单位、剩余额度为 0。测试复现的是选定排序下的状态转换,不模拟真实内存池、竞价或区块构建者,也不证明某个历史攻击的金额或归因。

5. 集成时真正需要检查的权限

  • 谁是 Token 上的 Spender,是否能升级,哪些入口能够触发代扣。
  • from、收款人、金额与业务调用者或已验证签名是否绑定。
  • 当前 allowance、待提交许可及撤销交易各处于什么状态。
  • 限额和授权期限由哪一层实现;标准 ERC-20 allowance 没有自动失效时间。

延伸阅读:无限授权、forceApprove 与 Permit2。