重入锁如果写进虚拟机:EIP-7705 想用两条操作码换掉每份合约里的锁代码 图 1
重入锁如果写进虚拟机:EIP-7705 想用两条操作码换掉每份合约里的锁代码 · 图 1

自从 2016 年 The DAO 事件之后,「先改账、再转账」的防重入纪律就成了以太坊智能合约开发的第一课。但纪律归纪律,执行它的方式一直是各合约自己写代码:要么用一个布尔变量当门锁,要么借助 EIP-1153 引入的临时存储记一个标志位。EIP-7705 的思路是干脆把这把锁下沉到虚拟机:增加 NONREENTRANTREENTRANT 两条新操作码,让「本合约此刻拒绝被再次进入」成为协议层的一个开关,而不是应用层的一段代码。这份提案在 2024 年 5 月创建,属于 Standards Track Core 类,目前状态是 Stagnant(停滞),没有进入任何已排期的主网升级。

两条操作码各自做什么

按提案文本,NONREENTRANT 的字节码是 0xF6REENTRANT0xF7。合约执行到 NONREENTRANT 时,给自己挂上一个「非重入」标志;此后任何通过 CALLSTATICCALLDELEGATECALL 进来的调用都会失败,效果等价于进来就执行一次 REVERT。要重新开放入口,合约再执行 REENTRANT 清掉标志。两条操作码都是幂等的:已经挂了锁再挂一次没有副作用,已经开着门再开一次也一样。提案把它们的成本都定为 5 gas,与常见的中档操作码同价——相比在存储或临时存储里读写标志位,这几乎是免费的保护。

标志位的作用域与回滚语义是重点

真正值得逐字读的是边界条款。第一,标志只活在当前这一笔交易里:交易结束,所有合约的锁全部自动解除,不存在跨交易残留的「忘了解锁」。第二,遇到 REVERT 或异常终止时,合约的非重入标志回滚到进入那次调用之前的值,和余额、存储的回滚规则保持一致。这两条决定了它不会变成一种新型拒绝服务工具:攻击者无法通过触发回滚把某个合约永久锁死,下一笔交易开始时一切归零。对开发者来说,等价的心智模型是「每笔交易开始时全部门开着,函数头部执行一次 NONREENTRANT 就等于挂上交易级的一次性门牌」。

和临时存储、检查锁相比省了什么

传统重入锁要占一个存储槽:读写一次就是几百到两千多 gas,还引入「外部调用前必须改锁」的排序义务。EIP-1153 的临时存储把锁的读写降到大约一次热访问的价位,已经很便宜,但仍要占槽位、要显式管理。7705 把这一切压成一条指令:不用选槽位、不用记地址、不用担心锁和状态更新谁先谁后。提案动机部分直言,重入攻击仍占 EVM 链上被盗资金的相当比例,很多项目为省成本根本没上锁;如果挂锁的成本降到五 gas,「默认挂锁」才可能成为现实。当然它也有代价:锁是合约级粗粒度的,同一合约内不同函数想有不同的重入策略时,仍然要靠调用顺序自己编排。

一条判断线

这份提案的状态词值得多读一遍。Stagnant 在 EIP 流程里意味着作者团队一段时间内没有推进,任何感兴趣的人都可以认领复活;它既不是被否决,也不是被排期。截至本文写作,以太坊主网的重入防御仍然完全依赖应用层代码,两条 0xF60xF7 操作码在现行 EVM 中是未定义字节。判断相关说法时看这条线:说「协议内置重入锁」的,讲的是尚未激活的草案;说「需要合约自己写锁」的,才是在讲现行规则。

快速问答

问:这两条操作码现在能用吗? 答:不能。它们只存在于提案文本里,未激活、未进主网,现行 EVM 中对应字节是无效的。

问:它和 EIP-1153 的临时存储是竞争关系吗? 答:更像同一问题的两个价位。临时存储是「更便宜的记账式锁」,7705 是「直接内置的开关」,后者的动机段明确把 1153 当作已经落地、但还不够便宜的前置背景。

问:挂锁之后自己调用自己也会被拒吗? 答:提案限制的是经由各类 *CALL 进入的上下文转移;跨合约调用的拒止效果明确,链上执行细节以提案文本为准。

风险提示

本文是机制解读,不构成投资建议,也不构成对任何合约安全性的保证。智能合约交互存在漏洞、授权误签与协议变更风险,任何把资金转入合约地址的行为都应先行审计与小额测试。提案内容可能随社区讨论修改或作废,请以提案仓库页面为准。