Skip to main content

秘密竞拍

是什么

秘密竞拍是一种拍卖形式,在这种拍卖中,出价者的出价是加密的,直到拍卖的出价阶段结束。在智能合约中,秘密竞拍通常包括以下步骤:

  1. 初始化拍卖

    • 合约创建时设置出价结束时间和揭示结束时间。
    • 设定拍卖的受益人地址。
  2. 放置秘密出价

    • 出价者发送加密的出价和押金到合约。
    • 加密的出价是通过哈希函数(如 keccak256)生成的,包括出价金额、是否为虚假出价的标记和一个秘密值。
  3. 揭示出价

    • 出价阶段结束后,在一个特定时间窗口内出价者揭示他们的出价。
    • 出价者提供他们的出价金额、是否虚假出价的标记和秘密值。
    • 合约验证揭示的信息是否与加密出价匹配。
    • 如果出价有效且未被超过,它可能成为新的最高出价。
  4. 取回未成功的出价

    • 如果出价者的出价不是最高的,他们可以取回他们的押金。
  5. 结束拍卖

    • 在揭示阶段结束后,任何人都可以调用函数来结束拍卖。
    • 合约将最高出价发送给受益人,并记录拍卖结束。

合约

在 Solidity 代码中,这些步骤通过函数和修饰符(modifiers)来实现,以确保每个步骤按照正确的逻辑和时间顺序执行。

pragma solidity >0.4.23 <0.5.0;

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);

/// 使用 modifier 可以更便捷的校验函数的入参。
/// `onlyBefore` 会被用于后面的 `bid` 函数:
/// 新的函数体是由 modifier 本身的函数体,并用原函数体替换 `_;` 语句来组成的。
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }

constructor( uint _biddingTime, uint _revealTime, address _beneficiary) public {
beneficiary = _beneficiary;
biddingEnd = now + _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[] _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++) {
Bid storage bid = bids[msg.sender][i];
(uint value, bool fake, bytes32 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 = 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);
}
}

条件-影响-交互 ?

在智能合约的开发中,遵循"条件-影响-交互"(Checks-Effects-Interactions)模式是一种重要的最佳实践,旨在减少智能合约中的安全漏洞,特别是防止重入攻击。这种模式建议在执行外部调用之前,首先检查所有的前置条件,然后更新合约的状态,最后进行外部调用或交互。下面是这三个步骤的详细说明:

  1. 条件(Checks)

    • 在执行任何逻辑之前,首先验证所有的输入和条件是否满足。
    • 这通常涉及到使用requireassertrevert语句来检查函数的参数,合约的状态变量,以及合约逻辑所依赖的任何条件。
  2. 影响(Effects)

    • 在进行任何外部交互之前,先更新合约的状态。
    • 这意味着在发送事件、转移资金或调用外部合约之前,应该先改变状态变量。
  3. 交互(Interactions)

    • 最后,执行所有的外部调用,比如向其他合约发送消息或转账。
    • 这一步应该在所有的检查和状态更新之后进行,以避免潜在的重入攻击。

在上面提供的盲拍合约代码中,这个模式在withdraw函数中得到了很好的体现:

function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 影响:首先更新状态
pendingReturns[msg.sender] = 0;

// 交互:然后执行交互
msg.sender.transfer(amount);
}
}

在这个函数中,首先是检查(条件)amount 是否大于 0,然后是更新(影响)pendingReturns[msg.sender] 状态为 0,最后是执行(交互)transfer 来发送资金。这个顺序确保了即使transfer调用的外部合约尝试重新进入withdraw函数,它也不能提取任何资金,因为pendingReturns[msg.sender] 已经被设置为 0 了。

如果影响后交互前失败了,会回滚吗?

是的,如果在更新状态(影响)后但在进行外部交互(交互)之前发生失败,智能合约的状态会回滚到交易开始前的状态。在以太坊智能合约中,如果在执行交易的过程中发生任何错误(例如,因为requirerevertassert引起的错误,或者资源(如 gas)耗尽),整个交易都会被回滚。

这意味着任何在交易过程中对智能合约状态所做的修改都将被撤销,就像这次交易从未发生过一样。这包括所有的状态变量更新、事件日志和任何以太币的转移。这是以太坊区块链的原子性特征的一部分,确保了交易要么完全执行,要么完全不执行。

在"条件-影响-交互"模式中,即使在状态更新后发生了失败,由于整个交易都会回滚,所以不会影响合约的安全性。这个特性是以太坊提供的一种保护机制,确保了智能合约的一致性和可预测性。