重入攻击是什么?Curve池内的7000万美元如何丢的?

近期 Curve 的 Vyper 编译语言漏洞导致超过 7000 万美元被盗,此次的漏洞被称为「重入攻击」,本文将深入介绍重入攻击。本文源自 CyberPunkMetalHead 于 Medium 所着文章 《A Deep Dive Into How Curve Pool’s $70 Million Reentrancy Exploit Was Possible》,由 星球日报Odaily 编译、整理。
近期的 Curve 池漏洞与我们在过去几年里看到的大多数加密货币黑客事件有所不同,因为与之前的许多漏洞不同,这一次并不直接与智能合约本身的漏洞有关,而是与它所使用的语言的底层编译器有关。
在这里,我们谈论的是 Vyper:一个面向智能合约的、具有Pythonic风格的编程语言,旨在与以太坊虚拟机(EVM)交互。我对此次漏洞的背后原因非常感兴趣,所以我决定深入研究。
随着这次漏洞的发展,每天的新闻头条都在报告新的数字。现在看来,情况终于得到了控制,但在此之前已经有超过 7000 万美元被盗。根据 LlamaRisk 的事后评估,截止到今天,有几个 DeFi 项目的池子也被黑客攻破,包括PEGD 的 pETH/ETH: 1100 万美元;Metronome 的 msETH/ETH: 340 万美元;Alchemix 的 alETH/ETH: 2260万美元;和Curve DAO: 大约2470万美元。
这次漏洞被称为重入错误,它是在 Vyper 编程语言的某些版本上出现的,特别是 v0.2.15、v0.2.16 和 v0.3.0。因此,使用这些特定版本的 Vyper 的所有项目都可能成为攻击的目标。
什么是重入(reentrancy)?
为了理解这次漏洞为什么会发生,我们首先需要了解什么是重入以及它是如何工作的。
如果一个函数在运行过程中可以被中断,并且在其之前的调用完成运行之前可以安全地再次被调用「重新进入」,则称该函数为可重入的。可重入函数在硬件中断处理、递归等应用中都有使用。
为了使一个函数变得可重入,它需要满足以下条件:
- 它不能使用全局和静态数据。这只是一种约定,没有硬性的限制,但如果使用全局数据的函数被中断和重新启动,它可能会丢失信息。
- 它不应修改自己的代码。无论函数何时被中断,都应该能够以相同的方式运行。这可以管理,但通常不建议这样做。
- 它不应该调用其他非重入函数。 重入不应与线程安全混淆,尽管它们紧密相关。一个函数可以是线程安全的,但仍然不是可重入的。为了避免混淆,重入只涉及到一个线程的运行。这是在没有多任务操作系统存在的时代的一个概念。
这里有一个实际的例子:
i = 5
def non_reentrant_function():
return i**5
def reentrant_function(number:int):
return number**5
函数 non_reentrant_function:
- 这个函数没有参数。
- 它直接返回全局变量 i 的五次方。
- 所以当你调用这个函数时,它总是返回 5**5,即 3125。
函数 reentrant_function:
- 这个函数有一个参数 number,是整型。
- 它返回参数 number 的五次方。
- 这意味着你可以给这个函数传入任何整数,并得到这个数的五次方作为返回值。例如,如果你传入 2,它会返回 2 的 5 次方 ,即 32。
值得注意的是,许多智能合约函数都不是可重入的,因为它们访问如钱包余额之类的全局信息。
什么是锁(Lock)?
锁本质上是一种线程同步机制,某个进程可以声称或「锁定」另一个进程。
最简单的锁类型被称为二进制信号量。这种锁为被锁定的数据提供独占访问。还有更复杂的锁类型,可以提供对读数据的共享访问。在编程中误用锁可能导致死锁或活锁,进程持续互相阻塞,状态不断改变但没有进展。
编程语言在后台使用锁来优雅地管理和共享多个子程序之间的状态更改。但是,某些语言,如 C# 和 Vyper 允许在代码中直接使用锁。
@nonreentrant(‘lock’) def func(): assert not self.locked, “locked” self.locked = True # Do stuff # Release the lock after finishing doing stuff raw_call(msg.sender, b””, value=0) self.locked = False # More code here
在上面的例子中,我们希望确保如果 msg.sender(合约调用者)是另一个合约,它不会在运行时调用代码。如果在 raw_call() 下面还有更多的代码,而没有锁,msg.sender 可能会在我们的函数运行完毕之前调用上面的所有代码。
因此,在 Vyper 中,nonreentrant(‘lock’) 装饰器是一种控制对函数的访问的机制,以防止调用者在它们完成运行之前反复运行智能合约函数。
在许多 DeFi 黑客事件中,通常都是合约开发者没有预见到的智能合约错误,一个聪明但恶意的利用者发现了某些函数或数据暴露的方式中的弱点。但这次的情况独特之处在于,Curve 的智能合约以及所有其他成为攻击受害者的池和项目在代码本身中都没有已知的漏洞。合约是稳固的。
nonreentrant(‘lock’)是存在的。
由于 Vyper 语言在处理重入锁的方式上出现了问题,导致了这个问题的发生。所以,合约创建者可能部署了看似合理的代码,但由于编译器没有正确处理锁,使得攻击者能够利用这个有缺陷的锁进行利用,导致合约行为出现意料之外的结果。
让我们看看真正受到重入攻击的合约。注意 @nonreentrant(‘lock’) 修饰符吗?通常情况下,这应该可以防止重入,但实际上并未能防止。攻击者能够在函数返回结果之前反复调用 remove_liquidity()。
@nonreentrant(‘lock’) def remove_liquidity( _burn_amount: uint256, _min_amounts: uint256[N_COINS], _receiver: address = msg.sender ) -> uint256[N_COINS]: “”” @notice Withdraw coins from the pool @dev Withdrawal amounts are based on current deposit ratios @param _burn_amount Quantity of LP tokens to burn in the withdrawal @param _min_amounts Minimum amounts of underlying coins to receive @param _receiver Address that receives the withdrawn coins @return List of amounts of coins that were withdrawn “”” total_supply: uint256 = self.totalSupply amounts: uint256[N_COINS] = empty(uint256[N_COINS]) for i in range(N_COINS): old_balance: uint256 = self.balances[i] value: uint256 = old_balance * _burn_amount / total_supply assert value >= _min_amounts[i], “Withdrawal resulted in fewer coins than expected” self.balances[i] = old_balance – value amounts[i] = value if i == 0: raw_call(_receiver, b””, value=value) else: response: Bytes[32] = raw_call( self.coins[1], concat( method_id(“transfer(address,uint256)”), convert(_receiver, bytes32), convert(value, bytes32), ), max_outsize=32, ) if len(response) > 0: assert convert(response, bool) total_supply -= _burn_amount self.balanceOf[msg.sender] -= _burn_amount self.totalSupply = total_supply log Transfer(msg.sender, ZERO_ADDRESS, _burn_amount) log RemoveLiquidity(msg.sender, amounts, empty(uint256[N_COINS]), total_supply) return amounts
这是如何被利用的?
到目前为止,我们知道重入攻击是一种反复调用智能合约中的某个函数的方法。但这是如何导致资金被盗和在Curve 攻击中损失 7000 万美元的呢?
注意智能合约末尾的 self.balanceOf[msg.sender] -= _burn_amount 吗?这告诉智能合约池中 msg.sender 的流动性,减去燃烧费。接下来的代码行为 message.sender 调用 transfer()。
因此,一个恶意合约可以在金额更新之前不断地调用提现,几乎让他们可以选择提取池中的所有流动性。
这样的攻击通常的流程是这样的:
- 易受攻击的合约有 10 个 eth。
- 攻击者调用存款并存入 1 个 eth。
- 攻击者调用提现 1 个 eth,此时提现函数运行一些检查:
- 攻击者的帐户中是否有 1 个 eth?是的。
- 将 1 个 eth 转移到恶意合约。注意:合约的余额尚未更改,因为该函数仍在运行。
- 攻击者再次调用提现 1 个 eth。(重新入场)
- 攻击者的帐户中是否有 1 个 eth?是的。
这将重复,直到池中没有更多的流动性。
Vyper 语言中的这个问题已经被修复,在 0.3.0 版本之后不再存在。如果您是开发人员,或使用 Vyper 的 Web3 组织,请确保立即更新您的版本。
你可能感兴趣的文章
-
重入攻击是什么?Curve池内的7000万美元如何丢的?
之前的 Curve 池漏洞与我们在过去几年里看到的大多数加密货币黑客事件有所不同,因为与之前的许多漏洞不同,这一次并不直接与智能合约本身的漏洞有关,而是与它所使用的语…
2026-06-14 -
解读Tornado Cash混币器:监管者的眼中钉,却是最精妙的ZK零知识应用
近期 Vitalik 和一些学者联名发表了新论文,其中提到了 Tornado Cash 如何实现反洗钱方案(其实就是让取款人证明,自己的存款记录属于一个不包含黑钱的集合), 但文中缺乏…
2026-06-14 -
代币化股票到底是什么?以SpaceX xStock拆解代币化股票的优势与风险
代币化股票,也称为代币化权益或股权代币,是在公有或许可链上发行的数字代币,被设计为复制某一现实世界股票或股权的价格表现,本文将以SpaceX xStock为例,为大家拆解代币…
2026-06-14 -
加密货币混币器是什么?工作原理&类型、洗钱监管风险、代表项目全
本文整理了混币器的工作原理、不同类型以及监管风险等方面,以帮助读者了解这一加密货币技术…
2026-06-14 -
链上美股热潮来袭:定义、架构与税务要点拆解
所谓链上美股具体指的是哪些投资产品,其在法律的性质是什么,投资链上美股的收益是否需要缴税,如何进行税务筹划?本文给大家详细说说,需要的朋友可以参考下…
2026-06-14 -
SpaceX IPO首日攻略:别当普通热门股炒
本文编译自 The Flow Horse 围绕 SpaceX IPO 首日交易策略的视频内容,重点并非讨论 SpaceX 的长期基本面,而是拆解其上市初期可能面对的资金流、流通盘、指数纳入与解禁节…
2026-06-14 -
深入了解 Tornado Cash 隐私保护工具(混币器的运作、风险与争议)
混币器(Crypto Tumbler)是一种服务或智能合约,帮助用户混合加密货币交易,增强交易隐私,它的基本运作方式是将来自不同用户的资金进行混合,然后重新分配给这些用户,使…
2026-06-14 -
美股上链:六大类业务赛道详解,xStocks、Ondo、Alpaca是否能引爆叙事
这篇文章主要为大家介绍了美股代币化赛道解析:稳定币券商入口、代币化股票与证券基础设施建设并行推进,揭示链上美股未来可能的合规与流动性挑战,需要的朋友可以参考下…
2026-06-11 -
一文读懂Morpho Midnight:当链上借贷遇上固定利率与定期市场
Morpho推出Midnight白皮书,旨在为DeFi引入固定利率和固定期限借贷,协议将借贷重写为零息凭证买卖,依托隔离市场和日历到期日聚合流动性,下面小编给大家详细说说Morpho Mid…
2026-06-11 -
什么是加密货币挖矿?通俗讲解其运作原理、内核类型与收益现状
本文介绍了加密货币挖矿的原理,矿工利用算力解决难题以验证交易、发行新币并保障区块链安全,文章通俗讲解了哈希运算、挖矿难度等流程,分析了ASIC、矿池等类型,并结合比特…
2026-06-10










