✍️ Gate 广场「创作者认证激励计划」进行中!
我们欢迎优质创作者积极创作,申请认证
赢取豪华代币奖池、Gate 精美周边、流量曝光等超 $10,000+ 丰厚奖励!
立即报名 👉 https://www.gate.com/questionnaire/7159
📕 认证申请步骤:
1️⃣ App 首页底部进入【广场】 → 点击右上角头像进入个人主页
2️⃣ 点击头像右下角【申请认证】进入认证页面,等待审核
让优质内容被更多人看到,一起共建创作者社区!
活动详情:https://www.gate.com/announcements/article/47889
我只是想分享一个许多开发者常忽略的智能合约安全问题——重入攻击。如果你正在用 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。理解重入攻击及其防范措施,将帮助你构建更安全的智能合约。