重入攻击详解:Solidity中如何防止重入攻击

Article author

重入攻击仍然是Ethereum及其兼容EVM链上智能合约安全中最臭名昭著且持久的威胁之一。尽管业界多年来持续努力,但新协议仍然面临重入漏洞,导致重大经济损失——有时高达数百万美元。这类攻击经常出现在DeFi借贷、质押及桥接合约中,这些合约中外部调用和代币转移非常常见。

本文将剖析重入攻击的机制,识别典型的易受攻击合约模式,并分享经过验证的缓解策略。结合Soken对255份以上智能合约审计的分析,我们强调细致的Solidity编码规范与全面的测试流程如何有效阻止重入攻击。文章还链接至委托调用(delegatecall)误用和闪电贷(flash loan)策略的相关风险,提供Web3开发者和安全审计员不可或缺的全面理解。


什么是重入攻击?它如何利用智能合约漏洞?

重入攻击发生在恶意合约在初始执行未完成前,反复调用易受攻击合约的函数,借助合约状态的不一致性进行利用。

实际上,重入漏洞源于合约内部状态更新顺序和外部调用的安排不当。当合约通过call或transfer等方式向不可信地址转移ETH或代币时,接收方在状态变量更新前,可以递归地重新进入易受攻击函数。这使攻击者能在状态同步前抽干资金或操纵合约逻辑。

举例来说,臭名昭著的2016年DAO攻击通过重入漏洞被利用,损失近6000万美元——这成为一部警示录。业界后来认识到,根本原因是合约余额或内部状态仅在外部调用完成后才更新,形成了攻击者利用的状态不一致窗口。

Soken审计专家见解:“在我们最近超过40%的DeFi安全审计中,都能发现某种形式的重入模式,常涉及代理合约或多层调用的微妙变化。识别这些漏洞需要深入的跨合约调用分析,单一合约检查是远远不够的。”


导致重入漏洞的常见Solidity代码模式

重入漏洞通常源于以下操作顺序:在合约内部状态变量更新之前先执行外部调用(如ETH转账或代币调用)。

最易受攻击的典型模式如下:

function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount);
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
    balances[msg.sender] -= amount;
}

这里,通过call.value()的外部调用发生在用户余额扣除之前。该外部调用期间,攻击者的回退函数可以递归调用withdraw(),在余额更新之前抽干资金。

关键风险构造包括:

  • 状态更新前执行外部调用: 在更新余额之前发送ETH或代币。
  • 使用callsend时无防护措施: 低级调用绕过安全检查,可触发回退函数。
  • 缺失重入锁: 无互斥锁或其他锁机制。
  • 复杂调用链包含delegatecall: 可能通过被调用合约中的外部调用间接引入重入风险。

相比之下,更安全的代码模式是将顺序反转:

function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;  // 先更新状态
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

如何防止Solidity中的重入攻击:最佳实践与工具

防范重入攻击最有效的手段是在外部调用前更新状态,结合显式的重入保护模式

标准缓解技巧:

  1. Checks-Effects-Interactions 模式:
    始终先修改内部状态,再进行外部调用。这大幅减少外部调用期间合约状态的不一致性。

  2. 重入保护锁 / 互斥锁:
    OpenZeppelin的ReentrancyGuard通过简单互斥锁阻止嵌套调用。使用示例如下:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

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

    function withdraw(uint256 amount) public nonReentrant {
        require(balances[msg.sender] >= amount);
        balances[msg.sender] -= amount;
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success);
    }
}
  1. 限制外部调用:
    在关键函数中减少转账或交互,优先使用“拉取支付”模式,让用户显式提现。

  2. 静态分析和自动工具:
    Slither和Soken自研审计框架都能标记重入风险。

  3. 使用最新Solidity版本及其特性:
    Solidity 0.8+自带溢出检查,依然存在重入风险,应结合设计模式和代码安全。

Soken方法论: 我们的审计结合手动威胁建模、符号执行和模糊测试,能发现常规扫描难以识别的复杂重入漏洞。


现实案例:重入攻击实例及其影响

尽管重视程度提升,重入攻击仍然高发。以下表格对比了部分著名攻击事件,展示了此类漏洞的多样性和巨大损失:

