拍卖的方式有几种,其中有两种概念你需要先了解下,一种是公开拍卖(open auction),一种叫盲拍(blind auction)。简单来讲就是,前一种拍卖大家都能互相看到对方的出价,而后一种则看不到。
上一篇文章我们实现了一个简单的open auction
,本篇我们来讨论下如何实现一个blind auction
。
盲拍有个核心问题就是如何保证数据的安全性,而区块链的加密特性正是解决该问题的关键。
我们实现的思路是这样的,在拍卖期间,竞拍者并不会真正的发送自己的竞价,而是发送一个本次竞价的哈希值版本。因为哈希值本身基本不会重复,所以就可以唯一代表一次竞拍。等待拍卖结束时,在reveal阶段才会公开他们的竞拍。
盲拍另一个需要解决的问题是怎样保证约束力。就是如何防止竞拍人在赢得拍卖后不发送他们的货币,也就是防止他们乱喊价。在公开拍卖的场景是不存在这个问题的,因为公开拍卖是真实的以太币转移,在区块链上是公开的,不可篡改也没法抵赖。
下面这个示例给出了一种解决方案,就是每个人可以多次竞价,同时发送价格和哈希值,哈希值的输入包括一个fake
字段,如果fake是false表示这次有效的喊价(当然不一定是最高喊价),fake是true表示本次喊价无效。通过这种方法,即使每次交易都在链上公开了,别人也不知道你哪次竞价有效。
来看下示例代码。
代码语言:javascript复制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;
// 拍卖结束后根据这个map的数据退换其他竞拍者的出价
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
接下来定义几个事件,
代码语言:javascript复制///调用某个方法太早了,也就是还没到可以调用的时间
error TooEarly(uint time);
///调用某个方法太迟了
error TooLate(uint time);
/// 拍卖结束的方法已经被调用了
error AuctionEndAlreadyCalled();
继续看,
代码语言:javascript复制 modifier onlyBefore(uint time) {
if (block.timestamp >= time) revert TooLate(time);
_;
}
modifier onlyAfter(uint time) {
if (block.timestamp <= time) revert TooEarly(time);
_;
}
modifier
是个关键字,我们可以用这个关键字自定义修饰符,修饰符就是类似external,payable这种可以加到变量或者方法前面的关键字。修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。
比如这里的onlyBefore表示传入的时间不能早于当前区块链的时间。下面会看到具体的应用例子。
代码语言:javascript复制constructor(
uint biddingTime,
uint revealTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
biddingEnd = block.timestamp biddingTime;
revealEnd = biddingEnd revealTime;
}
这个是构造函数,比较好理解。revealTime
指的是最终披露竞价结果的时间。
function bid(bytes32 blindedBid)
external
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: blindedBid,
deposit: msg.value
}));
}
竞价的核心方法,入参是一个哈希,就是我们前面讲的,盲拍是不公开真正的出价,而是根据出价计算一个哈希结果代替出价。计算方法是:
代码语言:javascript复制keccak256(abi.encodePacked(value, fake, secret))
注意这里的fake字段,前面有解释。
方法的逻辑很简单,把出价放入map就可以了。
代码语言:javascript复制function reveal(
uint[] calldata values,
bool[] calldata fakes,
bytes32[] calldata secrets
)
external
onlyAfter(biddingEnd)
onlyBefore(revealEnd)
{
uint length = bids[msg.sender].length;
require(values.length == length);
require(fakes.length == length);
require(secrets.length == length);
uint refund;
for (uint i = ; i < length; i ) {
Bid storage bidToCheck = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(values[i], fakes[i], secrets[i]);
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
// Bid was not actually revealed.
// Do not refund deposit.
continue;
}
refund = bidToCheck.deposit;
if (!fake && bidToCheck.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// Make it impossible for the sender to re-claim
// the same deposit.
bidToCheck.blindedBid = bytes32();
}
payable(msg.sender).transfer(refund);
}
reveal方法是实现盲拍的核心,它是最终披露竞拍结果的方法,这个方法首先有约束时间不能早于竞拍结束的时间,又同时不能晚于披露的时间。这里有个新的东西叫calldata
,它表示一个只读的数据入参数,这个好处是我们不用担心这个数据在外部会被修改,在函数内部就可以直接便利数据而不用先复制到内存里。
方法的开始是一段参数检查,调用者传过来的披露数据是三组数组,每组数组的长度必须要和自己在盲拍阶段的出价次数一样(每个人可以出价多次)。也就是说你要揭露的竞价要和之前盲拍阶段喊价的次数一致。
然后是执行一段循环,循环的逻辑其实就是我前面讲的,就是每个人可以多次竞价,需要判断哪次的出价是有效的,如果是有效的再去看看是否是最高出价(placeBid),不是有效的出价要退还给出价的人。
这里用到了一个内部方法,如下:
代码语言:javascript复制function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != address()) {
// Refund the previously highest bidder.
pendingReturns[highestBidder] = highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
这个方法用来判断一个人的出价是否可以作为有效的一次竞拍,如果有效就更新当前的出价信息到highestBid和highestBidder。同时为了能在竞拍结束后退款,也会更新pendingReturns。
代码语言:javascript复制 function withdraw() external {
uint amount = pendingReturns[msg.sender];
if (amount > ) {
pendingReturns[msg.sender] = ;
payable(msg.sender).transfer(amount);
}
}
退款的方法,这个其实上一篇公开拍卖也讲过,拍卖结束后要把没有赢得竞拍的钱退还回去。
代码语言:javascript复制 function auctionEnd()
external
onlyAfter(revealEnd)
{
if (ended) revert AuctionEndAlreadyCalled();
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
这个也很简单,拍卖结束了,给一些状态置位,把钱拍卖的收益转给受益人。
微信公众号:犀牛的技术笔记
参考:
- https://docs.soliditylang.org/en/v0.8.10/solidity-by-example.html