2023 年 7 月 30 日,Curve 生态多个资金池在数小时内被抽走约 7000 万美元。事后统计显示,损失分布在几个不同项目:Alchemix 的 alETH/ETH 池约 2260 万美元,Curve DAO 自身约 2470 万美元,PEGD 的 pETH/ETH 约 1100 万美元,Metronome 的 msETH/ETH 约 340 万美元。这起事件特殊的地方在于,被攻击的合约本身没有写错代码,问题出在把这些代码翻译成字节码的编译器上。
如果你搜「重入攻击是什么」,通常得到的答案是「攻击者在合约更新余额之前反复调用提款函数」。这个描述对早期以太坊案例成立,但解释不了 Curve 这一轮。要弄清 7000 万美元怎么丢的,得先把重入的定义、锁的作用,以及编译器在中间做了什么讲清楚。
重入的定义与它最初解决的问题
重入(reentrancy)描述的是这样一种函数特性:函数执行到一半被中断,在它上一次调用还没结束之前,又被安全地重新调用一次。这个概念比区块链早得多,最早出现在单线程程序里——比如递归调用、硬件中断处理。一个可重入函数有几个约束:不依赖全局变量,不修改自身代码,不调用其他不可重入的函数。它和线程安全不是一回事,线程安全涉及多线程竞争,重入讨论的是单线程内的调用嵌套。

用一个简单对照就能看出差别。假设全局变量 i 等于 5,一个函数直接返回 i 的五次方,那么无论谁在什么时候调用,结果永远是 3125。这是不可重入的,因为结果依赖外部状态。另一个函数接收参数 number,返回 number 的五次方,传 2 得 32,传 3 得 243,调用多少次都只依赖传入值,这就是可重入的。
智能合约函数大多不可重入,因为它们要读写余额、总量、授权额度这类共享状态。以太坊的合约调用天然支持嵌套:A 合约调用 B 合约,B 合约在执行过程中还能回头调用 A。这个特性是 DeFi 可组合性的基础,也正好是重入攻击的入口。
锁机制与 Vyper 的 nonreentrant 装饰器
@nonreentrant('lock')
def func():
assert not self.locked, "locked"
self.locked = True
# 业务逻辑
raw_call(msg.sender, b"", value=0)
self.locked = False
# 后续代码
锁是一种同步机制,让某个进程声明对一段数据或一个函数的独占访问。最简单的形式是二进制信号量:进入前检查是否被占用,没占用就置为已占用,执行完再释放。编写并发代码时误用锁会导致死锁或活锁,进程互相阻塞,状态不断变化却没有实际推进。
大多数语言在底层用锁管理子程序之间的状态切换,开发者不直接接触。Vyper 和 C# 属于少数允许在代码里显式写锁的语言。Vyper 提供的是一个装饰器:。这个装饰器的意图很直接:如果 msg.sender 是另一个合约,就阻止它在当前函数跑完之前再次进入。没有锁的话,raw_call 之后的代码还没执行,调用方就可能重新触发上面全部逻辑。在 2023 年之前,这套写法被大量 DeFi 项目当作标准防护,Curve 的池子也用了。
编译器漏洞如何让锁失效
Curve 事件的关键在于,合约里的 @nonreentrant('lock') 确实写了,但 Vyper 编译器在特定版本上没有正确生成对应的锁逻辑。受影响版本是 v0.2.15、v0.2.16 和 v0.3.0,凡是使用这三个版本编译并部署的合约都存在风险。合约源码看起来无懈可击,部署后的字节码里却少了那道闸门。
这就是它和以往大多数 DeFi 被盗案的区别。过去常见的情况是开发者自己写漏了检查,或者把某个内部函数暴露成了 public,攻击者顺着设计缺陷下手。这一次开发者的代码是干净的,问题发生在从源码到 EVM 字节码的翻译环节。审计机构当时也很难发现,因为审计看的是源码,而缺陷只在编译产物里。
攻击者利用的方式可以概括为:在同一个交易内,通过构造恶意合约反复调用目标池的兑换或提款函数。由于锁没有真正生效,第一次调用的状态还没来得及更新,第二次调用就已经读到旧值,池子按错误的价格重复放款。多个池子被同一套手法命中,因为它们的编译版本相同,缺陷是共性而非个案。
对 Vyper 这类语言来说,还有一个容易被忽略的点:版本管理。Solidity 项目普遍在文件头写 pragma 锁定版本,升级编译器需要显式操作。Vyper 项目如果部署时没有记录、没有复核编译产物,事后很难快速判断自己是否落在受影响区间。Curve 事件之后,不少团队做的第一件事是回溯自己合约的编译工具链版本,而不是去看业务逻辑。
对普通用户能看出什么
从用户角度看,这起事件暴露的是一个常被低估的事实:你存入 DeFi 协议的资金,安全性不只取决于协议团队写的代码,还取决于他们用的编译器、依赖库、预言机等一整套上游组件。任何一层出问题,链上资金都可能在几分钟内被搬空,且没有回滚机制。
具体到判断方法,普通用户不需要读字节码,但可以做几件事。第一,看协议是否公开了合约的编译版本与验证信息,Etherscan 上已验证的合约会显示编译器版本,Vyper 合约同样能查到。第二,关注协议在重大安全事件后的公告中是否说明了受影响版本区间和处置方案。第三,理解一件事:审计报告不能覆盖编译器层面的缺陷,有审计不等于安全。
重入攻击本身不是新概念,DAO 事件在 2016 年就让以太坊社区认识了它,此后 Solidity 生态逐步形成了「先改状态、后转账」的检查-生效-交互模式,配合重入锁作为兜底。Curve 这一轮的教训在于,兜底机制本身也可能是坏的。想进一步了解的读者,可以在 Etherscan 上找到文中提到的几个池子的合约地址,查看其 Compiler Version 字段,对照 Vyper 官方在 2023 年 7 月 30 日发布的公告中列出的受影响版本列表,自行确认哪些合约属于风险区间。