事件名称 日期 损失金额 利用模式 关键教训
DAO 攻击 2016-06 约6000万美元 splitDAO 提案中的重入漏洞 状态更新必须先于外部调用
Lendf.Me 攻击 2020-04 超2500万美元 闪电贷放大重入 结合重入保护锁与闪电贷防护
Euler Finance 攻击 2023-03 1.97亿美元 嵌套重入加上delegatecall 复杂调用链增加攻击风险
最近的DeFi桥攻击 2025-11 1500万美元 代币转账重入漏洞 跨合约交互审计尤为重要

注: Soken的审计着重强调多层安全,特别是跨合约调用与可能动态触发回退函数的代币标准。


Delegatecall与闪电贷:加剧重入风险的向量

delegatecall在调用合约的上下文中执行代码,可能掩盖重入风险,扩大攻击面,尤其是与闪电贷组合时。

重入攻击常借助闪电贷实现资本最大化的递归抽干:

  • 闪电贷在单个交易中提供大量即时流动性。
  • 攻击者利用闪电贷资金触发重入调用,抽干协议资金后归还贷款。
  • delegatecall可能无意中执行外部不受信代码,修改合约状态或允许重入。

安全洞见:“Soken经验表明,重入防护应超越单合约范畴,涵盖协议整体设计——尤其在delegatecall和闪电贷共存时。合约必须结合重入保护、入口验证以及针对闪电贷的特定检查(比如贷款额度限制和递归调用限制)。”


常见重入防护技术对比表

技术 描述 优点 缺点 适用场景
Checks-Effects-Interactions 状态更新优先于外部调用 简单有效 需要严守编码规范 基础安全实践
重入保护锁(互斥锁) 使用Solidity修饰符防止重入调用 明确阻止递归调用 增加少量gas开销 推荐用于所有提现函数
拉取支付模式 用户主动提取资金 最大程度减少自动转账风险 需要用户交互 托管和质押合约的理想方案
静态动态分析 自动和手动代码审计 早期发现漏洞 可能遗漏复杂跨合约调用路径 部署前安全保障核心步骤
闪电贷和delegatecall检查 校验调用者及交易上下文 防止贷款放大重入攻击 实现复杂 闪电贷频繁的DeFi协议关键保障

Solidity代码示例:易受重入攻击合约及其修复

以下为一个最小易受攻击的合约和使用重入保护的强化版本:

// 易受攻击:提现时状态更新滞后
contract Vulnerable {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient funds");
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        balances[msg.sender] -= amount; // 漏洞:状态在外部调用后更新
    }
}

使用OpenZeppelin的ReentrancyGuard和先更新状态后调用的安全版本:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

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

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) external nonReentrant {
        require(balances[msg.sender] >= amount, "Insufficient funds");
        balances[msg.sender] -= amount; // 先更新状态
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

专业提示: 始终将checks-effects-interactions模式与重入保护锁结合使用。审计合约调用图谱并结合模糊测试能在部署前发现隐蔽的多跳重入漏洞。


重入漏洞依然是复杂DeFi生态中严重的安全威胁,尤其是在使用闪电贷与delegatecall的环境下。最有效的防御结合了代码规范(checks-effects-interactions)、显式重入锁、严格审计和上下文相关的交易验证。

Soken的安全研究结合282+份智能合约审计的经验,持续发现并修复重入及其相关风险。采用最佳实践与全面测试体系是任何处理用户资金项目的必修课。


需要专家安全指导? Soken团队已审计超过255个智能合约,为20亿美元以上协议资产保驾护航。无论您是需要全面审计免费安全X光评估,还是在加密监管方面寻求帮助,我们都已准备就绪。

联系Soken专家 | 查看我们的审计报告

Article author

常见问题

什么是智能合约中的重入攻击?

重入攻击是指恶意合约在第一次调用未完成前,重复调用受害合约,利用外部调用漏洞窃取资产或意外改变合约状态。

哪些智能合约模式最易受重入攻击?

在更新内部状态前进行外部调用(如代币转账或调用其他合约)的合约模式极易遭受重入攻击,因递归调用风险高。

如何在Solidity中防止重入漏洞?

常用方法包括使用Checks-Effects-Interactions模式,应用OpenZeppelin的'ReentrancyGuard'修饰符,以及减少外部调用或采用推迟支付等策略。

有哪些工具可帮助在部署前检测重入漏洞?

MythX、Slither和Securify等自动化安全分析工具能检测重入模式,同时结合人工代码审计和模糊测试提升漏洞发现率。

delegatecall和闪电贷与重入攻击有关吗?

是的,delegatecall滥用可能导致重入攻击,通过在另一个合约上下文执行代码;闪电贷可被攻击者利用放大影响,但本身不是重入攻击。

聊天