免费试读阅读约需 10 分钟
重入:最老的招数,至今还管用
一个先把钱转出去、之后才更新自己记录的合约,可以在转账还没结束时被再要一次这笔钱。这就是重入,Ethereum 历史上第一场著名灾难背后的漏洞,而它至今还在找到新的受害者。
这套机制
- 一个合约替存款人记录余额,并提供一个提取函数。
- 提取函数先把资金转出,然后再把这位存款人的余额置为零。
- 把资金转给另一个合约,就把控制权交给了那个合约的代码。
- 那段代码再次调用提取函数。余额还没被清零,所以检查第二次也通过了。
- 这个循环一直重复,直到资金池被掏空。
修复办法是一条顺序规则,叫做“检查、更新、交互”:先校验,再更新自己的状态,最后才和别人打交道。重入锁,也就是在第一次调用还没结束时拒绝第二次进入的那把锁,是这条规则之外再加的一道保险。
它在哪里发生过
- The DAO,2016年6月。一个重入漏洞从一只建在 Ethereum 上的投资基金里抽走了约 360 万 ETH。应对方式让这条链分裂了:多数人分叉出去撤销了这次盗窃,而拒绝撤销的那条链,今天仍以 Ethereum Classic 的名字继续运行。
- Curve Finance,2023年7月。漏洞不在 Curve 自己的逻辑里,而在 Vyper 编译器的某些特定版本中,那里的重入锁并没有按写出来的样子生效。几个资金池被掏空,据报损失约 7000 万美元。经过审计的代码,仍然可能被编译它的工具毁掉。
它为什么还活着
- 任何一次外部调用都可能交出控制权:代币转账钩子、回调,以及会通知接收方的较新代币标准。
- 只读重入更隐蔽。攻击者在状态更新到一半时重新进入一个只读函数,于是另一个协议读到错误的价格或余额,并据此行动。
- 跨函数重入走的是另一个共用同一份状态的函数,所以只在一个函数上加锁并不够。
真正能防住它的做法
- 把“检查、更新、交互”用在所有地方,不只是那个显眼的提取函数里。
- 在每一个会改变状态又会发起外部调用的入口上都加重入锁。
- 把每一个代币都当作敌对代码,因为代币合约会在转账过程中运行它自己的逻辑。
- 写从恶意接收方回调进合约的测试,这是在上线之前发现这个问题的办法。
这是教育内容,不构成财务建议。这里的任何内容都不是在推荐你使用或避开任何协议。