【大白话解析】OpenZeppelin 的 ReentrancyGuard 库:以太坊防重入攻击安全工具箱(附源代码)


🧠 一、什么是重入攻击(Reentrancy Attack)?

想象一下这个场景 👇:

  1. 你写了一个智能合约,里面有一个函数叫 withdraw(),功能是让用户提款。

  2. 这个函数会先检查用户余额,然后调用 send()或者调用用户的合约来转账给他,​​最后再更新用户的余额​​。

  3. ​但是!你先转账、后更新余额!​

  4. 这时,如果用户是一个​​恶意合约​ ​,它在收到转账后,可以​​立刻再次调用你的 withdraw() 函数​​(也就是在转账回调里再次触发你的代码)。

  5. 因为你的余额还没更新,所以它又能通过检查,又拿到钱......如此循环,直到你的合约没钱了。

🔥 ​​这就是经典的重入攻击!​

攻击的关键点在于:

​你在"外部调用(比如给用户转账)之后,才修改内部状态(比如扣余额)",导致攻击者可以在状态还未更新时,再次进入你的函数,重复执行敏感操作。​


🛡️ 二、ReentrancyGuard 是怎么防止这种攻击的?

它的核心思路非常简单但极其有效 👇:

​在函数执行前加一把"锁"(状态标记),函数执行期间不允许其他函数(尤其是外部递归调用)再次进入,等函数执行完才解锁。​

它用一个状态变量 _status来标记当前合约是不是正在执行某个"受保护"的函数:

状态值 含义 是否允许进入受保护函数
NOT_ENTERED = 1 当前没有函数在执行,是安全的 ✅ 可以进入
ENTERED = 2 已经有一个函数在执行中,不能重入 ❌ 不能进入,会报错

核心保护流程:

  1. ​你在一个函数前面加上 nonReentrant修饰器​

  2. 函数执行前,先检查状态:

    • 如果已经是 ENTERED(有人正在执行受保护函数),就​​直接报错,拒绝再次进入​

    • 如果是 NOT_ENTERED,就把状态设置为 ENTERED,表示"我正在运行,别人别进来了"

  3. 执行你的函数逻辑(比如转账、状态变更等)

  4. 函数执行完后,再把状态恢复为 NOT_ENTERED,允许其他函数正常调用

这样,即使有恶意合约试图在转账后立刻再次调用你的函数,也会因为状态是 ENTERED而被挡住,无法重入!


🧩 三、你代码中的核心部分解析

1. NOT_ENTERED = 1ENTERED = 2

这两个是状态常量,用来表示当前合约是不是正在执行某个需要防重入的函数。

  • NOT_ENTERED(1):安全状态,可以进入函数

  • ENTERED(2):危险状态,不能再进,防止重入


2. 状态变量:uint256 private _status;

这是核心!它记录当前合约是不是正在执行某个加了 nonReentrant修饰器的函数。

  • 初始值是 NOT_ENTERED,表示一开始大家都可以安全调用

  • 一旦某个受保护的函数开始执行,它就会被设为 ENTERED

  • 函数执行完,再设回 NOT_ENTERED


3. 修饰器:modifier nonReentrant()

这是用来保护你的函数的核心武器!使用方法超级简单:

javascript 复制代码
function withdraw() public nonReentrant {
    // 你的逻辑,比如检查余额、转账、更新状态
}

这个修饰器做了三件事:

  1. ​_nonReentrantBefore()​​ → 检查状态,如果已经有人在执行了,就报错;否则,把状态设为"正在执行"

  2. ​执行你写的函数逻辑(就是函数体)​

  3. ​_nonReentrantAfter()​​ → 把状态恢复为"未执行",允许别人下次再调用


4. 修饰器:modifier nonReentrantView()

这个是给只读函数(比如 view / pure 函数)用的,它​​只检查状态,不修改状态​​,也不会真的阻止函数运行,但可以防止在只读操作期间发生意外的重入。

一般用得少,但某些高级场景可能有用。


