智能合约安全 101 — 重入 & 常见 AI 生成错误

发布: (2026年1月12日 GMT+8 14:32)
10 min read
原文: Dev.to

Source: Dev.to

到 Web3 之旅的第 30 天时,“安全”不再是一个令人害怕的审计词汇,而是变得非常真实。

因为第一次“无辜的” withdraw 函数把整个合约的资金抽干时,它根本不像一次黑客攻击。它看起来像……一次正常的付款。

  1. 用户点击 Withdraw
  2. 合约向他们发送了一些 ETH。
  3. 一切看起来都正常——只是付款实际上并没有结束。它一直在回调。一次又一次地回调。

等到合约“意识到”发生了什么时,它的余额已经被抽空。

这就是 重入攻击——而在 AI 帮助我们比以往更快编写 Solidity 的今天,意外地将此类漏洞发布变得极其容易。

重入攻击到底是什么

把合约想象成自动售货机:

  1. 你投币。
  2. 机器检查你的余额。
  3. 然后它更新余额 并且掉落你的零食。

现在想象一个 bug:在零食门仍然打开时,你可以一次又一次地按“发放”按钮,在机器更新你的余额之前不断取零食。机器仍然以为你只拿了一件,却让你一直拿走零食。

这就是 Web3 中的重入攻击:

  • 智能合约向另一个地址/合约发送 ETH 或代币。
  • 在该转账进行时,接收方会运行代码。
  • 该代码在原合约的余额或状态更新 之前 调用回原合约的函数。
  • 攻击者重复这个循环,耗尽资金。

关键点: 合约 信任 外部调用只是一次“转账”。但在以太坊中,接收方可以是拥有自己逻辑的合约。

经典重入陷阱(以及 AI 为何喜欢它)

以下是 AI 工具在你请求“简单的 withdraw 函数”时常生成的易受攻击模式:

// 1️⃣ 检查 msg.sender 是否有足够的余额
require(balances[msg.sender] >= amount, "Insufficient balance");

// 2️⃣ 发送 ETH
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");

// 3️⃣ 更新余额
balances[msg.sender] = 0;

这在普通语言里看起来完全合乎逻辑,但 操作顺序 却致命:

  1. 外部调用被发出(指向攻击者合约)。
  2. 攻击者的 fallback 函数执行并再次调用 withdraw()
  3. 原合约仍认为攻击者还有余额,因为余额尚未被设为 0。
  4. 资金不断流出,直至合约余额耗尽。

AI 模型常常:

  • 使用 call{value: …}("") 却未提及重入风险。
  • 忘记使用重入保护(reentrancy guard)或 checks‑effects‑interactions 模式。
  • 在你要求“简单”“最小”或“省 gas”代码时,优先给出“可运行示例”,而非“安全示例”。

代码可以编译,测试可以通过,但主网不会宽恕。

Checks‑Effects‑Interactions: Your First Shield

要防止重入攻击,翻转自动售货机的逻辑:

  • 原来是: “先发送零食 → 再更新余额”
  • 改为: “先更新余额 → 再发送零食”

在 Solidity 中,这种做法称为 checks‑effects‑interactions(检查‑效果‑交互):

阶段你要做的事
Checks(检查)验证条件(require 语句)。
Effects(效果)更新内部状态(余额、映射等)。
Interactions(交互)最后调用外部合约或发送 ETH。

为什么有效: 当攻击者尝试重入时,你的合约状态已经变为 “你没有余额”。即使他们再次调用,require 也会失败。

只要看到 calldelegatecalltransfer 后面紧跟状态更改,你的 “重入雷达” 就应该响起。

可重入性防护与安全模式

现代 Solidity 提供了额外的安全网,几乎在所有情况下都应当使用:

  • ReentrancyGuard – 一个简单的“锁定”标志,阻止函数在完成之前再次被调用。
  • Pull‑over‑push 支付 – 让用户在受控的最小函数中自行提取,而不是自动发送 ETH。
  • 最小化外部调用 – 外部调用越少,潜在的可重入攻击面就越少。

可以把 ReentrancyGuard 想象成在自动售货机门上挂了一个 “忙碌中” 的标志:当一件零食正在出货时,任何人都不能再按按钮——包括试图通过后门刷按钮的同一个人。

作为初学者(尤其是使用 AI 时):

  • 默认在任何发送 ETH 或调用不可信合约的函数上使用 ReentrancyGuard
  • 只有在完全理解为何在没有它的情况下也是安全的情况下才移除它。

Common AI‑Generated Security Mistakes (Beyond Reentrancy)

Reentrancy 只是一类问题。AI 往往会重复几种危险的模式:

MistakeWhy it’s risky
Missing access control – admin‑only functions (setPrice, pause, withdrawAll) left as public.Anyone can call them.
Blind trust in msg.sender – no role checks (onlyOwner, AccessControl).No protection against unauthorized actors.
Unbounded loops over arrays – “simple” loops over large user lists that can run out of gas.Perfect for griefing or DoS.
Ignoring return values – not checking if token transfer or call actually succeeded.Silent failures that leave funds stuck.

所有这些在表面上看起来 合理。正是因为如此,它们才危险:问题不在编译器,而是在实际运行时出现。

作为初学者,你的任务不是写出“完美”的代码,而是要注意哪些地方 可能 出错,并在此放慢脚步。

为什么这对开发者很重要

重入不是“仅仅是另一个 bug”。它是一种会:

  • 把金库掏空。
  • 破坏用户信任。
  • 永久在链上追随你的名字。

思考这个(小挑战)

下次你向 AI 工具提问时:

“编写一个简单的 Solidity 合约,允许用户存入和提取 ETH。”

请按以下步骤操作:

  1. 扫描外部调用calltransfersend)。
  2. **检查顺序:**函数是在发送 ETH 之前还是 之后 更新状态?
  3. 问问自己:“如果接收方是一个在此处回调的合约,会发生什么?”

如果你感到哪怕一点点怀疑,那就说明你的安全直觉已经被唤醒了。

关键要点

重入攻击并不是魔法。它只是 “我在锁好自己的房子之前就信任了外部代码。”

随着 AI 开始编写我们更多的智能合约代码,请确保你始终:

  • 应用 checks‑effects‑interactions(检查‑效果‑交互)原则。
  • 使用 ReentrancyGuard(或等效的方案)。
  • 仔细审查每一次外部调用。

保持安全,保持好奇,并负责任地继续构建。

Solidity,你的优势不会是打字更快。
而是 发现 AI 留下的陷阱

不要以成为房间里最聪明的审计员为目标。
而是要成为那个永远不发布明显漏洞的开发者。

因为在 Web3 中,一个“无辜的”withdraw函数可能决定:

  • “你部署的那个不错的小 dApp。”
  • “还记得那个被抽干的合约吗?是的……那是我的。”

深入资源

Back to Blog

相关文章

阅读更多 »

打破我‘完美’合约的测试

第31天——为什么开发工具比以往任何时候都更重要 第一次测试摧毁了我的“完美”智能合约,并不是黑客,而是我自己的开发环境。我…

Web3 商店

概述 我编写并部署了一个演示 Web3 商店,使用 Solidity 和 ethers.js 作为学习练习。原本较旧的书籍推荐的工具是 web3.js,T...