Simple Open Auction¶
Warning
This is example code for learning purposes. Do not use in production without thorough review and testing.
As an introductory example of a smart contract written in Vyper, we will begin
with a simple open auction contract. As we dive into the code,
it is important to note that Vyper uses Python-like syntax, making it familiar
to Python developers, but it is a distinct language with its own type system,
decorators (like @deploy, @external), and keywords.
In this contract, we will be looking at a simple open auction contract where participants can submit bids during a limited time period. When the auction period ends, a predetermined beneficiary will receive the amount of the highest bid.
1#pragma version >0.3.10
2
3# Open Auction
4
5# Auction params
6# Beneficiary receives money from the highest bidder
7beneficiary: public(address)
8auctionStart: public(uint256)
9auctionEnd: public(uint256)
10
11# Current state of auction
12highestBidder: public(address)
13highestBid: public(uint256)
14
15# Set to true at the end, disallows any change
16ended: public(bool)
17
18# Keep track of refunded bids so we can follow the withdraw pattern
19pendingReturns: public(HashMap[address, uint256])
20
21# Create a simple auction with `_auction_start` and
22# `_bidding_time` seconds bidding time on behalf of the
23# beneficiary address `_beneficiary`.
24@deploy
25def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time: uint256):
26 self.beneficiary = _beneficiary
27 self.auctionStart = _auction_start # auction start time can be in the past, present or future
28 self.auctionEnd = self.auctionStart + _bidding_time
29 assert block.timestamp < self.auctionEnd # auction end time should be in the future
30
31# Bid on the auction with the value sent
32# together with this transaction.
33# The value will only be refunded if the
34# auction is not won.
35@external
36@payable
37def bid():
38 # Check if bidding period has started.
39 assert block.timestamp >= self.auctionStart
40 # Check if bidding period is over.
41 assert block.timestamp < self.auctionEnd
42 # Check if bid is high enough
43 assert msg.value > self.highestBid
44 # Track the refund for the previous high bidder
45 self.pendingReturns[self.highestBidder] += self.highestBid
46 # Track new high bid
47 self.highestBidder = msg.sender
48 self.highestBid = msg.value
49
50# Withdraw a previously refunded bid. The withdraw pattern is
51# used here to avoid a security issue. If refunds were directly
52# sent as part of bid(), a malicious bidding contract could block
53# those refunds and thus block new higher bids from coming in.
54@external
55def withdraw():
56 pending_amount: uint256 = self.pendingReturns[msg.sender]
57 self.pendingReturns[msg.sender] = 0
58 send(msg.sender, pending_amount)
59
60# End the auction and send the highest bid
61# to the beneficiary.
62@external
63def endAuction():
64 # It is a good guideline to structure functions that interact
65 # with other contracts (i.e. they call functions or send Ether)
66 # into three phases:
67 # 1. checking conditions
68 # 2. performing actions (potentially changing conditions)
69 # 3. interacting with other contracts
70 # If these phases are mixed up, the other contract could call
71 # back into the current contract and modify the state or cause
72 # effects (Ether payout) to be performed multiple times.
73 # If functions called internally include interaction with external
74 # contracts, they also have to be considered interaction with
75 # external contracts.
76
77 # 1. Conditions
78 # Check if auction endtime has been reached
79 assert block.timestamp >= self.auctionEnd
80 # Check if this function has already been called
81 assert not self.ended
82
83 # 2. Effects
84 self.ended = True
85
86 # 3. Interaction
87 send(self.beneficiary, self.highestBid)
As you can see, this example only has a constructor, three methods to call, and a few variables to manage the contract state. Believe it or not, this is all we need for a basic implementation of an auction smart contract.
Let’s get started!
3# Open Auction
4
5# Auction params
6# Beneficiary receives money from the highest bidder
7beneficiary: public(address)
8auctionStart: public(uint256)
9auctionEnd: public(uint256)
10
11# Current state of auction
12highestBidder: public(address)
13highestBid: public(uint256)
14
15# Set to true at the end, disallows any change
16ended: public(bool)
17
18# Keep track of refunded bids so we can follow the withdraw pattern
19pendingReturns: public(HashMap[address, uint256])
We begin by declaring a few variables to keep track of our contract state.
We initialize a global variable beneficiary by calling public on the
datatype address. The beneficiary will be the receiver of money from
the highest bidder. We also initialize the variables auctionStart and
auctionEnd with the datatype uint256 to manage the open auction
period and highestBid with datatype uint256 to manage the highest bid amount in wei. The variable ended is a
boolean to determine whether the auction is officially over. The variable pendingReturns is a HashMap which
enables the use of key-value pairs to keep proper track of the auction’s withdrawal pattern.
You may notice all of the variables being passed into the public
function. By declaring the variable public, the variable is
callable by external contracts. Initializing the variables without the public
function defaults to a private declaration and thus only accessible to methods
within the same contract. The public function additionally creates a
‘getter’ function for the variable, accessible through an external call such as
contract.beneficiary().
Now, the constructor.
22# `_bidding_time` seconds bidding time on behalf of the
23# beneficiary address `_beneficiary`.
24@deploy
25def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time: uint256):
26 self.beneficiary = _beneficiary
27 self.auctionStart = _auction_start # auction start time can be in the past, present or future
28 self.auctionEnd = self.auctionStart + _bidding_time
29 assert block.timestamp < self.auctionEnd # auction end time should be in the future
The contract is initialized with three arguments: _beneficiary of type
address, _auction_start with type uint256 and _bidding_time with
type uint256, the time difference between the start and end of the auction. We
store the beneficiary and auction start time, then compute self.auctionEnd
by adding _bidding_time to self.auctionStart.
Notice that we have access to the current time by calling block.timestamp.
block is an object available within any Vyper contract and provides information
about the block at the time of calling. Similar to block, another important object
available to us within the contract is msg, which provides information on the method
caller as we will soon see.
With initial setup out of the way, let’s look at how our users can make bids.
31# Bid on the auction with the value sent
32# together with this transaction.
33# The value will only be refunded if the
34# auction is not won.
35@external
36@payable
37def bid():
38 # Check if bidding period has started.
39 assert block.timestamp >= self.auctionStart
40 # Check if bidding period is over.
41 assert block.timestamp < self.auctionEnd
42 # Check if bid is high enough
43 assert msg.value > self.highestBid
44 # Track the refund for the previous high bidder
45 self.pendingReturns[self.highestBidder] += self.highestBid
46 # Track new high bid
47 self.highestBidder = msg.sender
48 self.highestBid = msg.value
The @payable decorator will allow a user to send some ether to the
contract in order to call the decorated method. In this case, a user wanting
to make a bid would call the bid() method while sending an amount equal
to their desired bid (not including gas fees). When calling any method within a
contract, we are provided with a built-in variable msg and we can access
the public address of any method caller with msg.sender. Similarly, the
amount of ether a user sends can be accessed by calling msg.value.
Here, we first check whether the current time is within the bidding period by
comparing with the auction’s start and end times using the assert function
which takes any boolean statement. We also check to see if the new bid is greater
than the highest bid. If all three assert statements pass, we can safely continue
to the next lines; otherwise, the bid() method will throw an error and revert the
transaction. We then record the previous highest bid in the pendingReturns mapping
(following the withdrawal pattern for security), and update highestBid and
highestBidder to reflect the new winning bid.
50# Withdraw a previously refunded bid. The withdraw pattern is
51# used here to avoid a security issue. If refunds were directly
52# sent as part of bid(), a malicious bidding contract could block
53# those refunds and thus block new higher bids from coming in.
54@external
55def withdraw():
56 pending_amount: uint256 = self.pendingReturns[msg.sender]
57 self.pendingReturns[msg.sender] = 0
58 send(msg.sender, pending_amount)
The withdraw() method allows previously outbid participants to withdraw
their funds. Rather than sending refunds directly during bid() (which
would allow a malicious contract to block new bids), we use the withdrawal
pattern:
each bidder pulls their own refund. The method reads the pending amount,
zeroes it out (to prevent re-entrancy), and sends the funds.
60# End the auction and send the highest bid
61# to the beneficiary.
62@external
63def endAuction():
64 # It is a good guideline to structure functions that interact
65 # with other contracts (i.e. they call functions or send Ether)
66 # into three phases:
67 # 1. checking conditions
68 # 2. performing actions (potentially changing conditions)
69 # 3. interacting with other contracts
70 # If these phases are mixed up, the other contract could call
71 # back into the current contract and modify the state or cause
72 # effects (Ether payout) to be performed multiple times.
73 # If functions called internally include interaction with external
74 # contracts, they also have to be considered interaction with
75 # external contracts.
76
77 # 1. Conditions
78 # Check if auction endtime has been reached
79 assert block.timestamp >= self.auctionEnd
80 # Check if this function has already been called
81 assert not self.ended
82
83 # 2. Effects
84 self.ended = True
85
86 # 3. Interaction
87 send(self.beneficiary, self.highestBid)
With the endAuction() method, we check whether our current time is past
the auctionEnd time we set upon initialization of the contract. We also
check that self.ended had not previously been set to True. We do this
to prevent any calls to the method if the auction had already ended,
which could potentially be malicious if the check had not been made.
We then officially end the auction by setting self.ended to True
and sending the highest bid amount to the beneficiary.
And there you have it - an open auction contract. Of course, this is a simplified example with barebones functionality and can be improved. Hopefully, this has provided some insight into the possibilities of Vyper. As we move on to exploring more complex examples, we will encounter more design patterns and features of the Vyper language.