📢 GM!Gate 广场|4/5 热议:#假期持币指南
🌿 踏青还是盯盘?#假期持币指南 带你过个“松弛感”长假!
春光正好,你是选择在山间深呼吸,还是在 K 线里找时机?在这个清明假期,晒出你的持币态度,做个精神饱满的交易员!
🎁 分享生活/交易感悟,抽 5 位锦鲤瓜分 $1,000 仓位体验券!
💬 茶余饭后聊聊:
1️⃣ 休假心态: 你是“关掉通知、彻底失联”派,还是“每 30 分钟必刷行情”派?
2️⃣ 懒人秘籍: 假期不想盯盘?分享你的“挂机”策略(定投/网格/理财)。
3️⃣ 四月展望: 假期过后,你最看好哪个币种“春暖花开”?
分享你的假期姿态 👉 https://www.gate.com/post
📅 4/4 15:00 - 4/6 18:00 (UTC+8)
我只是想分享一个许多开发者常忽略的智能合约安全问题——重入攻击。如果你正在用 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。理解重入攻击及其防范措施,将帮助你构建更安全的智能合约。