You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

以太坊Lottery智能合约buyTicket回滚 高gas消耗原因排查

Solidity彩票合约buyTicket调用回滚问题排查

直接导致交易回滚的硬错误

你判断的gas过高不是当前触发回滚的原因,以下3个逻辑错误才是首次调用就失败的核心:

  • 购票条件判断完全写反:require(msg.sender.balance <= ticketPrice, "Insufficient funds...") 逻辑颠倒,该语句会拦截所有地址余额大于票价的用户,仅允许余额小于等于票价的地址购票;且完全没有校验用户随交易实际发送的msg.value是否匹配票价,用户发送0ETH也能通过校验。
  • 存在无效自转账逻辑:payable(address(this)).transfer(ticketPrice); 是错误写法,用户调用payable函数发送的ETH会自动计入合约余额,不需要手动执行转账;该语句执行时会尝试从合约自身余额划转对应金额到自身,合约初始余额为0时会直接触发余额不足回滚。
  • 重复购票检查产生不必要gas消耗:hasTicket函数用全局状态变量hasTicketAnswer存储检查结果,每次调用都会修改链上状态,产生额外gas成本,该函数本可以写成只读视图函数,完全不需要修改状态。

其余隐藏逻辑漏洞

  • 开奖函数无权限控制:rollTheDice没有做owner校验,任意地址都可以触发开奖。
  • 开奖后状态未重置:开奖完成后没有清空Players数组、也没有重置奖池计数,下一轮购票会把历史玩家计入,转账时会因为合约余额不足回滚。
  • 维护了冗余的奖池计数变量:price变量手动累加购票金额,和address(this).balance取到的合约实际余额很容易出现不一致,直接读取合约原生余额即可,不需要额外维护该变量。
  • 随机数可被轻易操纵:当前用msg.sender、block.timestamp生成的随机数可被矿工、开奖调用者预测篡改,测试场景可用,生产环境必须接入链下随机数预言机。
  • 定义的nonce变量从未更新,随机数熵值不足。
  • 开奖转账用transfer方法存在硬编码gas限制,当中奖地址是合约时,fallback函数消耗gas超过2300就会导致转账失败。

修正后的核心实现参考

修正重复购票检查函数

// 改成view只读函数,不修改链上状态,无额外gas消耗
function hasTicket(address _sender) private view returns(bool) {
    for (uint i = 0; i < Players.length; i++) {
        if (Players[i] == _sender) return true;
    }
    return false;
}

修正购票函数

function buyTicket() external payable {
    require(ticketPrice > 0, "Price did not set, be patient...");
    require(!hasTicket(msg.sender), "You cannot have two tickets...");
    // 校验用户发送的金额刚好等于票价
    require(msg.value == ticketPrice, "Please send exact ticket price");
    // 删除错误的自转账逻辑,用户发送的ETH自动进入合约
    Players.push(msg.sender);

    emit TicketBought();
}

修正开奖函数

function rollTheDice() public {
    // 仅允许owner开奖
    require(msg.sender == owner, "Only owner can start draw");
    require(Players.length > 0, "No player joined");
    // 更新nonce增加随机数熵
    nonce++;
    uint randomIndex = uint(keccak256(abi.encodePacked(msg.sender, nonce, block.timestamp, block.prevrandao))) % Players.length;
    winner = payable(Players[randomIndex]);
    uint prizeAmount = address(this).balance;
    // 用call转账避免gas硬限制问题
    (bool sendSuccess, ) = winner.call{value: prizeAmount}("");
    require(sendSuccess, "Prize transfer failed");
    // 清空玩家数组,开启下一轮
    delete Players;

    emit Winner(winner);
}

关于数组遍历gas问题的说明

当玩家数量低于100个时,遍历数组做重复地址检查的gas消耗完全在区块gas限制范围内,不会触发回滚;仅当玩家数量达到上千规模时,O(n)复杂度的遍历才会逐步逼近区块gas上限。如果只是练习数组用法,该写法在测试场景完全可用,生产环境才需要搭配mapping做地址存在性标记,将检查复杂度降到O(1)。

内容的提问来源于stack exchange,提问作者0xGlitch

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 10:15:30