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