5. 私有函数:_nonReentrantBefore()_nonReentrantAfter()

  • _nonReentrantBefore():检查状态,如果已经处于 ENTERED,就报错;否则设为 ENTERED

  • _nonReentrantAfter():把状态重置回 NOT_ENTERED,表示函数执行完毕,可以再次调用


6. 自定义错误:error ReentrancyGuardReentrantCall();

当检测到重入调用时,它不会用传统的 require(false, "msg"),而是直接 revert一个自定义错误。

✅ 好处是:​​更省 gas,更清晰,更容易在调试工具中识别错误类型​


7. 辅助函数:_reentrancyGuardEntered()

这是一个内部函数,其他函数或合约可以调用它来​​查询当前是否已经有函数在受保护状态下执行中​​。

返回 true表示有人正在执行,返回 false表示安全。


✅ 四、这个库(ReentrancyGuard)有什么用?适用于哪些场景?

场景 是否适用 说明
用户提款函数(withdraw) ✅ 必须使用 经典的重入攻击场景,一定要用 nonReentrant 保护
转账、支付类函数 ✅ 推荐使用 任何涉及外部调用 + 状态变更的函数都要小心重入
奖励发放、空投领取 ✅ 推荐使用 防止用户通过重入多次领取
合约之间的交互(调用其他合约) ✅ 推荐使用 尤其是对方合约不可信时
只读函数(view / pure) ⚠️ 可选 一般不需要,但如果你担心意外重入逻辑,可以用 nonReentrantView

🔐 ​​特别提醒:只要你在一个函数中调用了外部合约(比如 transfer、call、delegatecall),并且这个调用之后还修改了你的状态(比如余额),你就应该用 nonReentrant 保护它!​


🧠 五、总结

这个 ReentrancyGuard合约,是一个​​用来防止"重入攻击"(Reentrancy Attack)的安全工具​ ​,它通过一个状态锁机制,确保某些函数在执行期间不会被同一个或外部合约​​重复调用(重入)​​,从而避免资金被盗、状态混乱等安全问题。

问题 答案
这个合约是干嘛的? 防止智能合约中的"重入攻击",保护你的函数不被恶意重复调用
什么是重入攻击? 攻击者通过外部调用,在你的函数还没执行完时又重新进入,反复捞钱或搞破坏
怎么防止的? 用一个状态锁(ENTERED / NOT_ENTERED),函数执行期间禁止重入
怎么用? 在需要保护的函数前加 nonReentrant修饰器即可
适用于哪些函数? 所有涉及"外部调用 + 状态变更"的函数,尤其是提款、转账、奖励发放等
和 OpenZeppelin 的区别? 几乎一样,你这是手写版 / 复刻版,官方的更权威、经过更多审计
为什么要用? 避免资金损失、防止合约被黑、是 Solidity 安全编程的基本功

✅ 如果你要在实际项目中使用,推荐这样做:

javascript 复制代码
// 直接使用 OpenZeppelin 官方库,安全、省事、经过审计
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract MySafeContract is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No balance");
        balances[msg.sender] = 0; // 先改状态
        payable(msg.sender).transfer(amount); // 后调用外部
    }
}

🏁 六、源代码

javascript 复制代码
// SPDX-License-Identifier: MIT
// 声明该代码使用的开源许可证是 MIT 许可证,意味着可以自由使用、修改和分发,只要保留版权声明即可

pragma solidity ^0.8.20;
// 指定该智能合约使用 Solidity 编译器版本 0.8.20 或更高但小于 0.9.0 的版本

