Solidity 智能合約實例分析——多方投票決選提案

1 場景

在投票的應用場景中氓奈,我們定義如下幾個關鍵要素:

  • 發(fā)起人,投票的發(fā)起人呢铆,具有管理權限和能力
  • 參與者,擁有投票權利的人
  • 旁觀者蹲缠,不參與投票的人棺克,但是可以獲知投票結果
  • 提案,對多個候選提案進行投票
多方投票

2 邏輯

  1. 所有參與者持有一個區(qū)塊鏈賬戶
  2. 發(fā)起人創(chuàng)建投票合約线定,創(chuàng)建時指定多個提案
  3. 發(fā)起人為有權投票的賬戶進行賦權
  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 數(shù)據(jù)結構

每個投票人斤讥,在此處用solidity的 struct 數(shù)據(jù)結構來表示纱皆。注意,此處 voted 只能是 true or false芭商,因此派草,不管委托投票還是自主投票,只能一次性用掉所有的 weight铛楣,不可拆分近迁。

struct Voter {
    uint weight; // 256bit 的非負整數(shù)投票權重
    bool voted; // 用戶是否已經投票
    address delegate; // 被委托人賬戶
    uint vote; // 投票提案編號
}

提案的數(shù)據(jù)結構相對簡單,一個是提案名稱簸州,一個是得票數(shù)鉴竭。

struct Proposal {
    bytes32 name; // 提案名稱
    uint voteCount; // 提案票數(shù)
}

下面三項全局變量(在 solidity 中歧譬,又稱狀態(tài) state),都聲明為 public搏存,這樣做的好處是缴罗,部署后,直接可以有類似于 Java 中的 getter 這樣的查詢函數(shù)供調用祭埂,不用再手動編寫面氓。

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

基礎類型狀態(tài)查詢函數(shù)沒有參數(shù),mapping 狀態(tài)查詢函數(shù)參數(shù)為 key蛆橡,array[] 狀態(tài)查詢函數(shù)參數(shù)為序號舌界。如下圖的合約列表所示,紅框中的三個查詢類函數(shù)(淺藍背景)泰演,就是編譯后自動生成的呻拌。

functions.jpg

4.2 構造函數(shù)

此處 proposalNames 變量被聲明為 memory ,表示該變量的聲明周期只在函數(shù)調用期間,函數(shù)退出將被銷毀睦焕。這樣做的好處是節(jié)省空間藐握,消耗的 gas 也更少。相對的垃喊,狀態(tài)變量 state 是存儲在 storage 中的猾普。

在提案列表初始化時,使用了 struct 數(shù)據(jù)的創(chuàng)建語句本谜,注意相關語法初家。

constructor(bytes32[] memory proposalNames) public {
    chairperson = msg.sender; // 指定合約部署賬戶為發(fā)起人
    voters[chairperson].weight = 1;

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

4.3 賦權函數(shù)

該函數(shù)的第一個 require 限制了只能由投票發(fā)起人調用。第二個 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 變量掖肋。其余變量沒設置,代表使用默認值赏参。我們查詢某賦權賬戶的信息如下志笼。可以發(fā)現(xiàn)登刺,bool 的默認值為 false; address 的默認值為 0x0; uint 的默認值為 0籽腕。

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

4.4 委托函數(shù)

這里的 senderdelegate_ 變量使用了 storage 修飾,是因為他們都指向了全局的狀態(tài)變量纸俭,后續(xù)對他們的修改皇耗,將引起狀態(tài)變量的改變。前面兩個 require揍很,限制了沒投票才能委托且不能委托自己郎楼。下面的 while 循環(huán)万伤,是為了實現(xiàn)冒泡式的委托,即如果 A 委托 B 投票呜袁,B 又委托了 C 投票敌买,那么最終,A 的投票權應該交接給 C阶界。

function delegate(address to) public {
    // 從狀態(tài)變量取值虹钮,用 storage 修飾
    Voter storage sender = voters[msg.sender];
    require(!sender.voted, "You already voted.");
    require(to != msg.sender, "Self-delegation is disallowed.");

    // 找出最上游的被委托方(不一定是入?yún)?`to`)
    while (voters[to].delegate != address(0)) {
        to = voters[to].delegate;
        // 受委托人不能又將自己的票委托給委托人,形成循環(huán)
        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 投票函數(shù)

投票函數(shù)很好理解芙粱,一次性行使完所有權重。

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 結果統(tǒng)計函數(shù)

這兩個函數(shù)都使用了 view 關鍵字修飾氧映,表示他們是查詢類函數(shù)春畔,不會改變狀態(tài)變量。.length 可以直接獲取數(shù)組的長度岛都。此處有一個小 bug 是律姨,如果多個提案最終得票數(shù)相同,則認為循環(huán)中先被訪問到的提案勝出臼疫。

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 執(zhí)行結果

一些 remix 下的調試結果择份。

ballot_res.jpg

(完)

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市多矮,隨后出現(xiàn)的幾起案子缓淹,更是在濱河造成了極大的恐慌,老刑警劉巖塔逃,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異料仗,居然都是意外死亡湾盗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門立轧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來格粪,“玉大人,你說我怎么就攤上這事氛改≌饰” “怎么了?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵胜卤,是天一觀的道長疆导。 經常有香客問我,道長葛躏,這世上最難降的妖魔是什么澈段? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任悠菜,我火速辦了婚禮,結果婚禮上败富,老公的妹妹穿的比我還像新娘悔醋。我一直安慰自己,他們只是感情好兽叮,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布芬骄。 她就那樣靜靜地躺著,像睡著了一般鹦聪。 火紅的嫁衣襯著肌膚如雪德玫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天椎麦,我揣著相機與錄音宰僧,去河邊找鬼。 笑死观挎,一個胖子當著我的面吹牛琴儿,可吹牛的內容都是我干的。 我是一名探鬼主播嘁捷,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼造成,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了雄嚣?” 一聲冷哼從身側響起晒屎,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缓升,沒想到半個月后鼓鲁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡港谊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年骇吭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歧寺。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡燥狰,死狀恐怖,靈堂內的尸體忽然破棺而出斜筐,到底是詐尸還是另有隱情龙致,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布顷链,位于F島的核電站目代,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜像啼,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一俘闯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧忽冻,春花似錦真朗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至湖笨,卻和暖如春旗扑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背慈省。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工臀防, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人边败。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓袱衷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親笑窜。 傳聞我的和親對象是個殘疾皇子致燥,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內容