Solidity 智能合约实例分析——多方投票决选提案

1 场景

在投票的应用场景中,我们定义如下几个关键要素:

  • 发起人,投票的发起人,具有管理权限和能力
  • 参与者,拥有投票权利的人
  • 旁观者,不参与投票的人,但是可以获知投票结果
  • 提案,对多个候选提案进行投票
Solidity 智能合约实例分析——多方投票决选提案_第1张图片
多方投票

2 逻辑

  1. 所有参与者持有一个区块链账户
  2. 发起人创建投票合约,创建时指定多个提案
  3. 发起人为有权投票的账户进行赋权
  4. 投票人可以选择委托投票或自主投票
  5. 投票结束,得票多者胜出,任意人可查看结果

3 完整代码

源代码地址 https://solidity.readthedocs.io/en/v0.5.1/solidity-by-example.html

pragma solidity >=0.4.22 <0.6.0;

contract Ballot {

    struct Voter {
        uint weight;
        bool voted;
        address delegate;
        uint vote;
    }

    struct Proposal {
        bytes32 name;
        uint voteCount;
    }

    address public chairperson;

    mapping(address => Voter) public voters;

    Proposal[] public proposals;

    constructor(bytes32[] memory proposalNames) public {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;

        for (uint i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    function giveRightToVote(address voter) public {
        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;
    }

    function delegate(address to) public {

        Voter storage sender = voters[msg.sender];
        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.");
        }

        sender.voted = true;
        sender.delegate = to;
        Voter storage delegate_ = voters[to];
        if (delegate_.voted) {
            proposals[delegate_.vote].voteCount += sender.weight;
        } else {
            delegate_.weight += sender.weight;
        }
    }

    function vote(uint proposal) public {
        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;

        proposals[proposal].voteCount += sender.weight;
    }

    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;
            }
        }
    }

    function winnerName() public view
            returns (bytes32 winnerName_)
    {
        winnerName_ = proposals[winningProposal()].name;
    }
}

4 解析

4.1 数据结构

每个投票人,在此处用solidity的 struct 数据结构来表示。注意,此处 voted 只能是 true or false,因此,不管委托投票还是自主投票,只能一次性用掉所有的 weight,不可拆分。

struct Voter {
    uint weight; // 256bit 的非负整数投票权重
    bool voted; // 用户是否已经投票
    address delegate; // 被委托人账户
    uint vote; // 投票提案编号
}

提案的数据结构相对简单,一个是提案名称,一个是得票数。

struct Proposal {
    bytes32 name; // 提案名称
    uint voteCount; // 提案票数
}

下面三项全局变量(在 solidity 中,又称状态 state),都声明为 public,这样做的好处是,部署后,直接可以有类似于 Java 中的 getter 这样的查询函数供调用,不用再手动编写。

address public chairperson;
mapping(address => Voter) public voters;
Proposal[] public proposals;

基础类型状态查询函数没有参数,mapping 状态查询函数参数为 keyarray[] 状态查询函数参数为序号。如下图的合约列表所示,红框中的三个查询类函数(浅蓝背景),就是编译后自动生成的。

functions.jpg

4.2 构造函数

此处 proposalNames 变量被声明为 memory ,表示该变量的声明周期只在函数调用期间,函数退出将被销毁。这样做的好处是节省空间,消耗的 gas 也更少。相对的,状态变量 state 是存储在 storage 中的。

在提案列表初始化时,使用了 struct 数据的创建语句,注意相关语法。

constructor(bytes32[] memory proposalNames) public {
    chairperson = msg.sender; // 指定合约部署账户为发起人
    voters[chairperson].weight = 1;

    for (uint i = 0; i < proposalNames.length; i++) {
        // 提案列表初始化
        proposals.push(Proposal({
            name: proposalNames[i],
            voteCount: 0
        }));
    }
}

4.3 赋权函数

该函数的第一个 require 限制了只能由投票发起人调用。第二个 require 限制了赋权人尚未进行投票且权重为 0。

function giveRightToVote(address voter) public {
    require(
        msg.sender == chairperson,
        "Only chairperson can give right to vote."
    );
    require(
        !voters[voter].voted,
        "The voter already voted."
    );
    require(voters[voter].weight == 0);
    // 默认每个账户的初始权重一样,都是1
    voters[voter].weight = 1;
}

注意,此处在赋权时,只设置了 Voter 结构体的 weight 变量。其余变量没设置,代表使用默认值。我们查询某赋权账户的信息如下。可以发现,bool 的默认值为 false; address 的默认值为 0x0; uint 的默认值为 0

  • uint256: weight 1
  • bool: voted false
  • address: delegate 0x0000000000000000000000000000000000000000
  • uint256: vote 0

4.4 委托函数

这里的 senderdelegate_ 变量使用了 storage 修饰,是因为他们都指向了全局的状态变量,后续对他们的修改,将引起状态变量的改变。前面两个 require,限制了没投票才能委托且不能委托自己。下面的 while 循环,是为了实现冒泡式的委托,即如果 A 委托 B 投票,B 又委托了 C 投票,那么最终,A 的投票权应该交接给 C。

function delegate(address to) public {
    // 从状态变量取值,用 storage 修饰
    Voter storage sender = voters[msg.sender];
    require(!sender.voted, "You already voted.");
    require(to != msg.sender, "Self-delegation is disallowed.");

    // 找出最上游的被委托方(不一定是入参 `to`)
    while (voters[to].delegate != address(0)) {
        to = voters[to].delegate;
        // 受委托人不能又将自己的票委托给委托人,形成循环
        require(to != msg.sender, "Found loop in delegation.");
    }

    sender.voted = true;    // 委托等同于投票
    sender.delegate = to; 
    Voter storage delegate_ = voters[to];

    if (delegate_.voted) {
        // 如果被委托人已经投票,则直接行使委托人的投票权到相同提案
        proposals[delegate_.vote].voteCount += sender.weight;
    } else {
        delegate_.weight += sender.weight;
    }
}

4.5 投票函数

投票函数很好理解,一次性行使完所有权重。

function vote(uint proposal) public {
    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;

    proposals[proposal].voteCount += sender.weight;
}

4.6 结果统计函数

这两个函数都使用了 view 关键字修饰,表示他们是查询类函数,不会改变状态变量。.length 可以直接获取数组的长度。此处有一个小 bug 是,如果多个提案最终得票数相同,则认为循环中先被访问到的提案胜出。

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;
        }
    }
}

function winnerName() public view
        returns (bytes32 winnerName_)
{
    winnerName_ = proposals[winningProposal()].name;
}

5 执行结果

一些 remix 下的调试结果。

ballot_res.jpg

(完)

你可能感兴趣的:(Solidity 智能合约实例分析——多方投票决选提案)