10分钟智能合约:进阶实战-3.2.2 跨函数重入

欢迎订阅专栏10分钟智能合约:进阶实战

跨函数重入:定义、原理与经典案例

跨函数重入 是指攻击者通过一次外部调用,递归地重新进入同一个合约的另一个函数 ,利用多个函数间共享的状态尚未更新的窗口期,执行非预期的操作。

这一攻击类型的标志性事件 是 2016 年的 The DAO 攻击,导致约 360 万 ETH 被分叉回滚,直接促成了以太坊经典(ETC)的诞生。


1. 核心原理

跨函数重入的根本原因与单函数重入相同:状态更新滞后于外部调用

区别在于,重入点不再是当前函数本身,而是另一个也依赖同一状态变量的函数

典型脆弱模式:

  • 合约中有 两个或以上 的函数读取/修改同一个状态变量(如 balances[user])。
  • 其中一个函数在状态更新前发起外部调用。
  • 攻击者在该外部调用的回调中,调用另一个函数,后者读取尚未更新的状态,执行操作(如再次提款)。

2. 经典示例:The DAO 攻击简化复现

以下代码高度简化了 DAO 的核心逻辑,仅用于演示跨函数重入原理,并非完整复现。

solidity 复制代码
// 有漏洞的DAO简化版
contract SimpleDAO {
    mapping(address => uint) public balances;

    // 存款
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    // 1️⃣ 拆分提案并提取资金(漏洞函数)
    function splitDAO(address _recipient, uint _amount) public {
        uint userBalance = balances[msg.sender];
        require(userBalance >= _amount, "Insufficient balance");

        // ⚠️ 漏洞:先进行外部调用,后更新状态
        // 假设这里调用了某种奖励分发合约,触发外部回调
        (bool success, ) = _recipient.call{value: _amount}("");
        require(success, "Transfer failed");

        // 状态更新滞后
        balances[msg.sender] -= _amount;
    }

    // 2️⃣ 另一个提款函数(被重入的目标)
    function withdraw(uint _amount) public {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        balances[msg.sender] -= _amount;
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Transfer failed");
    }
}

攻击者合约

solidity 复制代码
contract Attacker {
    SimpleDAO public dao;

    constructor(address _dao) {
        dao = SimpleDAO(_dao);
    }

    // 攻击入口:先存款,再触发漏洞
    function attack() public payable {
        dao.deposit{value: 1 ether}();
        dao.splitDAO(address(this), 1 ether);  // 将收款地址设为自己
    }

    // 收到ETH时的回调
    receive() external payable {
        if (address(dao).balance >= 1 ether) {
            dao.withdraw(1 ether);  // ⚠️ 跨函数重入:调用另一个函数 withdraw
        }
    }
}

3. 攻击过程详解

步骤 操作 状态变化 / 关键点
1 攻击者 attack() 调用 dao.deposit(1 ETH) balances[attacker] = 1 ETH
2 调用 dao.splitDAO(attacker, 1 ETH) 进入漏洞函数
3 检查 balances[attacker] ≥ 1 → 通过 此时余额仍为 1
4 向攻击者发送 1 ETH 触发攻击者的 receive()
5 攻击者 receive() 内调用 dao.withdraw(1 ETH) 跨函数重入开始
6 withdraw 检查 balances[attacker] ≥ 1 由于 splitDAO 尚未扣款,余额仍为 1 → 通过
7 withdraw 扣减余额 (1 → 0),并向攻击者发送 1 ETH 攻击者再得 1 ETH
8 withdraw 执行完毕,返回 splitDAO 继续执行原 splitDAO
9 splitDAO 执行 balances[attacker] -= 1 此时余额已是 0,再减 1 → 下溢?在 Solidity 0.8+ 会报错,早期版本会变成 2²⁵⁶-1
10 攻击者可重复循环(视回调控制) 直至合约 ETH 耗尽

攻击效果:攻击者从最初 1 ETH 余额,最终提取出远超其实际份额的资金。


4. 跨函数重入 vs 其他重入类型

