【区块链安全 | 第十一篇】Solidity 项目及代码示例

在这里插入图片描述

投票

以下合约实现了一个投票功能,较为复杂,但它展示了 Solidity 的许多功能。

电子投票的主要问题在于如何正确分配投票权以及如何防止操控,本文不会解决所有问题,但至少会展示如何实现委托投票,从而使计票过程自动化并完全透明。

基本思路
1.每次投票创建一个单独的合约,并为每个选项提供一个简短的名称。
2.合约的创建者(即主席)会逐个授予投票权给特定地址的用户。
3.获得投票权的用户可以直接投票,或者将投票权委托给他们信任的人。
4.投票结束后,winningProposal() 函数将返回获得票数最多的提案。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

/// @title 具有委托功能的投票合约
contract Ballot {
    // 定义一个新的复杂数据类型,将用于后续变量
    // 它表示一个选民
    struct Voter {
        uint weight; // 通过委托累积的投票权重
        bool voted;  // 是否已经投票(true 表示已投票)
        address delegate; // 被委托投票的人
        uint vote;   // 投给的提案索引
    }

    // 定义一个提案的数据结构
    struct Proposal {
        bytes32 name;   // 提案的名称(最多 32 字节)
        uint voteCount; // 该提案获得的投票数
    }

    address public chairperson; // 投票的主席

    // 状态变量,存储每个地址对应的选民信息
    mapping(address => Voter) public voters;

    // 动态数组,存储所有提案
    Proposal[] public proposals;

    /// 创建一个新的投票,提供一组提案名称
    constructor(bytes32[] memory proposalNames) {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        // 遍历提供的提案名称,为每个提案创建一个 Proposal 对象并添加到数组中
        for (uint i = 0; i < proposalNames.length; i++) {
            // 创建临时 Proposal 对象并添加到 proposals 数组中
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    // 赋予 `voter` 投票权,仅限主席调用
    function giveRightToVote(address voter) external {
        // `require` 语句检查条件,如果不满足,则会撤销所有状态更改
        require(
            msg.sender == chairperson,
            "Only chairperson can give right to vote."
        );
        require(
            !voters[voter].voted,
            "The voter already voted."
        );
        require(voters[voter].weight == 0);
        voters[voter].weight = 1;
    }

    /// 将投票权委托给 `to`
    function delegate(address to) external {
        // 获取调用者的选民信息
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "You have no right to vote");
        require(!sender.voted, "You already voted.");
        require(to != msg.sender, "Self-delegation is disallowed.");

        // 处理委托链,避免死循环
        while (voters[to].delegate != address(0)) {
            to = voters[to].delegate;
            require(to != msg.sender, "Found loop in delegation.");
        }

        Voter storage delegate_ = voters[to];

        // 确保被委托人具有投票权
        require(delegate_.weight >= 1);

        // 记录委托关系
        sender.voted = true;
        sender.delegate = to;

        if (delegate_.voted) {
            // 如果被委托人已投票,直接增加投票数
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            // 否则,增加被委托人的权重
            delegate_.weight += sender.weight;
        }
    }

    /// 投票给 `proposals[proposal].name`
    function vote(uint proposal) external {
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "Has no right to vote");
        require(!sender.voted, "Already voted.");
        sender.voted = true;
        sender.vote = proposal;

        // 若 `proposal` 超出范围,自动抛出异常并回滚
        proposals[proposal].voteCount += sender.weight;
    }

    /// @dev 计算当前得票最多的提案
    function winningProposal() public view
            returns (uint winningProposal_)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal_ = p;
            }
        }
    }

    /// 调用 `winningProposal()` 获取获胜提案索引,并返回其名称
    function winnerName() external view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

可以看到,分配投票权需要多个交易,效率较低。此外,如果两个或多个提案获得相同的票数,winningProposal() 无法识别平局。你能想到解决这些问题的方法吗?

盲拍卖

在本节中,我们将展示如何在以太坊上轻松创建一个完全盲拍的拍卖合约。我们将从一个公开拍卖开始,在这种拍卖中,每个人都可以看到所有的出价。然后,我们将扩展该合约,使其成为一个盲拍卖,即在竞拍期结束之前无法看到实际的出价。

1. 简单的公开拍卖

以下代码实现了简单的公开拍卖功能:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract SimpleAuction {
    // 拍卖的参数。时间可以是:
    // - 绝对的 Unix 时间戳(自 1970-01-01 起的秒数)
    // - 以秒为单位的时间段。
    address payable public beneficiary; // 受益人(拍卖收益接收者)
    uint public auctionEndTime; // 拍卖结束时间

    // 拍卖的当前状态。
    address public highestBidder; // 最高出价者
    uint public highestBid; // 最高出价

    // 允许被退还的之前的出价
    mapping(address => uint) pendingReturns;

    // 拍卖结束标志,设置为 true 后不允许修改
    // 默认初始化为 `false`。
    bool ended;

    // 发生状态变更时触发的事件。
    event HighestBidIncreased(address bidder, uint amount); // 最高出价增加事件
    event AuctionEnded(address winner, uint amount); // 拍卖结束事件

    // 定义错误类型,描述可能的失败情况。

    // 三斜杠注释(`///`)是 natspec 注释。
    // 在用户确认交易或显示错误信息时,这些注释会被展示。

    /// 拍卖已经结束。
    error AuctionAlreadyEnded();
    /// 目前已有更高或相等的出价。
    error BidNotHighEnough(uint highestBid);
    /// 拍卖尚未结束。
    error AuctionNotYetEnded();
    /// `auctionEnd` 函数已经被调用过了。
    error AuctionEndAlreadyCalled();

    /// 创建一个简单的拍卖,拍卖时长为 `biddingTime` 秒,
    /// 受益人地址为 `beneficiaryAddress`。
    constructor(
        uint biddingTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        auctionEndTime = block.timestamp + biddingTime;
    }

    /// 竞拍者可以调用该函数进行出价,并随交易一起发送金额。
    /// 如果竞拍未获胜,则出价会被退还。
    function bid() external payable {
        // 该函数不需要参数,所有信息都包含在交易中。
        // 关键字 `payable` 允许该函数接收 Ether。

        // 如果竞拍时间已结束,则终止执行。
        if (block.timestamp > auctionEndTime)
            revert AuctionAlreadyEnded();

        // 如果出价未超过当前最高出价,则撤销交易并返还 Ether。
        if (msg.value <= highestBid)
            revert BidNotHighEnough(highestBid);

        if (highestBid != 0) {
            // 直接使用 `highestBidder.send(highestBid)` 退还资金是不安全的,
            // 因为可能会执行一个不受信任的合约。
            // 更安全的做法是让收款人主动提现。
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// 允许竞标失败的用户提取他们的出价。
    function withdraw() external returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // 先将待提取金额置零,防止重入攻击。
            pendingReturns[msg.sender] = 0;

            // `msg.sender` 不是 `address payable` 类型,
            // 需要显式转换为 `payable(msg.sender)` 才能调用 `send()`。
            if (!payable(msg.sender).send(amount)) {
                // 发送失败时恢复余额,不抛出异常。
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    /// 结束拍卖,并将最高出价转给受益人。
    function auctionEnd() external {
        // 通常与外部合约交互的函数应该按照以下三步结构:
        // 1. 检查条件
        // 2. 执行状态变更
        // 3. 与外部合约交互
        // 如果这三步混合在一起,外部合约可能会回调当前合约,
        // 修改状态或导致 Ether 付款多次执行。
       

猜你喜欢

转载自blog.csdn.net/2301_77485708/article/details/146715382
今日推荐