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回滚
修复方案
- 修复直接触发报错的
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; } - 删除重复定义的空
ERC20Interface和GamblinoToken合约,直接使用导入的OpenZeppelin ERC20标准实现,代币部署完成后将正确的合约地址赋值给gamblinoAddress即可 - 给
pickWinner()加上restricted修饰符,限制仅管理员可调用开奖,同时增加奖池非空判断:function pickWinner() public restricted { require(tickets.length > 0, "No tickets sold"); // 剩余逻辑 } - 引入OpenZeppelin的
SafeERC20库,使用safeTransfer、safeTransferFrom替代原生的transfer/transferFrom方法,自动校验转账返回值,避免静默失败 - 替换现有不安全的链上随机数逻辑,接入可验证随机数方案生成开奖随机数,避免结果被操纵
内容的提问来源于stack exchange,提问作者user3362334
相关产品推荐
相关产品推荐

