一句话解释
重入攻击(Reentrancy)是利用合约“先执行外部调用、后更新自身状态”的时序漏洞,让同一个函数在一次交易里被反复触发、资金被循环提走的攻击方式。它是智能合约最经典的漏洞类型之一,名字来源于程序执行顺序被“重新进入”打乱。
正常流程与被打断的流程
假设一个存提款合约允许用户取回自己的存款。正常流程是:验证余额、把用户的记账余额清零、再转款。如果合约写成“先转款、后清零”,攻击者就可以在转款这一步做文章。
以太坊上,合约向外转账时,如果接收方也是一个合约,EVM 会把控制权交给接收方的回退函数(fallback 或 receive)。攻击者就在自己的合约里写好回退函数:收到钱后不罢休,立刻再次调用提款函数。此时受害合约的记账余额还没来得及清零,于是同一段资金被第二次、第三次提出,直到金库被搬空或触达调用深度限制。一次交易里,提款函数被“重入”了很多层。
The DAO:历史给的答案
2016 年 6 月,去中心化投资基金 The DAO 的分裂(splitDAO)函数存在这一时序缺陷,攻击者累计提取了约 360 万枚 ETH,占当时基金规模的四分之一左右,并最终促成了以太坊社区通过硬分叉回滚资金、拒绝回滚的分叉链成为以太坊经典(ETH 与 ETC 由此分开)。这一事件直接推动了以太坊的路径重数组和后来的 EIP-150 调整调用气体规则,重入防护也从此成为合约开发的必考题(分叉经过可参考重组和分叉的概念之外的治理型分叉,两者机制不同,注意区分)。
防御:先记账,再给钱
社区总结出的标准纪律叫 checks-effects-interactions:先做检查(checks),再更新自身状态(effects),最后才与外部交互(interactions)。只要保证“控制权交出去之前账本已经改完”,循环提款就失去了抓手。工程上还有两道常见保险:一是重入锁(mutex),在函数执行期间拒绝再次进入;二是 OpenZeppelin 等安全库提供的 ReentrancyGuard 修饰器。需要注意的是,还有更隐蔽的变体,比如只读重入(利用价格预言函数在状态未更新时读到旧值)和跨合约、跨代币的重入,防御思路仍然是同一句:不要把未结账本的状态暴露给不可信的外部调用。
普通用户看什么
普通人无法逐行审代码,但可以在审计报告和安全页里检索关键词“reentrancy”,确认审计方明确测试过重入路径、开发方是否部署了重入锁(部分合约的源码在浏览器里可直接查看,方法见区块链浏览器怎么用)。对收益率异常诱人的DeFi金库,缺审计、拒绝披露合约地址或合约不可验证,都是比“收益高”更响的警报。
快速问答
问:重入攻击只发生在转账合约上吗? 答:不是。任何“外部调用之后才更新状态”的逻辑都可能中招,铸造、赎回、治理投票权重计算都出现过实例,只读重入甚至不涉及转账。
问:加一层 require 检查余额不就行了? 答:不够。require 检查的是当前值,重入发生时检查依据的状态本身还没更新,条件依然成立。修复必须改变执行顺序或加互斥锁。
问:被重入偷走的钱能追回吗? 答:链上转账不可逆,只能靠协议方发起协调或治理行动,The DAO 那种全网回滚属于极小概率的例外。
三个常见误区
误区一:“我的合约很小,不值得被攻击。”攻击者扫描的是全网所有已部署字节码,模板复制粘贴时把漏洞也一起复制了,绝大多数重入受害合约都是直接使用或魔改公开模板的中小项目。误区二:“加个转账成功判断就能挡住。”外部调用返回成功不代表对方合约没有在回调里完成重入,判断依据本身可能被掏空。误区三:“审计过了就绝对安全。”审计是时点性的,只对提交的版本与当时的攻击面负责,后续升级、代理合约换实现、参数变更都可能把防线改薄。把这三条反过来读就是正确姿势:小项目也要按标准模式写、防御逻辑要看执行顺序而不是返回值、每次改动都重新过一遍安全检查。
风险提示:本文仅作技术科普,不构成任何投资建议或安全审计意见;与智能合约交互前请自行核实合约来源与审计信息。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。