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
变量。其余变量没设置,代表使用默认值。我们查询某赋权账户的信息如下。可以发现,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 下的调试结果。
(完)