https://vyper.readthedocs.io/en/stable/vyper-by-example.html
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 remember that all Vyper syntax is valid Python3 syntax, however not all Python3 functionality is available in Vyper.
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# Open Auction
# Auction params
# Beneficiary receives money from the highest bidder
beneficiary: public(address)
auctionStart: public(uint256)
auctionEnd: public(uint256)
# Current state of auction
highestBidder: public(address)
highestBid: public(uint256)
# Set to true at the end, disallows any change
ended: public(bool)
# Keep track of refunded bids so we can follow the withdraw pattern
pendingReturns: public(HashMap[address, uint256])
# Create a simple auction with `_auction_start` and
# `_bidding_time` seconds bidding time on behalf of the
# beneficiary address `_beneficiary`.
@external
def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time: uint256):
self.beneficiary = _beneficiary
self.auctionStart = _auction_start # auction start time can be in the past, present or future
self.auctionEnd = self.auctionStart + _bidding_time
assert block.timestamp < self.auctionEnd # auction end time should be in the future
# Bid on the auction with the value sent
# together with this transaction.
# The value will only be refunded if the
# auction is not won.
@external
@payable
def bid():
# Check if bidding period has started.
assert block.timestamp >= self.auctionStart
# Check if bidding period is over.
assert block.timestamp < self.auctionEnd
# Check if bid is high enough
assert msg.value > self.highestBid
# Track the refund for the previous high bidder
self.pendingReturns[self.highestBidder] += self.highestBid
# Track new high bid
self.highestBidder = msg.sender
self.highestBid = msg.value
# Withdraw a previously refunded bid. The withdraw pattern is
# used here to avoid a security issue. If refunds were directly
# sent as part of bid(), a malicious bidding contract could block
# those refunds and thus block new higher bids from coming in.
@external
def withdraw():
pending_amount: uint256 = self.pendingReturns[msg.sender]
self.pendingReturns[msg.sender] = 0
send(msg.sender, pending_amount)
# End the auction and send the highest bid
# to the beneficiary.
@external
def endAuction():
# It is a good guideline to structure functions that interact
# with other contracts (i.e. they call functions or send Ether)
# into three phases:
# 1. checking conditions
# 2. performing actions (potentially changing conditions)
# 3. interacting with other contracts
# If these phases are mixed up, the other contract could call
# back into the current contract and modify the state or cause
# effects (Ether payout) to be performed multiple times.
# If functions called internally include interaction with external
# contracts, they also have to be considered interaction with
# external contracts.
# 1. Conditions
# Check if auction endtime has been reached
assert block.timestamp >= self.auctionEnd
# Check if this function has already been called
assert not self.ended
# 2. Effects
self.ended = True
# 3. Interaction
send(self.beneficiary, self.highestBid)
As you can see, this example only has a constructor, two 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
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Auction params
# Beneficiary receives money from the highest bidder
beneficiary: public(address)
auctionStart: public(uint256)
auctionEnd: public(uint256)
# Current state of auction
highestBidder: public(address)
highestBid: public(uint256)
# Set to true at the end, disallows any change
ended: public(bool)
# Keep track of refunded bids so we can follow the withdraw pattern
pendingReturns: 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
, the smallest denomination of ether, to manage auction state. The variable ended
is a boolean to determine whether the auction is officially over. The variable pendingReturns
is a map
which enables the use of key-value pairs to keep proper track of the auctions 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
23
24
25
26
27
@external
def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time: uint256):
self.beneficiary = _beneficiary
self.auctionStart = _auction_start # auction start time can be in the past, present or future
self.auctionEnd = self.auctionStart + _bidding_time
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 then store these three pieces of information into the contract variables self.beneficiary
, self.auctionStart
and self.auctionEnd
respectively. 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, lets look at how our users can make bids.
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@external
@payable
def bid():
# Check if bidding period has started.
assert block.timestamp >= self.auctionStart
# Check if bidding period is over.
assert block.timestamp < self.auctionEnd
# Check if bid is high enough
assert msg.value > self.highestBid
# Track the refund for the previous high bidder
self.pendingReturns[self.highestBidder] += self.highestBid
# Track new high bid
self.highestBidder = msg.sender
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
.
Note
msg.sender
and msg.value
can only be accessed from external functions. If you require these values within an internal function they must be passed as parameters.
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 the three assert
statements pass, we can safely continue to the next lines; otherwise, the bid()
method will throw an error and revert the transaction. If the two assert
statements and the check that the previous bid is not equal to zero pass, we can safely conclude that we have a valid new highest bid. We will send back the previous highestBid
to the previous highestBidder
and set our new highestBid
and highestBidder
.