文章目录
投票
以下合约实现了一个投票功能,较为复杂,但它展示了 Solidity 的许多功能。
电子投票的主要问题在于如何正确分配投票权以及如何防止操控,本文不会解决所有问题,但至少会展示如何实现委托投票,从而使计票过程自动化并完全透明。
基本思路
1.每次投票创建一个单独的合约,并为每个选项提供一个简短的名称。
2.合约的创建者(即主席)会逐个授予投票权给特定地址的用户。
3.获得投票权的用户可以直接投票,或者将投票权委托给他们信任的人。
4.投票结束后,winningProposal() 函数将返回获得票数最多的提案。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
/// @title 具有委托功能的投票合约
contract Ballot {
// 定义一个新的复杂数据类型,将用于后续变量
// 它表示一个选民
struct Voter {
uint weight; // 通过委托累积的投票权重
bool voted; // 是否已经投票(true 表示已投票)
address delegate; // 被委托投票的人
uint vote; // 投给的提案索引
}
// 定义一个提案的数据结构
struct Proposal {
bytes32 name; // 提案的名称(最多 32 字节)
uint voteCount; // 该提案获得的投票数
}
address public chairperson; // 投票的主席
// 状态变量,存储每个地址对应的选民信息
mapping(address => Voter) public voters;
// 动态数组,存储所有提案
Proposal[] public proposals;
/// 创建一个新的投票,提供一组提案名称
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
// 遍历提供的提案名称,为每个提案创建一个 Proposal 对象并添加到数组中
for (uint i = 0; i < proposalNames.length; i++) {
// 创建临时 Proposal 对象并添加到 proposals 数组中
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
// 赋予 `voter` 投票权,仅限主席调用
function giveRightToVote(address voter) external {
// `require` 语句检查条件,如果不满足,则会撤销所有状态更改
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;
}
/// 将投票权委托给 `to`
function delegate(address to) external {
// 获取调用者的选民信息
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "You have no right to vote");
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.");
}
Voter storage delegate_ = voters[to];
// 确保被委托人具有投票权
require(delegate_.weight >= 1);
// 记录委托关系
sender.voted = true;
sender.delegate = to;
if (delegate_.voted) {
// 如果被委托人已投票,直接增加投票数
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// 否则,增加被委托人的权重
delegate_.weight += sender.weight;
}
}
/// 投票给 `proposals[proposal].name`
function vote(uint proposal) external {
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;
// 若 `proposal` 超出范围,自动抛出异常并回滚
proposals[proposal].voteCount += sender.weight;
}
/// @dev 计算当前得票最多的提案
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;
}
}
}
/// 调用 `winningProposal()` 获取获胜提案索引,并返回其名称
function winnerName() external view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}
}
可以看到,分配投票权需要多个交易,效率较低。此外,如果两个或多个提案获得相同的票数,winningProposal() 无法识别平局。你能想到解决这些问题的方法吗?
盲拍卖
在本节中,我们将展示如何在以太坊上轻松创建一个完全盲拍的拍卖合约。我们将从一个公开拍卖开始,在这种拍卖中,每个人都可以看到所有的出价。然后,我们将扩展该合约,使其成为一个盲拍卖,即在竞拍期结束之前无法看到实际的出价。
1. 简单的公开拍卖
以下代码实现了简单的公开拍卖功能:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleAuction {
// 拍卖的参数。时间可以是:
// - 绝对的 Unix 时间戳(自 1970-01-01 起的秒数)
// - 以秒为单位的时间段。
address payable public beneficiary; // 受益人(拍卖收益接收者)
uint public auctionEndTime; // 拍卖结束时间
// 拍卖的当前状态。
address public highestBidder; // 最高出价者
uint public highestBid; // 最高出价
// 允许被退还的之前的出价
mapping(address => uint) pendingReturns;
// 拍卖结束标志,设置为 true 后不允许修改
// 默认初始化为 `false`。
bool ended;
// 发生状态变更时触发的事件。
event HighestBidIncreased(address bidder, uint amount); // 最高出价增加事件
event AuctionEnded(address winner, uint amount); // 拍卖结束事件
// 定义错误类型,描述可能的失败情况。
// 三斜杠注释(`///`)是 natspec 注释。
// 在用户确认交易或显示错误信息时,这些注释会被展示。
/// 拍卖已经结束。
error AuctionAlreadyEnded();
/// 目前已有更高或相等的出价。
error BidNotHighEnough(uint highestBid);
/// 拍卖尚未结束。
error AuctionNotYetEnded();
/// `auctionEnd` 函数已经被调用过了。
error AuctionEndAlreadyCalled();
/// 创建一个简单的拍卖,拍卖时长为 `biddingTime` 秒,
/// 受益人地址为 `beneficiaryAddress`。
constructor(
uint biddingTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
auctionEndTime = block.timestamp + biddingTime;
}
/// 竞拍者可以调用该函数进行出价,并随交易一起发送金额。
/// 如果竞拍未获胜,则出价会被退还。
function bid() external payable {
// 该函数不需要参数,所有信息都包含在交易中。
// 关键字 `payable` 允许该函数接收 Ether。
// 如果竞拍时间已结束,则终止执行。
if (block.timestamp > auctionEndTime)
revert AuctionAlreadyEnded();
// 如果出价未超过当前最高出价,则撤销交易并返还 Ether。
if (msg.value <= highestBid)
revert BidNotHighEnough(highestBid);
if (highestBid != 0) {
// 直接使用 `highestBidder.send(highestBid)` 退还资金是不安全的,
// 因为可能会执行一个不受信任的合约。
// 更安全的做法是让收款人主动提现。
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
emit HighestBidIncreased(msg.sender, msg.value);
}
/// 允许竞标失败的用户提取他们的出价。
function withdraw() external returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 先将待提取金额置零,防止重入攻击。
pendingReturns[msg.sender] = 0;
// `msg.sender` 不是 `address payable` 类型,
// 需要显式转换为 `payable(msg.sender)` 才能调用 `send()`。
if (!payable(msg.sender).send(amount)) {
// 发送失败时恢复余额,不抛出异常。
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// 结束拍卖,并将最高出价转给受益人。
function auctionEnd() external {
// 通常与外部合约交互的函数应该按照以下三步结构:
// 1. 检查条件
// 2. 执行状态变更
// 3. 与外部合约交互
// 如果这三步混合在一起,外部合约可能会回调当前合约,
// 修改状态或导致 Ether 付款多次执行。