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 狀態查詢函數參數為 key,array[] 狀態查詢函數參數為序號。如下圖的合約列表所示,紅框中的三個查詢類函數(淺藍背景),就是編譯后自動生成的。

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 變量。其余變量沒設置,代表使用默認值。我們查詢某賦權賬戶的信息如下??梢园l現,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

(完)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容