1 场景
在盲拍的应用场景中,我们定义如下几个关键要素:
- 受益人,最终的钱款接收方
- 竞拍者,任意持有合法账户的人都可以参与竞拍
- 竞拍时间,只能在指定时间内完成出价
- 明牌时间,参与者亮出出价
2 逻辑
- 所有参与者持有一个区块链账户
- 发起人创建盲拍合约,创建时指定受益人,竞拍时间,揭晓时间
- 竞拍者在竞拍时间结束前,可以进行出价
- 竞拍者可以随时撤回自己的出价,并回收抵押资金
- 竞拍结束,价高者得
3 完整代码
源代码地址 https://solidity.readthedocs.io/en/v0.5.1/solidity-by-example.html
pragma solidity >0.4.23 <0.6.0;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address payable 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);
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }
constructor(
uint _biddingTime,
uint _revealTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
function bid(bytes32 _blindedBid)
public
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}
function reveal(
uint[] memory _values,
bool[] memory _fake,
bytes32[] memory _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++) {
Bid storage bidToCheck = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(_values[i], _fake[i], _secret[i]);
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
continue;
}
refund += bidToCheck.deposit;
if (!fake && bidToCheck.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
bidToCheck.blindedBid = bytes32(0);
}
msg.sender.transfer(refund);
}
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != address(0)) {
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);
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
}
4 解析
4.1 数据结构
一笔盲拍竞价数据由两个关键要素构成:加密出价,保证金。
struct Bid {
bytes32 blindedBid; // 加密出价
uint deposit; // 保证金
}
这个合约中的出现了两类地址,受益人账户,比普通 address
多了一个 payable
修饰关键字,这表明这个账户能进行代币的相关操作。买家账户,只作为显示使用,不需要 payable
。
address payable public beneficiary;
address public highestBidder;
几个关于竞拍时间、流程控制的变量。
uint public biddingEnd;
uint public revealEnd;
bool public ended;
uint public highestBid;
将账户与竞价信息和抵押保证金相关联。
mapping(address => Bid[]) public bids;
mapping(address => uint) pendingReturns;
4.2 构造函数
solidity 的内置变量 now
将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。在这里,以秒为单位,因此 _biddingTime
, _revealTime
都是从当前开始经过XXX秒后。
constructor(
uint _biddingTime,
uint _revealTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
4.3 修改器
modifier
指示函数修改器。本示例中,这种修改器以嵌入的方式加到被作用函数上。运行时,_;
部分会用被作用函数的原有代码替代。
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }
4.4 竞价函数
该函数使用了修改器 onlyBefore
,函数执行的实际代码为
require(now > _time);
... // bid函数代码
payable
关键字修饰该函数,表明涉及资金操作(msg.value
)。其中,
_blindedBid
= keccak256(abi.encodePacked(value, fake, secret)).
是加密编码后的竞价信息,value
是出价,不得小于出价人持有的代币;fake
为 false
时竞价才有效;secret
可以视为密钥。一个账户可以多次出价,通常,竞拍者会多次把 fake
设置为 true
并且给出一个无效 value
提交,用以隐藏真正出价。
function bid(bytes32 _blindedBid)
public payable onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}
4.5 明牌函数
该函数限制为在竞价结束后,明牌结束前执行。用到了修改器 onlyAfter
,onlyBefore
。此处竞拍者需要传入自己所有的历史出价的三要素,并且以出价先后顺序排序。在明牌校验时,用到了多变量赋值语句:
(x,y,z) = (a,b,c)
明牌的具体逻辑如下。
function reveal(
uint[] memory _values,
bool[] memory _fake,
bytes32[] memory _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++) {
// [][]不是二维数组,第一层是map解出value,第二层是访问一维数组
Bid storage bidToCheck = bids[msg.sender][i];
// 多变量赋值语句
(uint value, bool fake, bytes32 secret) =
(_values[i], _fake[i], _secret[i]);
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
continue;
}
refund += bidToCheck.deposit;
if (!fake && bidToCheck.deposit >= value) {
// 尝试纳入有效竞价
if (placeBid(msg.sender, value))
refund -= value;
}
// 避免重复退款
bidToCheck.blindedBid = bytes32(0);
}
msg.sender.transfer(refund);
}
4.6 出价函数
该函数用 internal
关键字修饰,表示只能合约内部调用。类似于 Java 中的 private
关键字。入参需要在调用者的代码中进行校验。
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
4.7 退款函数
用户可以调用该函数以退回资金。此处逻辑遵循 条件(condition)--结果(effect)--交互(interact)
的标准化过程,防止重复退款的事件发生。
function withdraw() public {
uint amount = pendingReturns[msg.sender];
// 条件
if (amount > 0) {
// 先设置结果
pendingReturns[msg.sender] = 0;
// 再执行交互流程
msg.sender.transfer(amount);
}
}
4.8 结束函数
这个函数出发了一个 event
,它是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中。在DAPP的应用中,如果监听了某事件,当事件发生时,会进行回调。
event AuctionEnded(address winner, uint highestBid);
function auctionEnd()
public onlyAfter(revealEnd)
{
require(!ended);
// 事件触发
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
(完)