1 場景
在投票的應用場景中,我們定義如下幾個關鍵要素:
- 發起人,投票的發起人,具有管理權限和能力
- 參與者,擁有投票權利的人
- 旁觀者,不參與投票的人,但是可以獲知投票結果
- 提案,對多個候選提案進行投票
2 邏輯
- 所有參與者持有一個區塊鏈賬戶
- 發起人創建投票合約,創建時指定多個提案
- 發起人為有權投票的賬戶進行賦權
- 投票人可以選擇委托投票或自主投票
- 投票結束,得票多者勝出,任意人可查看結果
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[]
狀態查詢函數參數為序號。如下圖的合約列表所示,紅框中的三個查詢類函數(淺藍背景),就是編譯后自動生成的。
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 委托函數
這里的 sender
和 delegate_
變量使用了 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 下的調試結果。
(完)