世界杯竞猜项目Dapp-第一章(合约开发)

前言

最近卡塔尔世界杯如火如荼,让我们一起来尝试利用 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];
    }
}
posted @ 2022-12-04 20:00  这个杀手冷死了  阅读(85)  评论(0编辑  收藏  举报