Home

Published

- 8 min read

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

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

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