世界杯开始了,我们用智能合约写一个猜球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);å
    }
}
Subscribe to quentangle
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.