Blind Auction

Warning

This is example code for learning purposes. Do not use in production without thorough review and testing.

Before we dive into our other examples, let’s briefly explore another type of auction that you can build with Vyper. Similar to blind auction examples in Solidity, this contract allows for an auction where there is no time pressure towards the end of the bidding period.

  1#pragma version >0.3.10
  2
  3# Blind Auction. Adapted to Vyper from [Solidity by Example](https://github.com/ethereum/solidity/blob/develop/docs/solidity-by-example.rst#blind-auction-1)
  4
  5struct Bid:
  6  blindedBid: bytes32
  7  deposit: uint256
  8
  9# Note: because Vyper does not allow for dynamic arrays, we have limited the
 10# number of bids that can be placed by one address to 128 in this example
 11MAX_BIDS: constant(int128) = 128
 12
 13# Event for logging that auction has ended
 14event AuctionEnded:
 15    highestBidder: address
 16    highestBid: uint256
 17
 18# Auction parameters
 19beneficiary: public(address)
 20biddingEnd: public(uint256)
 21revealEnd: public(uint256)
 22
 23# Set to true at the end of auction, disallowing any new bids
 24ended: public(bool)
 25
 26# Final auction state
 27highestBid: public(uint256)
 28highestBidder: public(address)
 29
 30# State of the bids
 31bids: HashMap[address, Bid[128]]
 32bidCounts: HashMap[address, int128]
 33
 34# Allowed withdrawals of previous bids
 35pendingReturns: HashMap[address, uint256]
 36
 37
 38# Create a blinded auction with `_biddingTime` seconds bidding time and
 39# `_revealTime` seconds reveal time on behalf of the beneficiary address
 40# `_beneficiary`.
 41@deploy
 42def __init__(_beneficiary: address, _biddingTime: uint256, _revealTime: uint256):
 43    self.beneficiary = _beneficiary
 44    self.biddingEnd = block.timestamp + _biddingTime
 45    self.revealEnd = self.biddingEnd + _revealTime
 46
 47
 48# Place a blinded bid with:
 49#
 50# _blindedBid = keccak256(concat(
 51#       convert(value, bytes32),
 52#       convert(fake, bytes32),
 53#       secret)
 54# )
 55#
 56# The sent ether is only refunded if the bid is correctly revealed in the
 57# revealing phase. The bid is valid if the ether sent together with the bid is
 58# at least "value" and "fake" is not true. Setting "fake" to true and sending
 59# not the exact amount are ways to hide the real bid but still make the
 60# required deposit. The same address can place multiple bids.
 61@external
 62@payable
 63def bid(_blindedBid: bytes32):
 64    # Check if bidding period is still open
 65    assert block.timestamp < self.biddingEnd
 66
 67    # Check that payer hasn't already placed maximum number of bids
 68    numBids: int128 = self.bidCounts[msg.sender]
 69    assert numBids < MAX_BIDS
 70
 71    # Add bid to mapping of all bids
 72    self.bids[msg.sender][numBids] = Bid(
 73        blindedBid=_blindedBid,
 74        deposit=msg.value
 75        )
 76    self.bidCounts[msg.sender] += 1
 77
 78
 79# Returns a boolean value, `True` if bid placed successfully, `False` otherwise.
 80@internal
 81def placeBid(bidder: address, _value: uint256) -> bool:
 82    # If bid is less than highest bid, bid fails
 83    if (_value <= self.highestBid):
 84        return False
 85
 86    # Refund the previously highest bidder
 87    if (self.highestBidder != empty(address)):
 88        self.pendingReturns[self.highestBidder] += self.highestBid
 89
 90    # Place bid successfully and update auction state
 91    self.highestBid = _value
 92    self.highestBidder = bidder
 93
 94    return True
 95
 96
 97# Reveal your blinded bids. You will get a refund for all correctly blinded
 98# invalid bids and for all bids except for the totally highest.
 99@external
100def reveal(_numBids: int128, _values: uint256[128], _fakes: bool[128], _secrets: bytes32[128]):
101    # Check that bidding period is over
102    assert block.timestamp > self.biddingEnd
103
104    # Check that reveal end has not passed
105    assert block.timestamp < self.revealEnd
106
107    # Check that number of bids being revealed matches log for sender
108    assert _numBids == self.bidCounts[msg.sender]
109
110    # Calculate refund for sender
111    refund: uint256 = 0
112    for i: int128 in range(MAX_BIDS):
113        # Note that loop may break sooner than 128 iterations if i >= _numBids
114        if (i >= _numBids):
115            break
116
117        # Get bid to check
118        bidToCheck: Bid = (self.bids[msg.sender])[i]
119
120        # Check against encoded packet
121        value: uint256 = _values[i]
122        fake: bool = _fakes[i]
123        secret: bytes32 = _secrets[i]
124        blindedBid: bytes32 = keccak256(concat(
125            convert(value, bytes32),
126            convert(fake, bytes32),
127            secret
128        ))
129
130        # Bid was not actually revealed
131        # Do not refund deposit
132        assert blindedBid == bidToCheck.blindedBid
133
134        # Add deposit to refund if bid was indeed revealed
135        refund += bidToCheck.deposit
136        if (not fake and bidToCheck.deposit >= value):
137            if (self.placeBid(msg.sender, value)):
138                refund -= value
139
140        # Make it impossible for the sender to re-claim the same deposit
141        zeroBytes32: bytes32 = empty(bytes32)
142        bidToCheck.blindedBid = zeroBytes32
143
144    # Send refund if non-zero
145    if (refund != 0):
146        send(msg.sender, refund)
147
148
149# Withdraw a bid that was overbid.
150@external
151def withdraw():
152    # Check that there is an allowed pending return.
153    pendingAmount: uint256 = self.pendingReturns[msg.sender]
154    if (pendingAmount > 0):
155        # If so, set pending returns to zero to prevent recipient from calling
156        # this function again as part of the receiving call before `transfer`
157        # returns (see the remark above about conditions -> effects ->
158        # interaction).
159        self.pendingReturns[msg.sender] = 0
160
161        # Then send return
162        send(msg.sender, pendingAmount)
163
164
165# End the auction and send the highest bid to the beneficiary.
166@external
167def auctionEnd():
168    # Check that reveal end has passed
169    assert block.timestamp > self.revealEnd
170
171    # Check that auction has not already been marked as ended
172    assert not self.ended
173
174    # Log auction ending and set flag
175    log AuctionEnded(highestBidder=self.highestBidder, highestBid=self.highestBid)
176    self.ended = True
177
178    # Transfer funds to beneficiary
179    send(self.beneficiary, self.highestBid)

While this blind auction is almost functionally identical to the blind auction implemented in Solidity, the differences in their implementations help illustrate the differences between Solidity and Vyper.

 9# Note: because Vyper does not allow for dynamic arrays, we have limited the
10# number of bids that can be placed by one address to 128 in this example
11MAX_BIDS: constant(int128) = 128

One difference is that in this example, we use a fixed-size array, limiting the number of bids that can be placed by one address to 128 in this example. Bidders who want to make more than this maximum number of bids would need to do so from multiple addresses.