quentangle

Posted on Dec 06, 2022Read on Mirror.xyz

世界杯开始了,我们用智能合约写一个猜球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);å
    }
}