世界盃競猜專案Dapp-第一章(合約開發)

這個殺手冷死了 發表於 2022-12-04

前言

最近卡達世界盃如火如荼,讓我們一起來嘗試利用 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];
    }
}