前言
最近卡達世界盃如火如荼,讓我們一起來嘗試利用 solidity 語言做一個世界盃競猜的 Dapp 實戰專案,本次實戰學習主要參考:https://github.com/dukedaily/solidity-expert,我會針對原始專案做更詳盡的註解,持續更新中…
業務需求
- 參賽球隊一經設定不可改變,整個活動結束後無法投票;
- 全⺠均可參與,無許可權控制;
- 每次投票為 1 ether,且只能選擇一支球隊;
- 每個人可以投注多次;
- 僅管理員公佈最終結果,完成獎金分配,開獎後邏輯:
- winner 共享整個獎金池(一部分是自己的本金,一部分是利潤);
- winner 需自行領取獎金(因為有手續費);
- 下一期自行開始
基礎合約實現
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract WorldCup {
// 1. 狀態變數:管理員、所有玩家、獲獎者地址、第幾期、參賽球隊
// 2. 核心方法:下注、開獎、兌現
// 3. 輔助方法:獲取獎金池金額、管理員地址、當前期數、參與人數、所有玩家、參賽球隊
// 管理員
address public admin;
// 第幾期
uint8 public currRound;
// 參賽球隊
string[] public countries = ["GERMANY", "FRANCH", "CHINA", "BRIZAL", "KOREA"];
// 期數 => 玩家
mapping(uint8 => mapping(address => Player)) players;
// 期數 => 投注各球隊的玩家
mapping(uint8 => mapping(Country => address[])) public countryToPlayers;
// 玩家對應贏取的獎金
mapping(address => uint256) public winnerVaults;
// 投注截止時間-使用不可變數,可透過建構函式傳值,部署後無法改變
uint256 public immutable deadline;
// 所有玩家待兌現的獎金
uint256 public lockedAmts;
enum Country {
GERMANY,
FRANCH,
CHINA,
BRAZIL,
KOREA
}
event Play(uint8 _currRound, address _player, Country _country);
event Finialize(uint8 _currRound, uint256 _country);
event ClaimReward(address _claimer, uint256 _amt);
// 驗證管理員身份
modifier onlyAdmin {
require(msg.sender == admin, "not authorized!");
_;
}
// 玩家投注資訊
struct Player {
// 是否開獎
bool isSet;
// 投注的球隊份額
mapping(Country => uint256) counts;
}
constructor(uint256 _deadline) {
admin = msg.sender;
require(_deadline > block.timestamp, "WorldCupLottery: invalid deadline!");
deadline = _deadline;
}
// 下注過程
function play(Country _selected) payable external {
// 引數校驗
require(msg.value == 1 gwei, "invalid funds provided!");
require(block.timestamp < deadline, "it's all over!");
// 更新 countryToPlayers
countryToPlayers[currRound][_selected].push(msg.sender);
// 更新 players(storage 是引用傳值,修改會同步修改原變數)
Player storage player = players[currRound][msg.sender];
// player.isSet = false;
player.counts[_selected] += 1;
emit Play(currRound, msg.sender, _selected);
}
// 開獎過程
function finialize(Country _country) onlyAdmin external {
// 找到 winners
address[] memory winners = countryToPlayers[currRound][_country];
// 分發給所有壓中玩家的實際獎金
uint256 distributeAmt;
// 本期總獎勵金額(獎池金額 - 所有玩家待兌現的獎金)
uint currAvalBalance = getVaultBalance() - lockedAmts;
console.log("currAvalBalance:", currAvalBalance, "winners count:", winners.length);
for (uint i = 0; i < winners.length; i++) {
address currWinner = winners[i];
// 獲取每個地址應該得到的份額
Player storage winner = players[currRound][currWinner];
if (winner.isSet) {
console.log("this winner has been set already, will be skipped!");
continue;
}
winner.isSet = true;
// 玩家購買的份額
uint currCounts = winner.counts[_country];
// (本期總獎勵 / 總獲獎人數)* 當前地址持有份額
uint amt = (currAvalBalance / countryToPlayers[currRound][_country].length) * currCounts;
// 玩家對應贏取的獎金
winnerVaults[currWinner] += amt;
distributeAmt += amt;
// 放入待兌現的獎金池
lockedAmts += amt;
console.log("winner:", currWinner, "currCounts:", currCounts);
console.log("reward amt curr:", amt, "total:", winnerVaults[currWinner]);
}
// 未分完的獎勵即為平臺收益
uint giftAmt = currAvalBalance - distributeAmt;
if (giftAmt > 0) {
winnerVaults[admin] += giftAmt;
}
emit Finialize(currRound++, uint256(_country));
}
// 獎金兌現
function claimReward() external {
uint256 rewards = winnerVaults[msg.sender];
require(rewards > 0, "nothing to claim!");
// 玩家領取完獎金置為 0
winnerVaults[msg.sender] = 0;
// 從待兌現獎金池中移除該玩家份額
lockedAmts -= rewards;
(bool succeed,) = msg.sender.call{value: rewards}("");
require(succeed, "claim reward failed!");
console.log("rewards:", rewards);
emit ClaimReward(msg.sender, rewards);
}
// 獲取獎池金額
function getVaultBalance() public view returns(uint256 bal) {
bal = address(this).balance;
}
// 獲取當期下注當前球隊的人數
function getCountryPlayers(uint8 _round, Country _country) external view returns(uint256) {
return countryToPlayers[_round][_country].length;
}
// 獲取當前玩家當期押注份額
function getPlayerInfo(uint8 _round, address _player, Country _country) external view returns(uint256 _counts) {
return players[_round][_player].counts[_country];
}
}