以太坊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
相关产品推荐
相关产品推荐

