世界杯开始了,我们用智能合约写一个猜球Dapp

世界杯开始了,各路博彩公司也都为世界杯推出了玩法丰富的成熟的竞猜产品。今天我们用智能合约来写一个简单的猜胜负的应用,希望有一天这类的体育精彩能够全部通过智能合约,以去中心化的方式去运营。

作者:团长(https://twitter.com/quentangle_

世界杯开始了,各路博彩公司也都为世界杯推出了玩法丰富的成熟的竞猜产品。今天我们用智能合约来写一个简单的猜胜负的应用,希望有一天这类的体育精彩能够全部通过智能合约,以去中心化的方式去运营。

简单列一下我们的需求:

  • 两个球队参赛:主队home team, 客队away team
  • 3种可能的比赛结果:(以主队角度)胜平负
  • 用户可以押注任一种比赛结果
  • 比赛结束后,押注正确的玩家按比例瓜分总的押注奖金

需求很简单,下面我们开始写代码。

首先用hardhat创建一个新的项目,我们可以从hardhat自带的实例项目的框架上开始写。

npm install --save-dev hardhat 
npx hardhat

我们在contract目录中创建一个名为Bet.sol的文件。在文件中创建一个合约:

// SPDX-License-Identifier: UNLICENSED 
pragma solidity ^0.8.9; 
contract Bet { 
   
}

因为我们的合约涉及到资金的分配和运营类的变量的设置,所以首先我们需要为合约做一个权限管理。我们利用OpenZeppelin提供的AccessControl库来实现。我们设置两个角色,一个ADMIN的角色负责掌管资金,一个OPERATOR的角色负责变量的设置:

// SPDX-License-Identifier: UNLICENSED 
pragma solidity ^0.8.9; 
 
import "@openzeppelin/contracts/access/AccessControl.sol"; 
contract Bet is AccessControl{ 
  // role used to withdraw money from contract 
  bytes32 public constant ADMIN = keccak256("ADMIN"); 
  // role used to operate contract 
  bytes32 public constant OPERATOR = keccak256("OPERATOR"); 
}

我们定义主队为1,客队为2,两队打平的结果为3

uint8 public immutable home = 1; 
uint8 public immutable away = 2; 
uint8 public immutable draw = 3;

我们还需要几个映射来存储用户的投注信息:

// team => deposit sum 
  mapping(uint8 => uint256) public pool; 
  // player => deposit number 
  mapping(address => uint256) public tickets; 
  // player => team 
  mapping(address => uint8) public sidePick; 
  // player => claimed 
  mapping(address => bool) public claimed;

简单解释一下,pool用来存储每个队的投注总额,tickets用来存储每个用户的投注额,sidePick是每个用户投注方,claimed用来记录用户是否已经领过奖。

我们设置一些标志位来管理合约的运营状态:

// flags 
    bool public betStart = false; 
    bool public claimStart = false; 
    bool public withdrawable = false;

接下来就是合约的主要的投注方法,其实也就是把用户的投注资金和投注方做一个妥善的存储:

function bet(uint8 team) external payable nonReentrant { 
    require(betStart, "Bet haven't start yet"); 
    require(team == home || team == away || team == draw, "Invalid team"); 
    if (team == home) { 
        pool[home] = pool[home] + msg.value; 
        tickets[msg.sender] = tickets[msg.sender] + msg.value; 
        sidePick[msg.sender] = home; 
    } else if (team == away) { 
        pool[away] = pool[team] + msg.value; 
        tickets[msg.sender] = tickets[msg.sender] + msg.value; 
        sidePick[msg.sender] = away; 
    } else { 
        pool[draw] = pool[draw] + msg.value; 
        tickets[msg.sender] = tickets[msg.sender] + msg.value; 
        sidePick[msg.sender] = draw; 
    } 
    allBetFund = allBetFund + msg.value; 
    emit Wager(msg.sender, team, msg.value); 
}

nonReentrant是我们设置的一个防止重入攻击的锁,需要通过下面的命令来引入,并在定义合约时候继承:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 
contract Bet is AccessControl, ReentrancyGuard { 
... 
}

其他部分的代码就是将用户投注的球队和用户投注的金额(ETH)存储到相应的映射中。投注之后会将相应的投注信息写入Event log。

接下来是开奖的逻辑,这部分我们暂时通过OPERATOR来手动设置:

function setResult(uint8 team) external onlyRole(OPERATOR) { 
    result = team; 
    emit Result(team); 
}

一个更好的方式是通过预言机来自动获取并写入结果,等后面优化时我们会加上预言机的逻辑。

接下来就是获奖用户领奖了,在不考虑抽成的情况下,猜中的用户会按比例获取到猜错的用户的总投注额:

如果需要合约从中抽成,那么将从总的投注进而中减去抽成就可以。代码如下:

function claim() external { 
    require(claimStart, "Claim haven't started"); 
    require(!claimed[msg.sender], "Already claimed"); 
    require(sidePick[msg.sender] == result, "You didn't win the game"); 
    uint256 allClaimableFund = allBetFund / (fundRate / 100); 
    uint256 amount = (tickets[msg.sender] / pool[result]) * 
        allClaimableFund; 
    claimed[msg.sender] = true; 
    payable(msg.sender).transfer(amount); 
    emit Claim(msg.sender, amount); 
}

到这里我们的主要的逻辑就完成了。下面还需要写一些运营代码,用于对设置一些标志位的开关:

function setBetStart(bool _start) external onlyRole(OPERATOR) { 
    betStart = _start; 
} 
function setClaimStart(bool _start) external onlyRole(OPERATOR) { 
    claimStart = _start; 
}

以下是完整的代码:

// SPDX-License-Identifier: UNLICENSED 
pragma solidity ^0.8.9; 
import "@openzeppelin/contracts/access/AccessControl.sol"; 
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 
contract Bet is AccessControl, ReentrancyGuard { 
    // role used to withdraw money from contract 
    bytes32 public constant ADMIN = keccak256("ADMIN"); 
    // role used to operate contract 
    bytes32 public constant OPERATOR = keccak256("OPERATOR"); 
    uint8 public immutable home = 1; 
    uint8 public immutable away = 2; 
    uint8 public immutable draw = 3; 
    uint8 public result = 0; 
    uint256 public allBetFund; 
    uint256 public fundRate = 100; // 0% to contract operator 
    // team => deposit sum 
    mapping(uint8 => uint256) public pool; 
    // player => deposit number 
    mapping(address => uint256) public tickets; 
    // player => team 
    mapping(address => uint8) public sidePick; 
    // player => claimed 
    mapping(address => bool) public claimed; 
    // flags 
    bool public betStart = false; 
    bool public claimStart = false; 
    bool public withdrawable = false; 
    // events 
    event Wager(address indexed player, uint8 team, uint256 indexed amount); 
    event Claim(address indexed player, uint256 indexed amount); 
    event Result(uint8 indexed team); 
    function bet(uint8 team) external payable nonReentrant { 
        require(betStart, "Bet haven't start yet"); 
        require(team == home || team == away || team == draw, "Invalid team"); 
        if (team == home) { 
            pool[home] = pool[home] + msg.value; 
            tickets[msg.sender] = tickets[msg.sender] + msg.value; 
            sidePick[msg.sender] = home; 
        } else if (team == away) { 
            pool[away] = pool[team] + msg.value; 
            tickets[msg.sender] = tickets[msg.sender] + msg.value; 
            sidePick[msg.sender] = away; 
        } else { 
            pool[draw] = pool[draw] + msg.value; 
            tickets[msg.sender] = tickets[msg.sender] + msg.value; 
            sidePick[msg.sender] = draw; 
        } 
        allBetFund = allBetFund + msg.value; 
        emit Wager(msg.sender, team, msg.value); 
    } 
    function setResult(uint8 team) external onlyRole(OPERATOR) { 
        result = team; 
        emit Result(team); 
    } 
    function claim() external { 
        require(claimStart, "Claim haven't started"); 
        require(!claimed[msg.sender], "Already claimed"); 
        require(sidePick[msg.sender] == result, "You didn't win the game"); 
        uint256 allClaimableFund = allBetFund / (fundRate / 100); 
        uint256 amount = (tickets[msg.sender] / pool[result]) * 
            allClaimableFund; 
        claimed[msg.sender] = true; 
        payable(msg.sender).transfer(amount); 
        emit Claim(msg.sender, amount); 
    } 
    function setBetStart(bool _start) external onlyRole(OPERATOR) { 
        betStart = _start; 
    } 
    function setClaimStart(bool _start) external onlyRole(OPERATOR) { 
        claimStart = _start; 
    } 
    function ethBalance() external view returns (uint256) { 
        return address(this).balance; 
    } 
    function setWithdrawable(bool _enable) external onlyRole(OPERATOR) { 
        withdrawable = _enable; 
    } 
    function withdraw() external onlyRole(ADMIN) { 
        require(withdrawable, "Can't withdraw now"); 
        uint256 balance = address(this).balance; 
        payable(msg.sender).transfer(balance);å 
    } 
}