0x00 前言

为了增强该挑战的约束力和仪式感,我们要确保挑战规则和惩罚的落实,智能合约就是一个很好能够实现这一目标的技术。通过编写智能合约,我们可以在代码层面上确保规则的执行,保证规则的强制性和可信度。

这篇文章是该系列的第一篇,就让我们来尝试一下写一个这样的合约吧!我们将使用Solidity编写一个每周博客挑战智能合约,并部署到Polygon上。最近Sui刚上线,我个人也很喜欢Sui上的开发体验,后续可能还会做一个Sui版本的。

0x01 需求

  1. 设置挑战开始时间、挑战周期、周期数、挑战者地址、参与者地址、惩罚币种、惩罚金额
  2. 获取挑战者的惩罚币种扣费授权(Approve)
  3. 挑战者必须在第一个周期结束前存入3倍的惩罚金作为押金,否则挑战结束,视为挑战失败
  4. 扣费的时候,如果挑战者账户上的代币不足,则用押金扣费,如果两次出现代币不足的情况,则视为挑战失败,剩余押金平均发给其他参与者
  5. 挑战者可以通过合约提交博客内容,具体而言,博客文件会先上传IPFS,再将IPFS地址传到链上(当然,这里会有个bug,比如我随便传一个地址上去,合约也认为我是提交了的。但我以人格担保,绝对保证博客质量,但为了简单起见,我们就不考虑这个了~)
  6. 每周更新,合约记录本周期挑战者是否有提交博客,如果没有,按照约定发放惩罚金
  7. 达标率在60%以上,视为挑战成功,获得一个代表挑战成功的NFT(待定),其他情况视为挑战失败
  8. 挑战结束,挑战者可取回剩余押金
  9. 可以重置挑战,以便发起下一场挑战

0x02 合约设计

既然上面的需求描述得那么详细,那么我们可以先问问GPT,生成的代码如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract BlogChallenge {
    // 挑战的数据结构
    struct Challenge {
        uint256 startTime; // 挑战开始时间
        uint256 cycle; // 挑战周期
        uint256 numberOfCycles; // 周期数
        address challenger; // 挑战者地址
        address[] participants; // 参与者地址
        IERC20 penaltyToken; // 惩罚币种
        uint256 penaltyAmount; // 惩罚金额
        uint256 deposit; // 押金
        bool[] blogSubmissions; // 博客提交情况
        bool ended; // 挑战是否已结束
    }

    // 当前挑战
    Challenge public currentChallenge;

    // 押金倍数
    uint256 public constant DEPOSIT_MULTIPLIER = 3;

    // 设置挑战
    function setChallenge(
        uint256 _startTime,
        uint256 _cycle,
        uint256 _numberOfCycles,
        address _challenger,
        address[] memory _participants,
        IERC20 _penaltyToken,
        uint256 _penaltyAmount
    ) public {
        // 初始化当前挑战
        currentChallenge = Challenge({
            startTime: _startTime,
            cycle: _cycle,
            numberOfCycles: _numberOfCycles,
						challenger: _challenger,
            participants: _participants,
            penaltyToken: _penaltyToken,
            penaltyAmount: _penaltyAmount,
            deposit: _penaltyAmount * DEPOSIT_MULTIPLIER,
            blogSubmissions: new bool[](_numberOfCycles),
            ended: false
        });
    }

    // 存入惩罚金
    function depositPenalty() public {
        // 只有挑战者可以存入
        require(msg.sender == currentChallenge.challenger, "只有挑战者可以存入");
        // 将押金从挑战者账户转到合约账户
        require(currentChallenge.penaltyToken.transferFrom(msg.sender, address(this), currentChallenge.deposit), "转账失败");
    }

    // 提交博客
    function submitBlog(bool _blogSubmitted) public {
        // 只有挑战者可以提交
        require(msg.sender == currentChallenge.challenger, "只有挑战者可以提交博客");
        // 计算当前周期
        uint256 currentCycle = (block.timestamp - currentChallenge.startTime) / currentChallenge.cycle;
        // 记录博客提交情况
        currentChallenge.blogSubmissions[currentCycle] = _blogSubmitted;
    }

    // 更新周期
    function updateCycle() public {
        // 计算当前周期
        uint256 currentCycle = (block.timestamp - currentChallenge.startTime) / currentChallenge.cycle;
        // 如果挑战者没有提交博客,则发放惩罚金
        if (!currentChallenge.blogSubmissions[currentCycle]) {
            if (currentChallenge.penaltyToken.balanceOf(currentChallenge.challenger) < currentChallenge.penaltyAmount) {
                if (currentChallenge.deposit >= currentChallenge.penaltyAmount) {
                    currentChallenge.deposit -= currentChallenge.penaltyAmount;
                } else {
                    endChallenge(false);
                    return;
                }
            } else {
                currentChallenge.penaltyToken.transferFrom(currentChallenge.challenger, address(this), currentChallenge.penaltyAmount);
						}
        }
    }

    // 检查挑战是否成功
    function checkSuccess() public view returns (bool) {
        // 成功提交的次数
        uint256 successfulSubmissions = 0;
        // 遍历所有周期
        for (uint256 i = 0; i < currentChallenge.numberOfCycles; i++) {
            // 如果该周期提交了博客,成功次数加一
            if (currentChallenge.blogSubmissions[i]) {
                successfulSubmissions++;
            }
        }
        // 如果成功次数达到总周期数的60%,则挑战成功
        return successfulSubmissions >= currentChallenge.numberOfCycles * 60 / 100;
    }

    // 结束挑战
    function endChallenge(bool _success) public {
        // 挑战必须还未结束
        require(!currentChallenge.ended, "挑战已经结束");
        // 如果挑战成功,发放NFT(待实现)
        if (_success) {
            // TODO: 发放NFT
        } else {
            // 如果挑战失败,将剩余押金平均分给参与者
            uint256 participantReward = currentChallenge.deposit / currentChallenge.participants.length;
            for (uint256 i = 0; i < currentChallenge.participants.length; i++) {
                currentChallenge.penaltyToken.transfer(currentChallenge.participants[i], participantReward);
            }
        }
        // 标记挑战为已结束
        currentChallenge.ended = true;
    }

    // 重置挑战
    function resetChallenge() public {
        // 当前挑战必须已结束
        require(currentChallenge.ended, "当前挑战未结束");
        // 删除当前挑战
        delete currentChallenge;
    }

    // 提取押金
    function withdrawDeposit() public {
        // 只有挑战者可以提取押金
        require(msg.sender == currentChallenge.challenger, "只有挑战者可以提取押金");
        // 挑战必须已结束
        require(currentChallenge.ended, "挑战未结束");
        // 将剩余押金转给挑战者
        currentChallenge.penaltyToken.transfer(msg.sender, currentChallenge.deposit);
    }
}

好,本文完结。

我们先简单分析下AI给的代码,不难发现,至少有几点问题:

  1. 代码里的blogSubmission是bool,不符合我们需求里的ipfs地址(string),而且一个周期内是允许提交多个博客的,所以每周提交的blog应该是一个string[],整体就是string[][]