10分钟智能合约:进阶实战-3.9 非预期的 Ether

53 阅读5分钟

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

非预期的 Ether:定义、原理与防御

非预期的 Ether 指智能合约在未设计接收 ETH 的情况下,却因某些原因获得了 ETH,导致这些资金被锁定、合约行为异常或被攻击者利用。这类问题源于对 EVM 中 ETH 转账机制的误解,是审计中常见的高风险点。

核心认知:在以太坊中,强制发送 ETH 到合约的方式不止 send/transfer/call,还有 selfdestruct 和预先挖矿。合约无法主动拒绝这两种方式。


1. 核心原理

EVM 允许 ETH 通过以下途径进入合约:

途径是否可被合约拒绝说明
receive() / fallback()✅ 是正常转账需经过这两个函数,若未实现或实现中 revert,则转账失败。
selfdestruct(target)❌ 否自毁合约会将所有 ETH 强制发送给 target不触发任何接收函数
预先挖矿❌ 否在合约部署前,可向合约地址直接发送 ETH(因地址可通过 CREATE2 预计算)。

因此,即使合约没有 payable 函数,也可能拥有 ETH 余额。若开发者假设合约余额始终为零或仅来自某特定操作,就会产生漏洞。


2. 常见漏洞类型

类型描述后果
余额依赖错误合约使用 address(this).balance 作为逻辑依据,但未考虑强制转入的 ETH。攻击者可操纵余额,使合约做出非预期行为(如发放过多奖励、价格操纵)。
无提款功能合约可接收 ETH,但没有提款函数,导致资金永久锁定。用户或管理员误转入的 ETH 无法取回。
逻辑冲突合约要求余额在特定条件下为零或固定值,但强制转入破坏条件。后续函数调用失败或状态不一致。
重入与 selfdestruct 结合攻击者利用 selfdestruct 向目标合约强制注入 ETH,改变其余额,从而绕过检查或触发重入。历史上曾用于绕过 Uniswap 储备验证等。
payable 误用函数被错误标记为 payable,而内部逻辑未处理 ETH,导致用户误转资产且无法取回。资金损失。

3. 典型漏洞示例

3.1 余额依赖逻辑被操纵

// 漏洞合约:根据合约余额按比例分配奖励
contract Vault {
    mapping(address => uint) public shares;
    uint public totalShares;

    function deposit() public payable {
        shares[msg.sender] += msg.value;
        totalShares += msg.value;
    }

    function claimReward() public {
        uint userShare = shares[msg.sender];
        require(userShare > 0, "No shares");

        // ❌ 假设合约余额等于总存款,但可能被强制注入 ETH
        uint reward = address(this).balance * userShare / totalShares;
        shares[msg.sender] = 0;
        payable(msg.sender).transfer(reward);
    }
}

攻击:攻击者先 deposit 少量 ETH,获得份额。然后通过 selfdestruct 向该合约强制转入大量 ETH,使 address(this).balance 远大于 totalShares。调用 claimReward 时,reward 计算出的金额远超实际应得,导致资金被抽空。

3.2 无 receive/fallback 却接收 ETH

// 没有 payable receive/fallback
contract NoReceive {
    function doSomething() external {
        // 业务逻辑,但不处理 ETH
    }
}

用户或协议错误地向 NoReceive 发送 ETH(例如通过 transfer),由于没有 receive 函数且 fallback 也未实现 payable,转账会失败。但如果通过 selfdestruct 发送,合约会成功收到 ETH,且永远无法取出

3.3 强制注入改变价格计算(AMM 类)

// 简单 AMM 池
contract Pair {
    uint public reserve0;
    uint public reserve1;

    function swap(uint amount0Out) external {
        // 根据储备计算价格
        // ❌ 假设 reserve0 和 reserve1 完全由 swap 改变,未考虑强制转入
        // ...
    }
}

攻击者可先 selfdestruct 向池子强制注入 token0(或 ETH),改变 reserve0,使价格计算偏离实际,再用极低成本抽干另一种资产。


4. 防御措施

策略实现方式
不依赖余额进行关键逻辑使用内部会计(如 totalDeposits 变量)跟踪用户实际存入的资产,而不是依赖 address(this).balance
实现提款函数即使合约不设计为收款方,也应提供 withdrawsweep 函数(仅限 owner),以便取回意外转入的 ETH。
使用 receivefallback 明确行为若合约不应接收 ETH,则在 receive()revert;若可接收,明确记录事件并处理。
监测 selfdestruct 风险审计中确认其他合约是否会 selfdestruct 到本合约,并评估影响。
限制 payable 修饰符仅对真正需要接收 ETH 的函数添加 payable
余额检查时考虑溢出若仍需使用 balance,计算时确保不会因强制注入导致算术溢出(虽然 0.8+ 自动检查)。

安全代码示例:

contract SafeVault {
    mapping(address => uint) public deposited;
    uint public totalDeposited;   // ✅ 使用内部会计

    function deposit() public payable {
        deposited[msg.sender] += msg.value;
        totalDeposited += msg.value;
    }

    function claimReward() public {
        uint userDeposit = deposited[msg.sender];
        require(userDeposit > 0, "No deposit");

        // ✅ 基于内部会计计算奖励,而非合约余额
        uint reward = totalDeposited * userDeposit / totalDeposited; // 实际应为收益分配逻辑
        // 简化示例:假设奖励 = 用户存款占总量比例 * 合约余额
        // 但更安全的是:奖励 = (userDeposit * totalYield) / totalDeposited,其中 totalYield 独立记账
        // 以下为正确方式:
        uint totalYield = address(this).balance - totalDeposited; // 仅用于参考,非关键逻辑
        if (totalYield > 0) {
            reward = totalYield * userDeposit / totalDeposited;
        }

        deposited[msg.sender] = 0;
        totalDeposited -= userDeposit;
        payable(msg.sender).transfer(reward);
    }
}

5. 真实案例

  • Uniswap V1 流动性池:早期版本中,攻击者通过 selfdestruct 向池子强制注入 ETH,操纵价格后进行套利。Uniswap 后续通过增加 K 值检查缓解,但无法根除。
  • Parity 多签钱包:部分钱包因 selfdestruct 强制发送 ETH 导致状态不一致,后续升级中考虑了余额检查。
  • 各类彩票/分红合约:许多合约使用 address(this).balance 作为奖池,攻击者用 selfdestruct 注入极小 ETH 改变用户的分红比例。

6. 总结

非预期的 Ether 本质是开发者对 EVM 转账机制的认知盲区:

  • 合约无法拒绝通过 selfdestruct 发送的 ETH。
  • 因此,不要假设合约余额仅来自你设计的路径
  • 关键逻辑应基于内部账本uint 变量)而非 address(this).balance

防御核心:

  1. 审计中识别对 balance 的依赖
  2. 提供清退意外 ETH 的管理函数
  3. 明确 receive/fallback 行为,拒绝非预期转账。

理解这一漏洞,有助于构建更加健壮的 DeFi 协议和钱包合约。