GoPlus Security:由 Vyper 编译器 0.2.15 版本漏洞引发的 Curve 攻击分析
背景
近期,一些使用 Vyper 0.2.15 版本编写的稳定池(alETH/msETH/pETH)遭受了重入攻击。这是由于该版本编译器实现错误导致的重入锁故障问题。此漏洞导致了约5000万美金的损失。本文将分析该漏洞的根本原因和攻击原理。同时,将提供优化建议以防止类似事件再次发生。
根因分析
漏洞根源来自于编译器对@nonreentrant(<str>)修饰器语义的错误实现。
正确语义描述
@nonreentrant(<str>)是用于限制函数的重入,其语义则类似于多线程中的互斥锁:
单函数修饰:当一个函数被该修饰器修饰,它的多个调用无法同时执行。
多函数修饰:当多个函数都使用该修饰器,且重入限制键相同(字符串值相同),它们的多个调用仍然无法同时执行。
一个正确的实现是,对每个重入限制键设置一个布尔值,0代表有一个函数在执行,1代表没有函数执行。多个被该键限制的函数根据该变量来进行同步(获得键-1,释放键+1,尝试获得键需检查值是否为1)。
实际语义实现
在Vyper编译器0.2.15版本及之前,对于该语义的实现是错误的。错误代码如下所示。
分析 代码中的storage_slot对应上文提到的同步变量。错误在于它的值域,该变量一开始为0,每当解释器处理到一个函数声明中的@nonreentrant,便将同步变量 +1,这样是重复的。当代码满足上文中的多函数修饰 情况时,由于解释器处理了多个函数,同步变量的值域不再满足布尔值,与正确语义不符合(即存在锁失效)。其实,代码中的TODO 注释已经反映了开发者的顾虑。
修复 在patch中,检查了该键是否已被创建,若是则不再+1
结果 些合约因为该修饰器的实际实现与预期不符而受到攻击,即使这些合约在理论上是正确的。比如接下来介绍的案例。
合约攻击案例
所有满足上文提及的多函数修饰合约,都有可能被攻击。比如被攻击的一个合约,代码有5个函数都被同一个键@nonreentrant(“lock”)修饰,因此相互可以重入。
- add_liquidity
- remove_liquidity
- exchange
- remove_liquidity_imbalance
- remove_liquidity_one_coin
黑客的攻击利用了前两个函数。它们分别用于往流动池中添加和移除资产,流动性提供者从中可以获得交易费用和其他奖励。攻击的过程类似于一般的重入攻击:
- 第一次调用add_liquidity存资产。
- 调用remove_liquidity移除这些资产。
其中remove_liquidity 会通过外部调用,返还eth给黑客。此时黑客通过在fallback函数内调用add_liquidity完成重入。
在重入到add_liquidity 后,由于remove_liquidity 尚未减少代币总量total_supply ,因此在计算为黑客铸造的代币数量时,超出了原本应有的数量。
remove_liquidity()