// 定义一个抽象合约(不能直接部署,只能被继承使用),名字叫 ReentrancyGuard
// 这个合约的主要功能是防止"重入攻击"(Reentrancy Attack)
abstract contract ReentrancyGuard {

    // 定义两个常量,用来表示当前合约是否正在被某个 nonReentrant 函数调用中
    // 这两个值会存储在状态变量 _status 中,用来标记当前状态
    uint256 private constant NOT_ENTERED = 1;  // 表示当前没有函数在执行中,可以安全进入
    uint256 private constant ENTERED = 2;     // 表示已经有函数在执行中,不能再进入,防止重入

    // 状态变量,记录当前合约是否处于"函数执行中"的状态
    // 初始值为 NOT_ENTERED(1),表示一开始没有任何函数在执行
    uint256 private _status;

    // 定义一个自定义错误,当检测到重入调用时抛出这个错误
    // 这样可以更节省 gas,并且更清晰地表达错误原因
    error ReentrancyGuardReentrantCall();

    // 构造函数,在合约部署时自动执行一次
    // 将 _status 初始化为 NOT_ENTERED,表示一开始是安全的,没有函数在执行
    constructor() {
        _status = NOT_ENTERED;
    }

    // 这是一个修饰器(modifier),用于保护函数,防止被重入调用
    // 使用方法:在需要防止重入的函数定义前加上 "nonReentrant" 关键字
    // 比如:function withdraw() public nonReentrant { ... }
    modifier nonReentrant() {
        _nonReentrantBefore();  // 在执行函数逻辑之前,先检查并设置状态,防止重入
        _;                      // 这里是函数本身的代码,即被修饰的函数体
        _nonReentrantAfter();   // 在函数执行完之后,重置状态,允许后续调用
    }

    // 这也是一个修饰器,但是是专门给"只读函数"(view/pure)使用的
    // 它只检查是否处于 reentrant 状态,但不会修改 _status,因为 view 函数通常不改变状态
    modifier nonReentrantView() {
        _nonReentrantBeforeView();  // 只检查是否已经处于 ENTERED 状态
        _;                          // 执行被修饰的 view 函数
    }

    // 私有函数,仅检查当前状态,如果已经在执行中(ENTERED),就报错
    // 这个函数被 nonReentrantView 和 nonReentrant 都调用了
    function _nonReentrantBeforeView() private view {
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();  // 如果状态是 ENTERED,说明有函数正在执行,不允许其他函数进入,抛出错误
        }
    }

    // 私有函数,在调用真正的函数逻辑之前执行
    // 先调用 _nonReentrantBeforeView() 检查状态,然后将状态设置为 ENTERED,表示已经有函数在执行了
    function _nonReentrantBefore() private {
        _nonReentrantBeforeView();  // 检查当前是否可以进入函数
        _status = ENTERED;          // 设置状态为"正在执行",防止其他函数再次进入
    }

    // 私有函数,在函数执行完之后调用,用于重置状态
    // 把 _status 重新设回 NOT_ENTERED,表示当前没有函数在执行了,其他函数可以安全进入
    function _nonReentrantAfter() private {
        _status = NOT_ENTERED;  // 重置状态,允许后续的函数调用
    }

    // 内部函数,用于查询当前是否已经有函数在 nonReentrant 保护下执行中
    // 返回 true 表示已经有函数在执行(即处于 ENTERED 状态),返回 false 表示安全
    // 这个函数可以被继承的合约内部使用,用于自己判断当前状态
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}
相关推荐
TechubNews16 小时前
Moonchain:「新加坡大华银行」加持下连接现实金融与链上经济的价值通道
金融·区块链
taxunjishu1 天前
基于 CC-Link IE FB 转 DeviceNet 技术的三菱 PLC 与发那科机器人在汽车涂装线的精准喷涂联动
网络·人工智能·物联网·机器人·自动化·汽车·区块链
Sui_Network1 天前
Yotta Labs 选择 Walrus 作为去中心化 AI 存储与工作流管理的专用数据层
大数据·javascript·人工智能·typescript·去中心化·区块链
木西1 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(流动性挖矿-合约部分)
web3·智能合约·solidity
大翻哥哥2 天前
Python 2025:量子计算、区块链与边缘计算的新前沿
python·区块链·量子计算
AWS官方合作商2 天前
构建企业级区块链网络:基于AWS EC2的弹性、高可用解决方案
网络·区块链·aws
草原猫2 天前
区块链版权存证的法律效力与司法实践
区块链·区块链版权存证
空中湖2 天前
solidity从入门到精通 第七章:高级特性与实战项目
区块链·solidity
drjava_20192 天前
DeFi代币授权安全指南:保护您的数字资产
安全·区块链