世界盃競猜專案Dapp-第一章(合約開發)
阿新 • • 發佈:2022-12-05
前言
最近卡達世界盃如火如荼,讓我們一起來嘗試利用 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]; } }