类型 重入目标函数 典型特征 代表案例
单函数重入 同一个函数 函数内部递归调用自身 经典提款漏洞
跨函数重入 同一合约的不同函数 两个函数共享同一状态变量 The DAO 攻击
跨合约重入 另一个合约的函数 合约 A 调用 B,B 回调 A 的函数 多次发生于跨协议交互
只读重入 view 函数 不修改状态,但读取尚未更新的数据,用于操纵其他协议 Acala aUSD 攻击

5. 防御措施

核心防御:检查-生效-交互(Checks-Effects-Interactions)

任何可能被外部重入的函数,都必须在外部调用之前完成所有内部状态更新。

solidity 复制代码
function splitDAO(address _recipient, uint _amount) public {
    uint userBalance = balances[msg.sender];
    require(userBalance >= _amount, "Insufficient balance");

    // ✅ 先更新状态
    balances[msg.sender] -= _amount;

    // ✅ 再执行外部调用
    (bool success, ) = _recipient.call{value: _amount}("");
    require(success, "Transfer failed");
}

重入锁

使用 ReentrancyGuard 修饰符,确保在函数执行期间整个合约不可重入。注意:重入锁是全局性的,可以同时防御单函数和跨函数重入。

solidity 复制代码
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SimpleDAO is ReentrancyGuard {
    function splitDAO(address _recipient, uint _amount) public nonReentrant {
        // 安全
    }
    function withdraw(uint _amount) public nonReentrant {
        // 安全
    }
}

最小化外部调用

仅在绝对必要时发起外部调用,且尽可能在函数末尾进行。若必须与外部交互,优先使用**"拉取支付"(Pull Payment)**模式,将发送资产的责任转移给用户。


6. 现代启示与演变

  • 重入锁已成为标准 :OpenZeppelin 的 ReentrancyGuard 被绝大多数 DeFi 项目采用,显著降低了跨函数重入的风险。
  • 只读重入的新挑战 :即使不修改状态,重入期间读取不一致的瞬时状态仍可能被用于操纵预言机、价格计算等,需要结合其他约束防御。
  • 跨合约重入仍需警惕:当多个合约协同工作时,单一合约的重入锁无法阻止另一合约的回调,需要协议级别的协调设计。

总结

跨函数重入是重入攻击家族中极具破坏力的成员 ,其本质依然是状态更新滞后于外部调用 ,只是重入点被导向了另一个共享状态的函数。

防御跨函数重入并不需要特殊技巧------遵循 CEI 模式并配合重入锁即可一劳永逸地解决问题。然而,理解其与单函数重入的区别,有助于在审计中更敏锐地识别那些**"看似无关,实则共享状态"**的函数组合。

相关推荐
MicroTech20252 天前
微算法科技(NASDAQ :MLGO)量子优化编译:通过量子变分算法(VQE)重塑智能合约能效
科技·智能合约·量子计算
Web3_Daisy3 天前
Token 分红机制详解:实现逻辑、激励结构与风险分析
大数据·人工智能·物联网·web3·区块链
Web3VentureView5 天前
倒计时 12 小时,SYNBO 主网即将上线!
大数据·人工智能·金融·web3·区块链
碳链价值5 天前
Web3告别一刀切时代
web3
木西6 天前
Energy-Fi Protocol: 去中心化能源金融协议技术解析
web3·智能合约·solidity
Web3_Daisy6 天前
Flap怎么玩?低门槛 Meme 币的发射与链上策略
大数据·人工智能·web3·区块链·比特币
栗子~~7 天前
智能合约 -透明可升级合约[ hardhat、openzeppelin 、ethers ]的演示 demo
区块链·智能合约
TechubNews8 天前
從25Q4及全年財報數字看燦谷(Cango Inc)戰略轉向AI
网络·人工智能·web3·区块链
木西8 天前
Energy-Fi:基于 DePIN 的能源资产化协议设计与实现
web3·智能合约·solidity
暴躁小师兄数据学院8 天前
【WEB3.0零基础转行笔记】Go编程篇-第11讲:Gin框架
笔记·golang·web3·区块链·智能合约