🧠 一、什么是重入攻击(Reentrancy Attack)?
想象一下这个场景 👇:
-
你写了一个智能合约,里面有一个函数叫
withdraw()
,功能是让用户提款。 -
这个函数会先检查用户余额,然后调用
send()
或者调用用户的合约来转账给他,最后再更新用户的余额。 -
但是!你先转账、后更新余额!
-
这时,如果用户是一个恶意合约 ,它在收到转账后,可以立刻再次调用你的 withdraw() 函数(也就是在转账回调里再次触发你的代码)。
-
因为你的余额还没更新,所以它又能通过检查,又拿到钱......如此循环,直到你的合约没钱了。
🔥 这就是经典的重入攻击!
攻击的关键点在于:
你在"外部调用(比如给用户转账)之后,才修改内部状态(比如扣余额)",导致攻击者可以在状态还未更新时,再次进入你的函数,重复执行敏感操作。
🛡️ 二、ReentrancyGuard 是怎么防止这种攻击的?
它的核心思路非常简单但极其有效 👇:
在函数执行前加一把"锁"(状态标记),函数执行期间不允许其他函数(尤其是外部递归调用)再次进入,等函数执行完才解锁。
它用一个状态变量 _status
来标记当前合约是不是正在执行某个"受保护"的函数:
状态值 | 含义 | 是否允许进入受保护函数 |
---|---|---|
NOT_ENTERED = 1 |
当前没有函数在执行,是安全的 | ✅ 可以进入 |
ENTERED = 2 |
已经有一个函数在执行中,不能重入 | ❌ 不能进入,会报错 |
核心保护流程:
-
你在一个函数前面加上
nonReentrant
修饰器 -
函数执行前,先检查状态:
-
如果已经是
ENTERED
(有人正在执行受保护函数),就直接报错,拒绝再次进入 -
如果是
NOT_ENTERED
,就把状态设置为ENTERED
,表示"我正在运行,别人别进来了"
-
-
执行你的函数逻辑(比如转账、状态变更等)
-
函数执行完后,再把状态恢复为
NOT_ENTERED
,允许其他函数正常调用
这样,即使有恶意合约试图在转账后立刻再次调用你的函数,也会因为状态是 ENTERED
而被挡住,无法重入!
🧩 三、你代码中的核心部分解析
1. NOT_ENTERED = 1
和 ENTERED = 2
这两个是状态常量,用来表示当前合约是不是正在执行某个需要防重入的函数。
-
NOT_ENTERED
(1):安全状态,可以进入函数 -
ENTERED
(2):危险状态,不能再进,防止重入
2. 状态变量:uint256 private _status;
这是核心!它记录当前合约是不是正在执行某个加了 nonReentrant
修饰器的函数。
-
初始值是
NOT_ENTERED
,表示一开始大家都可以安全调用 -
一旦某个受保护的函数开始执行,它就会被设为
ENTERED
-
函数执行完,再设回
NOT_ENTERED
3. 修饰器:modifier nonReentrant()
这是用来保护你的函数的核心武器!使用方法超级简单:
javascript
function withdraw() public nonReentrant {
// 你的逻辑,比如检查余额、转账、更新状态
}
这个修饰器做了三件事:
-
_nonReentrantBefore() → 检查状态,如果已经有人在执行了,就报错;否则,把状态设为"正在执行"
-
执行你写的函数逻辑(就是函数体)
-
_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;
}
}