原文链接
date:20170613
投票系统
下面的例子稍微有点复杂,展示了很多Solidity的特性。它实现了投票功能。电子投票系统最大的问题是怎么分配投票权给正确的人以及防止暗箱操作。我们在这里不会解决所有的问题。但是至少这里展示了如何实现代理投票以及投票自动计数同时保持透明。
解决思路是给每一次投票生成一个合约,给投票的每一项起个简短名字。投票创建者(负责人呢,主席)单独的给每个地址分配投票权。
地址对应的人可以自己投票,也可以把投票权交给他们信任的人。
投票最后,winningProposal()
将返回投票结果。
pragma solidity ^0.4.11;
/// @title Voting with delegation.
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;
/// 生成一个新的投票,参数为候选人数组
function Ballot(bytes32[] proposalNames) {
chairperson = msg.sender; //确定负责人
voters[chairperson].weight = 1; //设置权重
// 给每个候选人名称生成一个新的proposal对象,并且添加到数组中
for (uint i = 0; i < proposalNames.length; i++) {
// `Proposal({...})` 生成一个临时Proposal对象
// `proposals.push(...)`将该对象添加到数组末尾
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}
// 赋予`voter`在这次投票的投票权
// 只有负责人`chairperson`才能调用这个方法
function giveRightToVote(address voter) {
// 如果`require`中的参数执行结果为`false`,
// 将会结束执行并且回滚所有的状态变化,余额变化。
// 这种方法很好的避免了错误调用的情况。
// 需要注意的是这个操作目前还是会消耗gas。
// 这一点,我们计划将来能改进下。
require((msg.sender == chairperson) && !voters[voter].voted);
voters[voter].weight = 1;
}
/// 将你的投票权委托给 `to`.
function delegate(address to) {
// 引用赋值,从数组中取出当前选民
Voter sender = voters[msg.sender];
require(!sender.voted);
// 自己委托给自己是不允许的
require(to != msg.sender);
// 如果`to`已经代理给了其他人,那么继续往前推
// 通常情况下,这个循环是很危险的,因为运行次数越多,
// 所需的gas也越多。这种情况下,委托不会执行。
// 但是其他情况下,这种循环会正确执行。
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
// 如果发现形成一个委托圈,这是不允许的。
// A委托B,B委托C,C委托A,不允许
require(to != msg.sender);
}
// 委托之后,就相当于已经投票了
sender.voted = true;
sender.delegate = to;
Voter delegate = voters[to];
if (delegate.voted) {
// 如果委托已经投票了,直接根据委托人的投票添加票数。
proposals[delegate.vote].voteCount += sender.weight;
} else {
// 如果委托人没有投过票,直接添加他的权重
delegate.weight += sender.weight;
}
}
/// 把你的选票(包含被人委托给你的选票)投给 `proposals[proposal].name`.
function vote(uint proposal) {
Voter sender = voters[msg.sender];
require(!sender.voted);
sender.voted = true;
sender.vote = proposal;
// 如果 `proposal` 不在候选人列表里,那么直接丢弃选票,并回滚所有的改变
proposals[proposal].voteCount += sender.weight;
}
/// @dev 获取选票最多的候选人的索引
function winningProposal() constant
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() constant
returns (bytes32 winnerName)
{
winnerName = proposals[winningProposal()].name;
}
}
可改进的地方
目前为止,给地址分配投票权的时候需要大量的交易。你有更好的方法吗?
秘密竞价
在这一章节中,我们将展示如何在以太坊平台中轻易的创建秘密竞价。我们将从公开竞价开始——每个人都可以看到出价,然后在将其扩展为秘密竞价——直到拍卖结束前,都不能看到出价。
简单的公开竞价
如下所示的竞价合约的思路是:在竞价阶段,每个人都可以出价。出价的时候就已经把钱转出去了,以便将报价和报价人绑定。如果有人出价更高,则返还之前的最高价。直到竞价结束,合约需要手动调用,才能将钱转给受惠人,合约不能自动触发。
pragma solidity ^0.4.11;
contract SimpleAuction {
// 拍卖的参数。 时间是unix时间戳(1970-01-01以来的秒数)
// 或者是时间间隔,单位为秒
address public beneficiary;
uint public auctionStart;
uint public biddingTime;
// 竞价的当前状态
address public highestBidder;
uint public highestBid;
// 允许返还之前最高价
mapping(address => uint) pendingReturns;
// 在结束的时候设置为true,不允许其他任何改变
bool ended;
// 事件监听
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
// 下面是所谓的natspec注释
// 通过三个斜线表示。
// 它会在请求用户确认交易的时候显示出来
/// Create a simple auction with `_biddingTime`
/// seconds bidding time on behalf of the
/// beneficiary address `_beneficiary`.
function SimpleAuction(
uint _biddingTime,
address _beneficiary
) {
beneficiary = _beneficiary;
auctionStart = now;
biddingTime = _biddingTime;
}
/// Bid on the auction with the value sent
/// together with this transaction.
/// The value will only be refunded if the
/// auction is not won.
function bid() payable {
// 这里不需要参数。因为交易的参数已经能够说明所有的信息了。
// payable关键字说明能够接收以太币
// 如果拍卖结束了,就不执行下面的语句,直接return。
require(now <= (auctionStart + biddingTime));
// 如果出价比当前的出价低,直接return
require(msg.value > highestBid);
if (highestBidder != 0) {
// 直接调用highestBidder.send(highestBid)将钱返还
// 是有很大的风险的,比如能够被调用者阻止,或者遇到1024堆栈溢出错误。
// 让竞拍失败者取回钱币会比较安全些。
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
HighestBidIncreased(msg.sender, msg.value);
}
/// 取回出价,因为竞拍失败
function withdraw() returns (bool) {
var amount = pendingReturns[msg.sender];
if (amount > 0) {
// 这里设置为0是非常重要的。
// 因为接受者可以在`send`函数返回之前,再次调用这个函数。
pendingReturns[msg.sender] = 0;
if (!msg.sender.send(amount)) {
// 这里不必抛出异常,直接设置欠款就可以了。
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// 结束竞拍,并把最高出价直接转入受惠人。
function auctionEnd() {
// 将函数组织起来,与其他的合约交互是很好的一个法则。
// (例如他们能够调用函数,或者发送以太币)
// 通常有三个步骤:
// 1. 检查条件
// 2. 实施操作,可能会改变条件
// 3. 和其他合约交互。
// 如果这些步骤弄混乱了,其他合约可能会回调本合约,
// 并且改变状态,或者多次实施操作(支付以太币)
// 如果函数在函数内部调用外部合约,同样需要考虑其他合约的交互问题。
// 1. 条件
require(now >= (auctionStart + biddingTime)); // 竞拍没有结束
require(!ended); // 这个函数已经调用过了。
// 2. 具体实施
ended = true;
AuctionEnded(highestBidder, highestBid);
// 3. 交互
beneficiary.transfer(highestBid);
}
}
秘密竞价
通过如下的操作,将之前的公开竞价转变为秘密竞价。秘密竞价的一个优势是没有时间压力。(?The advantage of a blind auction is that there is no time pressure towards the end of the bidding period. )在透明的计算平台实现秘密竞价貌似是个矛盾,但是加密算法在其中起到了关键作用。
在竞价期间,投标者并不发送它的出价,而是它的一个hash值。因为目前为止无法找到两个一摸一样的hash值,所以我们可以用它来表示出价。在结束投标的时候,出价者应该披露出价。出价者发送非加密的金额,合约对比金额的hash值是否与出价的金额相通。
另一个挑战是,如何将投标能够绑定用户,同时也能够保持秘密:唯一能够防止出价者赢得竞拍之后却不支付的方法就是让出价者在竞拍的时候就发送金额。但是由于在以太坊中金额传输不可能秘密进行,所以所有人都能看到金额。
如下的合约解决了这一问题:让出价者可以出比出价更高的金额。由于只有在披露阶段才能检验出有些出价是无效的,但是这是故意的(它提供了明确的标志说明该出价是无效出价):出价者可以通过多次的低价竞价或者高价竞价来迷惑竞拍。
pragma solidity ^0.4.11;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address public beneficiary;
uint public auctionStart;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
mapping(address => Bid[]) public bids;
address public highestBidder;
uint public highestBid;
// 允许取回之前的竞价
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
/// Modifiers are a convenient way to validate inputs to
/// functions. `onlyBefore` is applied to `bid` below:
/// The new function body is the modifier's body where
/// `_` is replaced by the old function body.
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }
function BlindAuction(
uint _biddingTime,
uint _revealTime,
address _beneficiary
) {
beneficiary = _beneficiary;
auctionStart = now;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
/// Place a blinded bid with `_blindedBid` = keccak256(value,
/// fake, secret).
/// The sent ether is only refunded if the bid is correctly
/// revealed in the revealing phase. The bid is valid if the
/// ether sent together with the bid is at least "value" and
/// "fake" is not true. Setting "fake" to true and sending
/// not the exact amount are ways to hide the real bid but
/// still make the required deposit. The same address can
/// place multiple bids.
function bid(bytes32 _blindedBid)
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}
/// 取回你的出价,你将获取到所有无效的竞价以及非最高价的有效出价。
function reveal(
uint[] _values,
bool[] _fake,
bytes32[] _secret
)
onlyAfter(biddingEnd)
onlyBefore(revealEnd)
{
uint length = bids[msg.sender].length;
require(_values.length == length);
require(_fake.length == length);
require(_secret.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
var bid = bids[msg.sender][i];
var (value, fake, secret) =
(_values[i], _fake[i], _secret[i]);
if (bid.blindedBid != keccak256(value, fake, secret)) {
// 出价并不真正取回。不用退还出价
continue;
}
refund += bid.deposit;
if (!fake && bid.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// 限制发送者相通的金额只发送一次。
bid.blindedBid = 0;
}
msg.sender.transfer(refund);
}
// 这是一个内部函数,其意义是:只能从内部调用,或者只能从原合同调用。
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != 0) {
// 取回次高价的金额
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
/// 取回出价。
function withdraw() returns (bool) {
var amount = pendingReturns[msg.sender];
if (amount > 0) {
// 这里设置为0是很关键的。因为在`send`返回值之前用户可以再调用一次。 (参看之前的观点:条件->实施操作->交互).
pendingReturns[msg.sender] = 0;
if (!msg.sender.send(amount)){
// 不必要抛出异常,直接设置未退换金额就好了
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// 竞价结束,将最高价转给受益人
function auctionEnd()
onlyAfter(revealEnd)
{
require(!ended);
AuctionEnded(highestBidder, highestBid);
ended = true;
// 我们将所有的金额都转入受益者,因为有些返还出价可能会失败。
beneficiary.transfer(this.balance);
}
}
安全远程购买
pragma solidity ^0.4.11;
contract Purchase {
uint public value;
address public seller;
address public buyer;
enum State { Created, Locked, Inactive }
State public state;
function Purchase() payable {
seller = msg.sender;
value = msg.value / 2;
require((2 * value) == msg.value);
}
modifier condition(bool _condition) {
require(_condition);
_;
}
modifier onlyBuyer() {
require(msg.sender == buyer);
_;
}
modifier onlySeller() {
require(msg.sender == seller);
_;
}
modifier inState(State _state) {
require(state == _state);
_;
}
event Aborted();
event PurchaseConfirmed();
event ItemReceived();
/// Abort the purchase and reclaim the ether.
/// Can only be called by the seller before
/// the contract is locked.
function abort()
onlySeller
inState(State.Created)
{
Aborted();
state = State.Inactive;
seller.transfer(this.balance);
}
/// Confirm the purchase as buyer.
/// Transaction has to include `2 * value` ether.
/// The ether will be locked until confirmReceived
/// is called.
function confirmPurchase()
inState(State.Created)
condition(msg.value == (2 * value))
payable
{
PurchaseConfirmed();
buyer = msg.sender;
state = State.Locked;
}
/// Confirm that you (the buyer) received the item.
/// This will release the locked ether.
function confirmReceived()
onlyBuyer
inState(State.Locked)
{
ItemReceived();
// 这里先改变状态非常重要。否则 `send`被调用之后,
// 下面的代码会再次执行。
state = State.Inactive;
// 注意: 这里事实上允许了买卖双方取消返还。这里使用了返还模式(?whitdraw pattern)
buyer.transfer(value);
seller.transfer(this.balance);
}
}
微支付应用
待完善