Learn Solidity (2) — Auction

Solidity学习笔记(2) — Auction

作者:孔令坤,转载请注明出处

今天用拍卖合约(Auction Contract)进行Smart Contract编程的学习。

环境的安装和使用方法这里不做过多的赘述,附上两个链接,大家可以用来参考:

MetaMask配置:https://karl.tech/learning-solidity-part-1-deploy-a-contract/

Solidity配置与运行案例:https://karl.tech/learning-solidity-part-2-voting/

另外附上官方的学习代码链接:https://solidity.readthedocs.io/en/latest/solidity-by-example.html#blind-auction

另外发现了一个不错的大纲型文档:https://medium.com/@mattcondon/getting-up-to-speed-on-ethereum-63ed28821bbe

1.SimpleAuction

通过智能合约,实现了拍卖中用户们的匿名化。具体而言,在拍卖中,用户们通过调用智能合约进行出价,由智能合约后台经过处理后选出出价最高的用户进行最后交易,并退回其它用户的钱。

首先,我们来看官方给出第一个例子,contract SimpleAuction{}。在这个例子中,在拍卖结束后,这个合约需要被人为地操作auctionEnd()函数,这样收益人beneficiary才能获得来自出价最高的用户的钱。另外,这个合约中,如果一个用户的报价被接受了,并不意味着这个用户已经可以拍得这个商品,因为有可能只是他出的前是目前而言最高的。如果之后有更高的出价,他需要操作withdraw()函数才能退回自己的金额。

代码分析

payable函数几乎是所有smart contract涉及到金额流动都会有的一个函数,因为涉及交易金额的信息都已经在以太坊的transaction中了,所以payable函数并不需要参数输入。在这个函数中,合约先判断是否还在拍卖截止前,然后看用户提供的拍卖金额是不是当前最高的,如果不是直接退回,如果是的话则存储到pendingReturns中,等待最后拍卖截止后再看是不是最高的价格。另外值得注意的是,由于require(msg.value > highestBid);所以在用户价格相同的时候,会取先出价的用户。

另外在这段代码中,出现了对event的调用(HighestBidIncreased()),通俗的来说event, i.e., 事件是以太坊EVM提供的一种日志基础设施。事件可以用来做操作记录,存储为日志。也可以用来实现一些交互功能,比如通知UI,返回函数调用结果等。(http://me.tryblockchain.org/blockchain-solidity-event.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
event HighestBidIncreased(address bidder, uint amount);
function bid() public payable {
require(now <= auctionEnd);
require(msg.value > highestBid);

if (highestBidder != 0) {
// Sending back the money by simply using
// highestBidder.send(highestBid) is a security risk
// because it could execute an untrusted contract.
// It is always safer to let the recipients
// withdraw their money themselves.
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
HighestBidIncreased(msg.sender, msg.value);
}

withdraw()函数可以说是simple auction中最笨的地方了,这里由于合约无法自动退回一些没有成功达成交易的用户的拍卖投标,用户需要在拍卖结束后自己去调用withdraw()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function withdraw() public returns (bool) {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// It is important to set this to zero because the recipient
// can call this function again as part of the receiving call
// before `send` returns.
pendingReturns[msg.sender] = 0;

if (!msg.sender.send(amount)) {
// No need to call throw here, just reset the amount owing
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}

在最后的auctionEnd()函数中,合约将最高的金额转移给受益人。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool ended;
function auctionEnd() public {
// 1. Conditions
require(now >= auctionEnd); // auction did not yet end
require(!ended); // this function has already been called

// 2. Effects
ended = true;
AuctionEnded(highestBidder, highestBid);

// 3. Interaction
beneficiary.transfer(highestBid);
}

2.Blind Auction

在官方提供的blind auction智能合约中,通过哈希函数的一系列特性,用户可以不直接把自己的拍卖投标放到合约中,而是只用提供自己的投标的哈希值。在拍卖结束后,用户通过提供未被哈希过的值作为依据,交给合约验证,从而判断之前提供的哈希值是有效的 — 确实由本人提交并且金额无误。

代码分析

由于代码较长,这里我直接通过注释的方式对代码进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address public beneficiary;
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 是一种简易的检验输入的方法,_代替旧的函数体
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }

function BlindAuction(
uint _biddingTime,
uint _revealTime,
address _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}

// 盲标 = keccak256(value, fake, secret).
// https://emn178.github.io/online-tools/keccak_256.html
// 一个地址可以下投多个盲标
function bid(bytes32 _blindedBid)
public
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}

// 揭示盲标是否合法
function reveal(
uint[] _values,
bool[] _fake,
bytes32[] _secret
)
public
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;
}
// 把blindedBid设置为0,防止用户重复揭示盲标
bid.blindedBid = bytes32(0);
}
msg.sender.transfer(refund); //把fake的投标的钱和真的投标的 deposit - value的钱还给用户
}

// 这是一个"internal"函数,意味着这个函数只能由合约自身来调用
// 之后的操作与simple auction相类似
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != 0) {
// Refund the previously highest bidder.
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}

function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
msg.sender.transfer(amount);
}
}
function auctionEnd()
public
onlyAfter(revealEnd)
{
require(!ended);
AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
}

3.一些思考与改进

在看代码的过程中,我发现用户的钱在投标后其实是会把钱直接锁死在了智能合约中。根据本合约的逻辑,最终其实会有很多用户的钱被锁定(因为在用户调用withdraw()函数之前,他们都认为自己下的标是最大的)这其实对用户而言非常的不友好。

这里,我重新修改了placeBid()函数,当highestBid发生变化时我直接把前一个highestBid的钱退还给了相应的用户。我把我的代码放到了下面的链接里:https://github.com/Ohyoukillkenny/Learn-Solidity