由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版本及之前,对于该语义的实现是错误的。错误代码如下所示。

for node in vyper_module.get_children(vy_ast.FunctionDef):
type_ = node._metadata["type"]
if type_.nonreentrant is not None:
type_.set_reentrancy_key_position(StorageSlot(storage_slot))

# TODO this could have better typing but leave it untyped until
# we nail down the format better
variable_name = f"nonreentrant.{type_.nonreentrant}"
ret[variable_name] = {
"type": "nonreentrant lock",
"location": "storage",
"slot": storage_slot,
}

# TODO use one byte - or bit - per reentrancy key
# requires either an extra SLOAD or caching the value of the
# location in memory at entrance
storage_slot += 1 # Error Here​

分析 代码中的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()

add_liquidity()

解决方案与未来发展

针对这则事故,我们对于未来的Web3安全开发解决方案有一些思考:

1. 协议开发者的责任与敏感性

协议开发者需要积极关注核心供应链的更新和安全问题,及时获取相关信息,增加对底层实现的理解。

2. DevSecOps的引入

如何在合约开发流程中更好的引入一些安全测试和验证的工具,无论是静态分析还是动态Fuzz,嵌入SDLC(软件开发生命周期)中,使得一些问题能够更早的被暴露并解决。

3. Software Composition Analysis (SCA) 工具的引入

我们注意到,在Vyper漏洞被曝出之后,其实给几个被攻击的协议团队有大约半天到一天的时间去做出补救措施去反应,但遗憾的是,最终并没能成功挽救被盗资产。我们认为对于这种供应链问题,应该引入完善的供应链管理平台,也就是Web2我们常说的制品库管理,当供应链出现问题的第一时间,可以快速告警并给出升级方案。

4. 交易底层范式的创新

在基础设施层面,考虑在运行时层面进行自动化防护,例如动态的安全检查,或者在执行关键操作时自动执行防护措施。我们也很开心看到了许多在这个层面上的一些趋势,例如Uniswap V4,通过引入Hook,大大增加了交易功能的扩展性,开发者可以在交易的多个阶段中去提供一些防护来增强安全性。类似的还有Artela,能够让开发者在Runtime层面实现多个阶段的扩展,天然的就能够解决重入这个大问题,具体可以参考https://medium.com/@artela_chinese/该篇文章。

结论

智能合约开发的复杂性和困难性使得漏洞的出现几乎不可避免。但通过更好地理解和使用现有的工具,持续关注和参与开源社区活动,优化和改进基础设施,我们可以降低漏洞的风险,提高整个系统的安全性和健壮性。未来的区块链安全不仅需要编译器和开发者的共同努力,更需要依赖于静态分析、动态Fuzz等先进工具的支持和推动。同样也更需要有创新者能够打破束缚,提出更好的底层交易模型以及方案,让整个Web3世界更加安全。

--

--