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

(完)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容