前言:
所谓秘密拍卖,就是在拍卖期间别人无法知道你的出价,因此拍卖的所有出价都是由自己决定,不受他人影响。在一定程度上,这样的秘密拍卖更能体现商品的真实价值,因为它避免了真实拍卖中抬价的现象。
秘密拍卖合约:
在solidity官方文档中,给出了一个秘密拍卖合约的代码。由于拍卖的过程需要进行交易,而交易的记录是死死记录在链上的,这不就无法秘密拍卖了吗?
它这里把交易的钱和承诺拍卖的钱进行了区分,也就是说,光从交易的钱中是无法看出真实的叫价。而真实的叫价,连同一个bool值和一个bytes32的秘密值以keccak的形式输入到合约中。因此,外人是无法知道你的真实叫价的。当且仅当拍卖结束,进入公布阶段时,所有人的拍卖才能被揭示。
需要注意的是,所有给了交易钱的人必须执行reveal函数,否则它的钱将永远无法被退回。当所有的人都执行了reveal(揭示)函数,合约会锁定最高价,然后将其他合法的叫价如数退还。倘若你忘记了你的一组(真实叫价,bool,bytes32 secret)值,那你该次的交易金额将永远锁定在合约中。揭示时间一旦过去,佛祖也回天乏力。
由于代码的复杂程度太高,我这里就不做测试了,代码如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.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` 会被用于后面的 `bid` 函数:
/// 新的函数体是由 modifier 本身的函数体,并用原函数体替换 `_;` 语句来组成的。
modifier onlyBefore(uint _time) { require(block.timestamp < _time); _; }
modifier onlyAfter(uint _time) { require(block.timestamp > _time); _; }
constructor(
uint _biddingTime,
uint _revealTime,
address payable _beneficiary
) public {
beneficiary = _beneficiary;
biddingEnd = block.timestamp + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
/// 可以通过 `_blindedBid` = keccak256(value, fake, secret)
/// 设置一个秘密竞拍。
/// 只有在出价披露阶段被正确披露,已发送的以太币才会被退还。
/// 如果与出价一起发送的以太币至少为 “value” 且 “fake” 不为真,则出价有效。
/// 将 “fake” 设置为 true ,然后发送满足订金金额但又不与出价相同的金额是隐藏实际出价的方法。
/// 同一个地址可以放置多个出价。
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 bid = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(_values[i], _fake[i], _secret[i]);
if (bid.blindedBid != keccak256(abi.encodePacked(value, fake, secret))){
// 出价未能正确披露
// 不返还订金
continue;
}
refund += bid.deposit;
if (!fake && bid.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// 使发送者不可能再次认领同一笔订金
bid.blindedBid = bytes32(0);
}
msg.sender.transfer(refund);
}
// 这是一个 "internal" 函数, 意味着它只能在本合约(或继承合约)内被调用
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) {
// 这里很重要,首先要设零值。
// 因为,作为接收调用的一部分,
// 接收者可以在 `transfer` 返回之前重新调用该函数。(可查看上面关于‘条件 -> 影响 -> 交互’的标注)
pendingReturns[msg.sender] = 0;
msg.sender.transfer(amount);
}
}
/// 结束拍卖,并把最高的出价发送给受益人
function auctionEnd()
public
onlyAfter(revealEnd)
{
require(!ended);
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
}
如上图,其实大家可能在很多地方都见过这段代码,但真的理解了吗?如果理解了,真的在remix上进行过测试了吗?只有亲手测试,才能看出一些问题。
我在这里提醒几个点
1. 在本地进行keccak256(value, fake, secret)测试时,要知道这里的value是以wei为单位的,我们一般都用ether进行测试,因为这样可以忽略掉gas。因此这里的value若以ether为单位,则要进行换算:1ether = 10^18 wei
2. 在本地做测试的时候,biddingtime和revealtime一定要调整好,如果太短则没有充足的时间测试,如果太长则需要等待一大段时间出结果
接下来,我们还是说一下这个合约中函数的功能
constructor:
构造函数,设定竞拍时间和揭示时间
bid:
规定在竞拍结束前,每个人可以多次竞拍,每次竞拍存入一个哈希和交易值
reveal:
规定在竞拍结束后,揭示结束前,每个人一般情况下必须进行揭示。倘若不进行揭示非但自己的叫价不会被记录,更重要的是自己的钱绝对拿不回来!
你需要输入的是你n次竞拍的真实信息,每次竞拍包括value,fake,secret,当且仅当你的这三个信息的keccak256与你之前输入的keccak256完全一致,你才可能会有退钱。同时,倘若你出的是最高价,将不会退钱。
这里其实是有点问题的,这就需要在所有人执行reveal两次。第一次是选取最高价,第二次是保证所有人都能退钱,可是,倘若有节点一直等待到最后一时刻才进行第一次reveal,那么它这种行为可能就会坑到人。
比如说,当前的最高价为99,出价人为A。然后,等到揭示的最后一时刻,出价人B第一次执行reveal,它的出价为100,那么B将成为最高出价人,同时,揭示阶段结束。这时候,A欲哭无泪,因为它既没有拍到,也取不出钱了。因此最稳妥的做法就是在reveal期间不停地调用reveal函数,防止被人坑。然而,这是很浪费资源的。
placeBid:
reveal调用地internal函数,负责记录了最高价以及出价人,同时将非最高价的钱转移到pendingReturn三种
withdraw:
无时间限制,可以任何时候取回自己失效的资金。前提是要通过reveal进行。并且,一定是曾经做过最高价的人才会有机会出动pendingRuturns,否则它的钱在reveal阶段就已经返还了。
autionEnd:
当揭示阶段结束,该函数负责将最高价者出的钱转移到受益人的账户上
总结:
这个办法的确做到了隐藏竞拍价格的目的,但reveal阶段非常麻烦,需要频繁reveal保证自己的曾经的最高资金在被别人盖掉后还有机会落入pendingreturns。这点可能是开发团队疏忽的。
大家如果有什么想法或者问题,欢迎在评论区留言,我们一起交流,共同进步!