我只是想分享一个许多开发者常忽略的智能合约安全问题——重入攻击。如果你正在用 Solidity 构建智能合约,这一点你必须要理解清楚。



简单来说,重入攻击发生在一个合约调用另一个合约时,而被调用的合约可以在执行过程中再次调用原始合约。假设你有 ContractA,存有 10 Ether,ContractB 向其发送 1 Ether。当 ContractB 提取资金时,ContractA 会检查余额是否大于 0,如果是,就将 Ether 发送出去。然而,如果 ContractB 有一个 fallback 函数(“备用函数”),它可以在 ContractA 还未完成执行时再次调用 ContractA 的提取函数。结果呢?ContractB 的余额仍然显示为 1 Ether,它就会再次收到 1 Ether,循环往复,直到 ContractA 的资金耗尽。

这种攻击方式是怎么运作的?攻击者需要两个东西:一个 attack() 函数来启动攻击,以及一个 fallback 函数用来再次调用提取函数。fallback 函数是一个特殊的外部函数,没有名字,没有参数,任何人都可以通过调用不存在的函数、没有传递数据,或者直接发送 Ether(不带数据)来触发它。

举个具体例子:EtherStore 合约有一个 deposit() 函数用来存储余额,以及一个 withdrawAll() 函数用来提取全部资金。问题在于,withdrawAll() 会先检查余额,发送 Ether,然后才将余额重置为 0。这就留下了重入攻击的空隙。

那么,如何防御呢?我会介绍三种方法。

第一,用 noReentrant 修饰符。思路非常简单:在函数执行时锁住合约。如果有人试图再次调用该函数,必须先通过锁的检查,但锁会在函数结束后才解开。修饰符是一种特殊的函数,可以让你在不重写全部逻辑的情况下,为函数添加条件。

第二,采用“检查-效果-交互”模式。不要先检查条件、再转账、最后更新余额,而是先进行条件检查,立即更新余额(“在转账之前”),然后再进行外部交互。这样,即使发生重入,余额也已变为 0,攻击者就无法再提取更多资金。

第三,如果你的项目涉及多个合约交互,建议使用 GlobalReentrancyGuard。不是只锁定单个函数,而是用一个存储在单独合约中的状态变量锁住整个系统。当任何合约中的任何函数被调用时,系统会检查是否已被锁定。如果已锁定,交易会被拒绝。这在你有像 ScheduledTransfer 这样向 AttackTransfer 发送资金的合约时特别有用,GlobalReentrancyGuard 能阻止整个重入链攻击。

这三种方法的妙处在于可以结合使用,视情况而定。一个重要函数?用 noReentrant。多个相关函数?用“检查-效果-交互”。整个项目复杂?用 GlobalReentrancyGuard。理解重入攻击及其防范措施,将帮助你构建更安全的智能合约。
查看原文
此页面可能包含第三方内容,仅供参考(非陈述/保证),不应被视为 Gate 认可其观点表述,也不得被视为财务或专业建议。详见声明
  • 赞赏
  • 评论
  • 转发
  • 分享
评论
请输入评论内容
请输入评论内容
暂无评论