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

Remix中Solidity彩票合约pickWinner()执行回退排查

问题描述

正在开发接入自定义ERC20代币的区块链彩票智能合约。
合约基础逻辑:

  • 玩家可自主购买任意数量的彩票,每一张售出的彩票都以address与ticketId配对的结构存储
  • 管理员调用*pickWinner()*函数时,合约会生成随机数,选取1张彩票作为中奖票据

调用*pickWinner()*函数前所有功能运行正常,尝试调用该函数时触发如下报错:

Gas estimation errored with the following message (see below). The transaction execution will likely fail. Do you want to force sending?
execution reverted { "originalError": { "code": 3, "data": "0x4e487b710000000000000000000000000000000000000000000000000000000000000032", "message": "execution reverted" } }

该报错未指明问题触发的具体位置,无法在不逐行删除代码测试的情况下快速定位,合约完整代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Lottery {

    event TicketsBought(address playerAddress, uint256 quantity, uint256 ticketPrice);
    event WinnerPicked(ticket ticket, uint256 amount);

    struct ticket {
        address walletAddress;
        uint256 ticketId;
    }

    uint256 currentTicketId = 0;

    address gamblinoAddress = 0xE9FEA6A6A1a8BB99E0C888c93A06D22f804A5fFA;
    uint public ticketPrice = 50 * 10**18;
    address public manager;
    ticket public winner;
    ticket[] public tickets;

    uint256 public balance;

    constructor() {
        manager = msg.sender;
        balance = 0;
    }

    function getPlayerAddresses() private view returns (address[] memory) {
        address[] memory addresses;

        for(uint i = 0; i < tickets.length; i++) {
             addresses[i] = tickets[i].walletAddress;
        }

        return addresses;
    }

    function buyTickets(uint256 quantity) public payable {
        uint256 amount = ticketPrice * quantity;
        GamblinoToken gamblinoToken = GamblinoToken(gamblinoAddress);
        uint256 allowance = gamblinoToken.allowance(msg.sender, address(this));
        uint256 playerBalance = gamblinoToken.balanceOf(msg.sender);
        require(playerBalance >= amount, 'Check token balance');
        require(allowance >= amount, 'Check the token allowance');

        balance = balance + amount;

        for (uint i = 0; i < quantity; i++) {
            ticket memory t = ticket(msg.sender, currentTicketId);
            tickets.push(t);
            currentTicketId++;
        }

        gamblinoToken.transferFrom(msg.sender, address(this), amount);
        emit TicketsBought(msg.sender, quantity, ticketPrice);

    }

    function random() public view returns (uint) {
        return uint(keccak256(abi.encodePacked(block.difficulty, block.timestamp, getPlayerAddresses())));
    }

    function pickWinner() public {
        uint index = random() % tickets.length;
        uint256 amount = balance;

        GamblinoToken gamblinoToken = GamblinoToken(gamblinoAddress);
        // the entitre balance of this contract to the winner.
        gamblinoToken.transfer(tickets[index].walletAddress, balance);
        // set balance to 0
        balance = 0;
        // set winner
        winner = tickets[index];
        // reset ticket id
        currentTicketId = 0;
        // clear players and start over.
        delete tickets;

        emit WinnerPicked(winner, amount);
    }

    function getTickets() public view returns (ticket[] memory) {
        return tickets;
    }

    // restrict to only the manager (the contract creator)
    modifier restricted() {
        require(msg.sender == manager);
        _;
    }
}

interface ERC20Interface {
    function totalSupply() external view returns (uint256);
    function balanceOf(address tokenOwner) external view returns (uint balance);
    function allowance(address tokenOwner, address spender) external view returns (uint remaining);
    function transfer(address to, uint tokens) external returns (bool success);
    function approve(address spender, uint tokens) external returns (bool success);
    function transferFrom(address from, address to, uint tokens) external returns (bool success);
    event Transfer(address indexed from, address indexed to, uint tokens);
    event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}

contract GamblinoToken is ERC20Interface {
    function totalSupply() public override view returns (uint256) {}
    function balanceOf(address tokenOwner) public override view returns (uint) {}
    function transfer(address receiver, uint numTokens) public override returns (bool) {}
    function approve(address delegate, uint numTokens) public override returns (bool) {}
    function allowance(address owner, address delegate) public override view returns (uint) {}
    function transferFrom(address owner, address buyer, uint numTokens) public override returns (bool) {}
}
问题根因

报错返回的错误数据后缀0x32对应Solidity 0.8+版本内置Panic错误码,十进制值为50,代表内存/数组访问越界,是代码里的getPlayerAddresses()函数触发的:

  • Solidity中memory类型的数组必须在声明时指定固定长度,该函数里只声明了address[] memory addresses;没有初始化长度,就直接在循环里执行addresses[i] = xxx赋值,直接触发内存越界回滚。这个函数被random()调用,random()又被pickWinner()调用,所以执行开奖流程第一步算随机数就会崩溃。

除了直接触发报错的问题,代码还存在其他明显缺陷:

  • 自定义的GamblinoToken合约所有函数都是空实现,没有任何实际逻辑,且和导入的OpenZeppelin ERC20实现重复,调用转账、余额查询等方法时不会得到预期结果
  • pickWinner()函数没有加已定义的restricted权限修饰符,任何地址都可以调用开奖,存在权限漏洞
  • 随机数生成逻辑依赖block.difficulty、block.timestamp等链上公开可被验证者/矿工篡改的数据,彩票场景下存在被操纵开奖结果的风险
  • ERC20转账操作没有检查返回值,部分不符合ERC20规范的代币转账失败时不会主动回滚,只会返回false,会出现中奖者没收到奖励但合约状态被清空的问题
  • 没有判断奖池是否为空,当tickets.length为0时执行取模运算会触发除0回滚
修复方案
  1. 修复直接触发报错的getPlayerAddresses()函数,声明内存数组时初始化固定长度:
    function getPlayerAddresses() private view returns (address[] memory) {
        // memory数组必须初始化时指定长度
        address[] memory addresses = new address[](tickets.length);
        for(uint i = 0; i < tickets.length; i++) {
             addresses[i] = tickets[i].walletAddress;
        }
        return addresses;
    }
    
  2. 删除重复定义的空ERC20Interface和GamblinoToken合约,直接使用导入的OpenZeppelin ERC20标准实现,代币部署完成后将正确的合约地址赋值给gamblinoAddress即可
  3. 给pickWinner()加上restricted修饰符,限制仅管理员可调用开奖,同时增加奖池非空判断:
    function pickWinner() public restricted {
        require(tickets.length > 0, "No tickets sold");
        // 剩余逻辑
    }
    
  4. 引入OpenZeppelin的SafeERC20库,使用safeTransfer、safeTransferFrom替代原生的transfer/transferFrom方法,自动校验转账返回值,避免静默失败
  5. 替换现有不安全的链上随机数逻辑,接入可验证随机数方案生成开奖随机数,避免结果被操纵

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 11:57:20