Vyper by Example

Simple Open Auction

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# Open Auction
 2
 3# Auction params
 4# Beneficiary receives money from the highest bidder
 5beneficiary: public(address)
 6auctionStart: public(uint256)
 7auctionEnd: public(uint256)
 8
 9# Current state of auction
10highestBidder: public(address)
11highestBid: public(uint256)
12
13# Set to true at the end, disallows any change
14ended: public(bool)
15
16# Keep track of refunded bids so we can follow the withdraw pattern
17pendingReturns: public(HashMap[address, uint256])
18
19# Create a simple auction with `_auction_start` and
20# `_bidding_time` seconds bidding time on behalf of the
21# beneficiary address `_beneficiary`.
22@external
23def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time: uint256):
24    self.beneficiary = _beneficiary
25    self.auctionStart = _auction_start  # auction start time can be in the past, present or future
26    self.auctionEnd = self.auctionStart + _bidding_time
27    assert block.timestamp < self.auctionEnd # auction end time should be in the future
28
29# Bid on the auction with the value sent
30# together with this transaction.
31# The value will only be refunded if the
32# auction is not won.
33@external
34@payable
35def bid():
36    # Check if bidding period has started.
37    assert block.timestamp >= self.auctionStart
38    # Check if bidding period is over.
39    assert block.timestamp < self.auctionEnd
40    # Check if bid is high enough
41    assert msg.value > self.highestBid
42    # Track the refund for the previous high bidder
43    self.pendingReturns[self.highestBidder] += self.highestBid
44    # Track new high bid
45    self.highestBidder = msg.sender
46    self.highestBid = msg.value
47
48# Withdraw a previously refunded bid. The withdraw pattern is
49# used here to avoid a security issue. If refunds were directly
50# sent as part of bid(), a malicious bidding contract could block
51# those refunds and thus block new higher bids from coming in.
52@external
53def withdraw():
54    pending_amount: uint256 = self.pendingReturns[msg.sender]
55    self.pendingReturns[msg.sender] = 0
56    send(msg.sender, pending_amount)
57
58# End the auction and send the highest bid
59# to the beneficiary.
60@external
61def endAuction():
62    # It is a good guideline to structure functions that interact
63    # with other contracts (i.e. they call functions or send Ether)
64    # into three phases:
65    # 1. checking conditions
66    # 2. performing actions (potentially changing conditions)
67    # 3. interacting with other contracts
68    # If these phases are mixed up, the other contract could call
69    # back into the current contract and modify the state or cause
70    # effects (Ether payout) to be performed multiple times.
71    # If functions called internally include interaction with external
72    # contracts, they also have to be considered interaction with
73    # external contracts.
74
75    # 1. Conditions
76    # Check if auction endtime has been reached
77    assert block.timestamp >= self.auctionEnd
78    # Check if this function has already been called
79    assert not self.ended
80
81    # 2. Effects
82    self.ended = True
83
84    # 3. Interaction
85    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# Auction params
 4# Beneficiary receives money from the highest bidder
 5beneficiary: public(address)
 6auctionStart: public(uint256)
 7auctionEnd: public(uint256)
 8
 9# Current state of auction
10highestBidder: public(address)
11highestBid: public(uint256)
12
13# Set to true at the end, disallows any change
14ended: public(bool)
15
16# Keep track of refunded bids so we can follow the withdraw pattern
17pendingReturns: 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@external
23def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time: uint256):
24    self.beneficiary = _beneficiary
25    self.auctionStart = _auction_start  # auction start time can be in the past, present or future
26    self.auctionEnd = self.auctionStart + _bidding_time
27    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@external
34@payable
35def bid():
36    # Check if bidding period has started.
37    assert block.timestamp >= self.auctionStart
38    # Check if bidding period is over.
39    assert block.timestamp < self.auctionEnd
40    # Check if bid is high enough
41    assert msg.value > self.highestBid
42    # Track the refund for the previous high bidder
43    self.pendingReturns[self.highestBidder] += self.highestBid
44    # Track new high bid
45    self.highestBidder = msg.sender
46    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 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.

60@external
61def endAuction():
62    # It is a good guideline to structure functions that interact
63    # with other contracts (i.e. they call functions or send Ether)
64    # into three phases:
65    # 1. checking conditions
66    # 2. performing actions (potentially changing conditions)
67    # 3. interacting with other contracts
68    # If these phases are mixed up, the other contract could call
69    # back into the current contract and modify the state or cause
70    # effects (Ether payout) to be performed multiple times.
71    # If functions called internally include interaction with external
72    # contracts, they also have to be considered interaction with
73    # external contracts.
74
75    # 1. Conditions
76    # Check if auction endtime has been reached
77    assert block.timestamp >= self.auctionEnd
78    # Check if this function has already been called
79    assert not self.ended
80
81    # 2. Effects
82    self.ended = True
83
84    # 3. Interaction
85    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.

And of course, no smart contract tutorial is complete without a note on security.

Note

It’s always important to keep security in mind when designing a smart contract. As any application becomes more complex, the greater the potential for introducing new risks. Thus, it’s always good practice to keep contracts as readable and simple as possible.

Whenever you’re ready, let’s turn it up a notch in the next example.

Blind Auction

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

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

28# State of the bids
29bids: HashMap[address, Bid[128]]
30bidCounts: HashMap[address, int128]

One key difference is that, because Vyper does not allow for dynamic arrays, we have limited 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.

Safe Remote Purchases

In this example, we have an escrow contract implementing a system for a trustless transaction between a buyer and a seller. In this system, a seller posts an item for sale and makes a deposit to the contract of twice the item’s value. At this moment, the contract has a balance of 2 * value. The seller can reclaim the deposit and close the sale as long as a buyer has not yet made a purchase. If a buyer is interested in making a purchase, they would make a payment and submit an equal amount for deposit (totaling 2 * value) into the contract and locking the contract from further modification. At this moment, the contract has a balance of 4 * value and the seller would send the item to buyer. Upon the buyer’s receipt of the item, the buyer will mark the item as received in the contract, thereby returning the buyer’s deposit (not payment), releasing the remaining funds to the seller, and completing the transaction.

There are certainly others ways of designing a secure escrow system with less overhead for both the buyer and seller, but for the purpose of this example, we want to explore one way how an escrow system can be implemented trustlessly.

Let’s go!

 1# Safe Remote Purchase
 2# Originally from
 3# https://github.com/ethereum/solidity/blob/develop/docs/solidity-by-example.rst
 4# Ported to vyper and optimized.
 5
 6# Rundown of the transaction:
 7# 1. Seller posts item for sale and posts safety deposit of double the item value.
 8#    Balance is 2*value.
 9#    (1.1. Seller can reclaim deposit and close the sale as long as nothing was purchased.)
10# 2. Buyer purchases item (value) plus posts an additional safety deposit (Item value).
11#    Balance is 4*value.
12# 3. Seller ships item.
13# 4. Buyer confirms receiving the item. Buyer's deposit (value) is returned.
14#    Seller's deposit (2*value) + items value is returned. Balance is 0.
15
16value: public(uint256) #Value of the item
17seller: public(address)
18buyer: public(address)
19unlocked: public(bool)
20ended: public(bool)
21
22@external
23@payable
24def __init__():
25    assert (msg.value % 2) == 0
26    self.value = msg.value / 2  # The seller initializes the contract by
27        # posting a safety deposit of 2*value of the item up for sale.
28    self.seller = msg.sender
29    self.unlocked = True
30
31@external
32def abort():
33    assert self.unlocked #Is the contract still refundable?
34    assert msg.sender == self.seller # Only the seller can refund
35        # his deposit before any buyer purchases the item.
36    selfdestruct(self.seller) # Refunds the seller and deletes the contract.
37
38@external
39@payable
40def purchase():
41    assert self.unlocked # Is the contract still open (is the item still up
42                         # for sale)?
43    assert msg.value == (2 * self.value) # Is the deposit the correct value?
44    self.buyer = msg.sender
45    self.unlocked = False
46
47@external
48def received():
49    # 1. Conditions
50    assert not self.unlocked # Is the item already purchased and pending
51                             # confirmation from the buyer?
52    assert msg.sender == self.buyer
53    assert not self.ended
54
55    # 2. Effects
56    self.ended = True
57
58    # 3. Interaction
59    send(self.buyer, self.value) # Return the buyer's deposit (=value) to the buyer.
60    selfdestruct(self.seller) # Return the seller's deposit (=2*value) and the
61                              # purchase price (=value) to the seller.

This is also a moderately short contract, however a little more complex in logic. Let’s break down this contract bit by bit.

16value: public(uint256) #Value of the item
17seller: public(address)
18buyer: public(address)
19unlocked: public(bool)

Like the other contracts, we begin by declaring our global variables public with their respective data types. Remember that the public function allows the variables to be readable by an external caller, but not writeable.

22@external
23@payable
24def __init__():
25    assert (msg.value % 2) == 0
26    self.value = msg.value / 2  # The seller initializes the contract by
27        # posting a safety deposit of 2*value of the item up for sale.
28    self.seller = msg.sender
29    self.unlocked = True

With a @payable decorator on the constructor, the contract creator will be required to make an initial deposit equal to twice the item’s value to initialize the contract, which will be later returned. This is in addition to the gas fees needed to deploy the contract on the blockchain, which is not returned. We assert that the deposit is divisible by 2 to ensure that the seller deposited a valid amount. The constructor stores the item’s value in the contract variable self.value and saves the contract creator into self.seller. The contract variable self.unlocked is initialized to True.

31@external
32def abort():
33    assert self.unlocked #Is the contract still refundable?
34    assert msg.sender == self.seller # Only the seller can refund
35        # his deposit before any buyer purchases the item.
36    selfdestruct(self.seller) # Refunds the seller and deletes the contract.

The abort() method is a method only callable by the seller and while the contract is still unlocked—meaning it is callable only prior to any buyer making a purchase. As we will see in the purchase() method that when a buyer calls the purchase() method and sends a valid amount to the contract, the contract will be locked and the seller will no longer be able to call abort().

When the seller calls abort() and if the assert statements pass, the contract will call the selfdestruct() function and refunds the seller and subsequently destroys the contract.

38@external
39@payable
40def purchase():
41    assert self.unlocked # Is the contract still open (is the item still up
42                         # for sale)?
43    assert msg.value == (2 * self.value) # Is the deposit the correct value?
44    self.buyer = msg.sender
45    self.unlocked = False

Like the constructor, the purchase() method has a @payable decorator, meaning it can be called with a payment. For the buyer to make a valid purchase, we must first assert that the contract’s unlocked property is True and that the amount sent is equal to twice the item’s value. We then set the buyer to the msg.sender and lock the contract. At this point, the contract has a balance equal to 4 times the item value and the seller must send the item to the buyer.

47@external
48def received():
49    # 1. Conditions
50    assert not self.unlocked # Is the item already purchased and pending
51                             # confirmation from the buyer?
52    assert msg.sender == self.buyer
53    assert not self.ended
54
55    # 2. Effects
56    self.ended = True
57
58    # 3. Interaction
59    send(self.buyer, self.value) # Return the buyer's deposit (=value) to the buyer.
60    selfdestruct(self.seller) # Return the seller's deposit (=2*value) and the
61                              # purchase price (=value) to the seller.

Finally, upon the buyer’s receipt of the item, the buyer can confirm their receipt by calling the received() method to distribute the funds as intended—where the seller receives 3/4 of the contract balance and the buyer receives 1/4.

By calling received(), we begin by checking that the contract is indeed locked, ensuring that a buyer had previously paid. We also ensure that this method is only callable by the buyer. If these two assert statements pass, we refund the buyer their initial deposit and send the seller the remaining funds. The contract is finally destroyed and the transaction is complete.

Whenever we’re ready, let’s move on to the next example.

Crowdfund

Now, let’s explore a straightforward example for a crowdfunding contract where prospective participants can contribute funds to a campaign. If the total contribution to the campaign reaches or surpasses a predetermined funding goal, the funds will be sent to the beneficiary at the end of the campaign deadline. Participants will be refunded their respective contributions if the total funding does not reach its target goal.

 1# Setup private variables (only callable from within the contract)
 2
 3funders: HashMap[address, uint256]
 4beneficiary: address
 5deadline: public(uint256)
 6goal: public(uint256)
 7timelimit: public(uint256)
 8
 9# Setup global variables
10@external
11def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256):
12    self.beneficiary = _beneficiary
13    self.deadline = block.timestamp + _timelimit
14    self.timelimit = _timelimit
15    self.goal = _goal
16
17# Participate in this crowdfunding campaign
18@external
19@payable
20def participate():
21    assert block.timestamp < self.deadline, "deadline not met (yet)"
22
23    self.funders[msg.sender] += msg.value
24
25# Enough money was raised! Send funds to the beneficiary
26@external
27def finalize():
28    assert block.timestamp >= self.deadline, "deadline has passed"
29    assert self.balance >= self.goal, "the goal has not been reached"
30
31    selfdestruct(self.beneficiary)
32
33# Let participants withdraw their fund
34@external
35def refund():
36    assert block.timestamp >= self.deadline and self.balance < self.goal
37    assert self.funders[msg.sender] > 0
38
39    value: uint256 = self.funders[msg.sender]
40    self.funders[msg.sender] = 0
41
42    send(msg.sender, value)

Most of this code should be relatively straightforward after going through our previous examples. Let’s dive right in.

 3funders: HashMap[address, uint256]
 4beneficiary: address
 5deadline: public(uint256)
 6goal: public(uint256)
 7timelimit: public(uint256)
 8
 9# Setup global variables
10@external
11def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256):
12    self.beneficiary = _beneficiary
13    self.deadline = block.timestamp + _timelimit

Like other examples, we begin by initiating our variables - except this time, we’re not calling them with the public function. Variables initiated this way are, by default, private.

Note

Unlike the existence of the function public(), there is no equivalent private() function. Variables simply default to private if initiated without the public() function.

The funders variable is initiated as a mapping where the key is an address, and the value is a number representing the contribution of each participant. The beneficiary will be the final receiver of the funds once the crowdfunding period is over—as determined by the deadline and timelimit variables. The goal variable is the target total contribution of all participants.

 9# Setup global variables
10@external
11def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256):
12    self.beneficiary = _beneficiary
13    self.deadline = block.timestamp + _timelimit
14    self.timelimit = _timelimit
15    self.goal = _goal

Our constructor function takes 3 arguments: the beneficiary’s address, the goal in wei value, and the difference in time from start to finish of the crowdfunding. We initialize the arguments as contract variables with their corresponding names. Additionally, a self.deadline is initialized to set a definitive end time for the crowdfunding period.

Now lets take a look at how a person can participate in the crowdfund.

17# Participate in this crowdfunding campaign
18@external
19@payable
20def participate():
21    assert block.timestamp < self.deadline, "deadline not met (yet)"
22
23    self.funders[msg.sender] += msg.value

Once again, we see the @payable decorator on a method, which allows a person to send some ether along with a call to the method. In this case, the participate() method accesses the sender’s address with msg.sender and the corresponding amount sent with msg.value. This information is stored into a struct and then saved into the funders mapping with self.nextFunderIndex as the key. As more participants are added to the mapping, self.nextFunderIndex increments appropriately to properly index each participant.

25# Enough money was raised! Send funds to the beneficiary
26@external
27def finalize():
28    assert block.timestamp >= self.deadline, "deadline has passed"
29    assert self.balance >= self.goal, "the goal has not been reached"
30
31    selfdestruct(self.beneficiary)

The finalize() method is used to complete the crowdfunding process. However, to complete the crowdfunding, the method first checks to see if the crowdfunding period is over and that the balance has reached/passed its set goal. If those two conditions pass, the contract calls the selfdestruct() function and sends the collected funds to the beneficiary.

Note

Notice that we have access to the total amount sent to the contract by calling self.balance, a variable we never explicitly set. Similar to msg and block, self.balance is a built-in variable that’s available in all Vyper contracts.

We can finalize the campaign if all goes well, but what happens if the crowdfunding campaign isn’t successful? We’re going to need a way to refund all the participants.

33# Let participants withdraw their fund
34@external
35def refund():
36    assert block.timestamp >= self.deadline and self.balance < self.goal
37    assert self.funders[msg.sender] > 0
38
39    value: uint256 = self.funders[msg.sender]
40    self.funders[msg.sender] = 0
41
42    send(msg.sender, value)

In the refund() method, we first check that the crowdfunding period is indeed over and that the total collected balance is less than the goal with the assert statement . If those two conditions pass, we let users get their funds back using the withdraw pattern.

Voting

In this contract, we will implement a system for participants to vote on a list of proposals. The chairperson of the contract will be able to give each participant the right to vote, and each participant may choose to vote, or delegate their vote to another voter. Finally, a winning proposal will be determined upon calling the winningProposals() method, which iterates through all the proposals and returns the one with the greatest number of votes.

  1# Voting with delegation.
  2
  3# Information about voters
  4struct Voter:
  5    # weight is accumulated by delegation
  6    weight: int128
  7    # if true, that person already voted (which includes voting by delegating)
  8    voted: bool
  9    # person delegated to
 10    delegate: address
 11    # index of the voted proposal, which is not meaningful unless `voted` is True.
 12    vote: int128
 13
 14# Users can create proposals
 15struct Proposal:
 16    # short name (up to 32 bytes)
 17    name: bytes32
 18    # number of accumulated votes
 19    voteCount: int128
 20
 21voters: public(HashMap[address, Voter])
 22proposals: public(HashMap[int128, Proposal])
 23voterCount: public(int128)
 24chairperson: public(address)
 25int128Proposals: public(int128)
 26
 27
 28@view
 29@internal
 30def _delegated(addr: address) -> bool:
 31    return self.voters[addr].delegate != empty(address)
 32
 33
 34@view
 35@external
 36def delegated(addr: address) -> bool:
 37    return self._delegated(addr)
 38
 39
 40@view
 41@internal
 42def _directlyVoted(addr: address) -> bool:
 43    return self.voters[addr].voted and (self.voters[addr].delegate == empty(address))
 44
 45
 46@view
 47@external
 48def directlyVoted(addr: address) -> bool:
 49    return self._directlyVoted(addr)
 50
 51
 52# Setup global variables
 53@external
 54def __init__(_proposalNames: bytes32[2]):
 55    self.chairperson = msg.sender
 56    self.voterCount = 0
 57    for i in range(2):
 58        self.proposals[i] = Proposal({
 59            name: _proposalNames[i],
 60            voteCount: 0
 61        })
 62        self.int128Proposals += 1
 63
 64# Give a `voter` the right to vote on this ballot.
 65# This may only be called by the `chairperson`.
 66@external
 67def giveRightToVote(voter: address):
 68    # Throws if the sender is not the chairperson.
 69    assert msg.sender == self.chairperson
 70    # Throws if the voter has already voted.
 71    assert not self.voters[voter].voted
 72    # Throws if the voter's voting weight isn't 0.
 73    assert self.voters[voter].weight == 0
 74    self.voters[voter].weight = 1
 75    self.voterCount += 1
 76
 77# Used by `delegate` below, callable externally via `forwardWeight`
 78@internal
 79def _forwardWeight(delegate_with_weight_to_forward: address):
 80    assert self._delegated(delegate_with_weight_to_forward)
 81    # Throw if there is nothing to do:
 82    assert self.voters[delegate_with_weight_to_forward].weight > 0
 83
 84    target: address = self.voters[delegate_with_weight_to_forward].delegate
 85    for i in range(4):
 86        if self._delegated(target):
 87            target = self.voters[target].delegate
 88            # The following effectively detects cycles of length <= 5,
 89            # in which the delegation is given back to the delegator.
 90            # This could be done for any int128ber of loops,
 91            # or even infinitely with a while loop.
 92            # However, cycles aren't actually problematic for correctness;
 93            # they just result in spoiled votes.
 94            # So, in the production version, this should instead be
 95            # the responsibility of the contract's client, and this
 96            # check should be removed.
 97            assert target != delegate_with_weight_to_forward
 98        else:
 99            # Weight will be moved to someone who directly voted or
100            # hasn't voted.
101            break
102
103    weight_to_forward: int128 = self.voters[delegate_with_weight_to_forward].weight
104    self.voters[delegate_with_weight_to_forward].weight = 0
105    self.voters[target].weight += weight_to_forward
106
107    if self._directlyVoted(target):
108        self.proposals[self.voters[target].vote].voteCount += weight_to_forward
109        self.voters[target].weight = 0
110
111    # To reiterate: if target is also a delegate, this function will need
112    # to be called again, similarly to as above.
113
114# Public function to call _forwardWeight
115@external
116def forwardWeight(delegate_with_weight_to_forward: address):
117    self._forwardWeight(delegate_with_weight_to_forward)
118
119# Delegate your vote to the voter `to`.
120@external
121def delegate(to: address):
122    # Throws if the sender has already voted
123    assert not self.voters[msg.sender].voted
124    # Throws if the sender tries to delegate their vote to themselves or to
125    # the default address value of 0x0000000000000000000000000000000000000000
126    # (the latter might not be problematic, but I don't want to think about it).
127    assert to != msg.sender
128    assert to != empty(address)
129
130    self.voters[msg.sender].voted = True
131    self.voters[msg.sender].delegate = to
132
133    # This call will throw if and only if this delegation would cause a loop
134        # of length <= 5 that ends up delegating back to the delegator.
135    self._forwardWeight(msg.sender)
136
137# Give your vote (including votes delegated to you)
138# to proposal `proposals[proposal].name`.
139@external
140def vote(proposal: int128):
141    # can't vote twice
142    assert not self.voters[msg.sender].voted
143    # can only vote on legitimate proposals
144    assert proposal < self.int128Proposals
145
146    self.voters[msg.sender].vote = proposal
147    self.voters[msg.sender].voted = True
148
149    # transfer msg.sender's weight to proposal
150    self.proposals[proposal].voteCount += self.voters[msg.sender].weight
151    self.voters[msg.sender].weight = 0
152
153# Computes the winning proposal taking all
154# previous votes into account.
155@view
156@internal
157def _winningProposal() -> int128:
158    winning_vote_count: int128 = 0
159    winning_proposal: int128 = 0
160    for i in range(2):
161        if self.proposals[i].voteCount > winning_vote_count:
162            winning_vote_count = self.proposals[i].voteCount
163            winning_proposal = i
164    return winning_proposal
165
166@view
167@external
168def winningProposal() -> int128:
169    return self._winningProposal()
170
171
172# Calls winningProposal() function to get the index
173# of the winner contained in the proposals array and then
174# returns the name of the winner
175@view
176@external
177def winnerName() -> bytes32:
178    return self.proposals[self._winningProposal()].name

As we can see, this is the contract of moderate length which we will dissect section by section. Let’s begin!

 3# Information about voters
 4struct Voter:
 5    # weight is accumulated by delegation
 6    weight: int128
 7    # if true, that person already voted (which includes voting by delegating)
 8    voted: bool
 9    # person delegated to
10    delegate: address
11    # index of the voted proposal, which is not meaningful unless `voted` is True.
12    vote: int128
13
14# Users can create proposals
15struct Proposal:
16    # short name (up to 32 bytes)
17    name: bytes32
18    # number of accumulated votes
19    voteCount: int128
20
21voters: public(HashMap[address, Voter])
22proposals: public(HashMap[int128, Proposal])
23voterCount: public(int128)
24chairperson: public(address)
25int128Proposals: public(int128)

The variable voters is initialized as a mapping where the key is the voter’s public address and the value is a struct describing the voter’s properties: weight, voted, delegate, and vote, along with their respective data types.

Similarly, the proposals variable is initialized as a public mapping with int128 as the key’s datatype and a struct to represent each proposal with the properties name and vote_count. Like our last example, we can access any value by key’ing into the mapping with a number just as one would with an index in an array.

Then, voterCount and chairperson are initialized as public with their respective datatypes.

Let’s move onto the constructor.

53@external
54def __init__(_proposalNames: bytes32[2]):
55    self.chairperson = msg.sender
56    self.voterCount = 0
57    for i in range(2):
58        self.proposals[i] = Proposal({
59            name: _proposalNames[i],
60            voteCount: 0
61        })
62        self.int128Proposals += 1

In the constructor, we hard-coded the contract to accept an array argument of exactly two proposal names of type bytes32 for the contracts initialization. Because upon initialization, the __init__() method is called by the contract creator, we have access to the contract creator’s address with msg.sender and store it in the contract variable self.chairperson. We also initialize the contract variable self.voter_count to zero to initially represent the number of votes allowed. This value will be incremented as each participant in the contract is given the right to vote by the method giveRightToVote(), which we will explore next. We loop through the two proposals from the argument and insert them into proposals mapping with their respective index in the original array as its key.

Now that the initial setup is done, lets take a look at the functionality.

66@external
67def giveRightToVote(voter: address):
68    # Throws if the sender is not the chairperson.
69    assert msg.sender == self.chairperson
70    # Throws if the voter has already voted.
71    assert not self.voters[voter].voted
72    # Throws if the voter's voting weight isn't 0.
73    assert self.voters[voter].weight == 0
74    self.voters[voter].weight = 1
75    self.voterCount += 1

Note

Throughout this contract, we use a pattern where @external functions return data from @internal functions that have the same name prepended with an underscore. This is because Vyper does not allow calls between external functions within the same contract. The internal function handles the logic and allows internal access, while the external function acts as a getter to allow external viewing.

We need a way to control who has the ability to vote. The method giveRightToVote() is a method callable by only the chairperson by taking a voter address and granting it the right to vote by incrementing the voter’s weight property. We sequentially check for 3 conditions using assert. The assert not function will check for falsy boolean values - in this case, we want to know that the voter has not already voted. To represent voting power, we will set their weight to 1 and we will keep track of the total number of voters by incrementing voterCount.

120@external
121def delegate(to: address):
122    # Throws if the sender has already voted
123    assert not self.voters[msg.sender].voted
124    # Throws if the sender tries to delegate their vote to themselves or to
125    # the default address value of 0x0000000000000000000000000000000000000000
126    # (the latter might not be problematic, but I don't want to think about it).
127    assert to != msg.sender
128    assert to != empty(address)
129
130    self.voters[msg.sender].voted = True
131    self.voters[msg.sender].delegate = to
132
133    # This call will throw if and only if this delegation would cause a loop
134        # of length <= 5 that ends up delegating back to the delegator.
135    self._forwardWeight(msg.sender)

In the method delegate, firstly, we check to see that msg.sender has not already voted and secondly, that the target delegate and the msg.sender are not the same. Voters shouldn’t be able to delegate votes to themselves. We, then, loop through all the voters to determine whether the person delegate to had further delegated their vote to someone else in order to follow the chain of delegation. We then mark the msg.sender as having voted if they delegated their vote. We increment the proposal’s voterCount directly if the delegate had already voted or increase the delegate’s vote weight if the delegate has not yet voted.

139@external
140def vote(proposal: int128):
141    # can't vote twice
142    assert not self.voters[msg.sender].voted
143    # can only vote on legitimate proposals
144    assert proposal < self.int128Proposals
145
146    self.voters[msg.sender].vote = proposal
147    self.voters[msg.sender].voted = True
148
149    # transfer msg.sender's weight to proposal
150    self.proposals[proposal].voteCount += self.voters[msg.sender].weight
151    self.voters[msg.sender].weight = 0

Now, let’s take a look at the logic inside the vote() method, which is surprisingly simple. The method takes the key of the proposal in the proposals mapping as an argument, check that the method caller had not already voted, sets the voter’s vote property to the proposal key, and increments the proposals voteCount by the voter’s weight.

With all the basic functionality complete, what’s left is simply returning the winning proposal. To do this, we have two methods: winningProposal(), which returns the key of the proposal, and winnerName(), returning the name of the proposal. Notice the @view decorator on these two methods. We do this because the two methods only read the blockchain state and do not modify it. Remember, reading the blockchain state is free; modifying the state costs gas. By having the @view decorator, we let the EVM know that this is a read-only function and we benefit by saving gas fees.

153# Computes the winning proposal taking all
154# previous votes into account.
155@view
156@internal
157def _winningProposal() -> int128:
158    winning_vote_count: int128 = 0
159    winning_proposal: int128 = 0
160    for i in range(2):
161        if self.proposals[i].voteCount > winning_vote_count:
162            winning_vote_count = self.proposals[i].voteCount
163            winning_proposal = i
164    return winning_proposal
165
166@view
167@external
168def winningProposal() -> int128:
169    return self._winningProposal()
170

The _winningProposal() method returns the key of proposal in the proposals mapping. We will keep track of greatest number of votes and the winning proposal with the variables winningVoteCount and winningProposal, respectively by looping through all the proposals.

winningProposal() is an external function allowing access to _winningProposal().

175@view
176@external
177def winnerName() -> bytes32:
178    return self.proposals[self._winningProposal()].name

And finally, the winnerName() method returns the name of the proposal by key’ing into the proposals mapping with the return result of the winningProposal() method.

And there you have it - a voting contract. Currently, many transactions are needed to assign the rights to vote to all participants. As an exercise, can we try to optimize this?

Now that we’re familiar with basic contracts. Let’s step up the difficulty.

Company Stock

This contract is just a tad bit more thorough than the ones we’ve previously encountered. In this example, we are going to look at a comprehensive contract that manages the holdings of all shares of a company. The contract allows for a person to buy, sell and transfer shares of a company as well as allowing for the company to pay a person in ether. The company, upon initialization of the contract, holds all shares of the company at first but can sell them all.

Let’s get started.

  1# Financial events the contract logs
  2
  3event Transfer:
  4    sender: indexed(address)
  5    receiver: indexed(address)
  6    value: uint256
  7
  8event Buy:
  9    buyer: indexed(address)
 10    buy_order: uint256
 11
 12event Sell:
 13    seller: indexed(address)
 14    sell_order: uint256
 15
 16event Pay:
 17    vendor: indexed(address)
 18    amount: uint256
 19
 20
 21# Initiate the variables for the company and it's own shares.
 22company: public(address)
 23totalShares: public(uint256)
 24price: public(uint256)
 25
 26# Store a ledger of stockholder holdings.
 27holdings: HashMap[address, uint256]
 28
 29# Set up the company.
 30@external
 31def __init__(_company: address, _total_shares: uint256, initial_price: uint256):
 32    assert _total_shares > 0
 33    assert initial_price > 0
 34
 35    self.company = _company
 36    self.totalShares = _total_shares
 37    self.price = initial_price
 38
 39    # The company holds all the shares at first, but can sell them all.
 40    self.holdings[self.company] = _total_shares
 41
 42# Public function to allow external access to _stockAvailable
 43@view
 44@external
 45def stockAvailable() -> uint256:
 46    return self._stockAvailable()
 47
 48# Give some value to the company and get stock in return.
 49@external
 50@payable
 51def buyStock():
 52    # Note: full amount is given to company (no fractional shares),
 53    #       so be sure to send exact amount to buy shares
 54    buy_order: uint256 = msg.value / self.price # rounds down
 55
 56    # Check that there are enough shares to buy.
 57    assert self._stockAvailable() >= buy_order
 58
 59    # Take the shares off the market and give them to the stockholder.
 60    self.holdings[self.company] -= buy_order
 61    self.holdings[msg.sender] += buy_order
 62
 63    # Log the buy event.
 64    log Buy(msg.sender, buy_order)
 65
 66# Public function to allow external access to _getHolding
 67@view
 68@external
 69def getHolding(_stockholder: address) -> uint256:
 70    return self._getHolding(_stockholder)
 71
 72# Return the amount the company has on hand in cash.
 73@view
 74@external
 75def cash() -> uint256:
 76    return self.balance
 77
 78# Give stock back to the company and get money back as ETH.
 79@external
 80def sellStock(sell_order: uint256):
 81    assert sell_order > 0 # Otherwise, this would fail at send() below,
 82        # due to an OOG error (there would be zero value available for gas).
 83    # You can only sell as much stock as you own.
 84    assert self._getHolding(msg.sender) >= sell_order
 85    # Check that the company can pay you.
 86    assert self.balance >= (sell_order * self.price)
 87
 88    # Sell the stock, send the proceeds to the user
 89    # and put the stock back on the market.
 90    self.holdings[msg.sender] -= sell_order
 91    self.holdings[self.company] += sell_order
 92    send(msg.sender, sell_order * self.price)
 93
 94    # Log the sell event.
 95    log Sell(msg.sender, sell_order)
 96
 97# Transfer stock from one stockholder to another. (Assume that the
 98# receiver is given some compensation, but this is not enforced.)
 99@external
100def transferStock(receiver: address, transfer_order: uint256):
101    assert transfer_order > 0 # This is similar to sellStock above.
102    # Similarly, you can only trade as much stock as you own.
103    assert self._getHolding(msg.sender) >= transfer_order
104
105    # Debit the sender's stock and add to the receiver's address.
106    self.holdings[msg.sender] -= transfer_order
107    self.holdings[receiver] += transfer_order
108
109    # Log the transfer event.
110    log Transfer(msg.sender, receiver, transfer_order)
111
112# Allow the company to pay someone for services rendered.
113@external
114def payBill(vendor: address, amount: uint256):
115    # Only the company can pay people.
116    assert msg.sender == self.company
117    # Also, it can pay only if there's enough to pay them with.
118    assert self.balance >= amount
119
120    # Pay the bill!
121    send(vendor, amount)
122
123    # Log the payment event.
124    log Pay(vendor, amount)
125
126# Public function to allow external access to _debt
127@view
128@external
129def debt() -> uint256:
130    return self._debt()
131
132# Return the cash holdings minus the debt of the company.
133# The share debt or liability only is included here,
134# but of course all other liabilities can be included.
135@view
136@external
137def worth() -> uint256:
138    return self.balance - self._debt()
139
140# Return the amount in wei that a company has raised in stock offerings.
141@view
142@internal
143def _debt() -> uint256:
144    return (self.totalShares - self._stockAvailable()) * self.price
145
146# Find out how much stock the company holds
147@view
148@internal
149def _stockAvailable() -> uint256:
150    return self.holdings[self.company]
151
152# Find out how much stock any address (that's owned by someone) has.
153@view
154@internal
155def _getHolding(_stockholder: address) -> uint256:
156    return self.holdings[_stockholder]

Note

Throughout this contract, we use a pattern where @external functions return data from @internal functions that have the same name prepended with an underscore. This is because Vyper does not allow calls between external functions within the same contract. The internal function handles the logic, while the external function acts as a getter to allow viewing.

The contract contains a number of methods that modify the contract state as well as a few ‘getter’ methods to read it. We first declare several events that the contract logs. We then declare our global variables, followed by function definitions.

 3event Transfer:
 4    sender: indexed(address)
 5    receiver: indexed(address)
 6    value: uint256
 7
 8event Buy:
 9    buyer: indexed(address)
10    buy_order: uint256
11
12event Sell:
13    seller: indexed(address)
14    sell_order: uint256
15
16event Pay:
17    vendor: indexed(address)
18    amount: uint256
19
20
21# Initiate the variables for the company and it's own shares.
22company: public(address)
23totalShares: public(uint256)
24price: public(uint256)
25
26# Store a ledger of stockholder holdings.
27holdings: HashMap[address, uint256]

We initiate the company variable to be of type address that’s public. The totalShares variable is of type uint256, which in this case represents the total available shares of the company. The price variable represents the wei value of a share and holdings is a mapping that maps an address to the number of shares the address owns.

29# Set up the company.
30@external
31def __init__(_company: address, _total_shares: uint256, initial_price: uint256):
32    assert _total_shares > 0
33    assert initial_price > 0
34
35    self.company = _company
36    self.totalShares = _total_shares
37    self.price = initial_price
38
39    # The company holds all the shares at first, but can sell them all.
40    self.holdings[self.company] = _total_shares

In the constructor, we set up the contract to check for valid inputs during the initialization of the contract via the two assert statements. If the inputs are valid, the contract variables are set accordingly and the company’s address is initialized to hold all shares of the company in the holdings mapping.

42# Public function to allow external access to _stockAvailable
43@view
44@external
45def stockAvailable() -> uint256:
46    return self._stockAvailable()

We will be seeing a few @view decorators in this contract—which is used to decorate methods that simply read the contract state or return a simple calculation on the contract state without modifying it. Remember, reading the blockchain is free, writing on it is not. Since Vyper is a statically typed language, we see an arrow following the definition of the _stockAvailable() method, which simply represents the data type which the function is expected to return. In the method, we simply key into self.holdings with the company’s address and check it’s holdings. Because _stockAvailable() is an internal method, we also include the stockAvailable() method to allow external access.

Now, lets take a look at a method that lets a person buy stock from the company’s holding.

51def buyStock():
52    # Note: full amount is given to company (no fractional shares),
53    #       so be sure to send exact amount to buy shares
54    buy_order: uint256 = msg.value / self.price # rounds down
55
56    # Check that there are enough shares to buy.
57    assert self._stockAvailable() >= buy_order
58
59    # Take the shares off the market and give them to the stockholder.
60    self.holdings[self.company] -= buy_order
61    self.holdings[msg.sender] += buy_order
62
63    # Log the buy event.
64    log Buy(msg.sender, buy_order)

The buyStock() method is a @payable method which takes an amount of ether sent and calculates the buyOrder (the stock value equivalence at the time of call). The number of shares is deducted from the company’s holdings and transferred to the sender’s in the holdings mapping.

Now that people can buy shares, how do we check someone’s holdings?

66# Public function to allow external access to _getHolding
67@view
68@external
69def getHolding(_stockholder: address) -> uint256:
70    return self._getHolding(_stockholder)
71

The _getHolding() is another @view method that takes an address and returns its corresponding stock holdings by keying into self.holdings. Again, an external function getHolding() is included to allow access.

72# Return the amount the company has on hand in cash.
73@view
74@external
75def cash() -> uint256:
76    return self.balance

To check the ether balance of the company, we can simply call the getter method cash().

78# Give stock back to the company and get money back as ETH.
79@external
80def sellStock(sell_order: uint256):
81    assert sell_order > 0 # Otherwise, this would fail at send() below,
82        # due to an OOG error (there would be zero value available for gas).
83    # You can only sell as much stock as you own.
84    assert self._getHolding(msg.sender) >= sell_order
85    # Check that the company can pay you.
86    assert self.balance >= (sell_order * self.price)
87
88    # Sell the stock, send the proceeds to the user
89    # and put the stock back on the market.
90    self.holdings[msg.sender] -= sell_order
91    self.holdings[self.company] += sell_order
92    send(msg.sender, sell_order * self.price)
93
94    # Log the sell event.
95    log Sell(msg.sender, sell_order)

To sell a stock, we have the sellStock() method which takes a number of stocks a person wishes to sell, and sends the equivalent value in ether to the seller’s address. We first assert that the number of stocks the person wishes to sell is a value greater than 0. We also assert to see that the user can only sell as much as the user owns and that the company has enough ether to complete the sale. If all conditions are met, the holdings are deducted from the seller and given to the company. The ethers are then sent to the seller.

 97# Transfer stock from one stockholder to another. (Assume that the
 98# receiver is given some compensation, but this is not enforced.)
 99@external
100def transferStock(receiver: address, transfer_order: uint256):
101    assert transfer_order > 0 # This is similar to sellStock above.
102    # Similarly, you can only trade as much stock as you own.
103    assert self._getHolding(msg.sender) >= transfer_order
104
105    # Debit the sender's stock and add to the receiver's address.
106    self.holdings[msg.sender] -= transfer_order
107    self.holdings[receiver] += transfer_order
108
109    # Log the transfer event.
110    log Transfer(msg.sender, receiver, transfer_order)

A stockholder can also transfer their stock to another stockholder with the transferStock() method. The method takes a receiver address and the number of shares to send. It first asserts that the amount being sent is greater than 0 and asserts whether the sender has enough stocks to send. If both conditions are satisfied, the transfer is made.

112# Allow the company to pay someone for services rendered.
113@external
114def payBill(vendor: address, amount: uint256):
115    # Only the company can pay people.
116    assert msg.sender == self.company
117    # Also, it can pay only if there's enough to pay them with.
118    assert self.balance >= amount
119
120    # Pay the bill!
121    send(vendor, amount)
122
123    # Log the payment event.
124    log Pay(vendor, amount)

The company is also allowed to pay out an amount in ether to an address by calling the payBill() method. This method should only be callable by the company and thus first checks whether the method caller’s address matches that of the company. Another important condition to check is that the company has enough funds to pay the amount. If both conditions satisfy, the contract sends its ether to an address.

126# Public function to allow external access to _debt
127@view
128@external
129def debt() -> uint256:
130    return self._debt()

We can also check how much the company has raised by multiplying the number of shares the company has sold and the price of each share. Internally, we get this value by calling the _debt() method. Externally it is accessed via debt().

132# Return the cash holdings minus the debt of the company.
133# The share debt or liability only is included here,
134# but of course all other liabilities can be included.
135@view
136@external
137def worth() -> uint256:
138    return self.balance - self._debt()

Finally, in this worth() method, we can check the worth of a company by subtracting its debt from its ether balance.

This contract has been the most thorough example so far in terms of its functionality and features. Yet despite the thoroughness of such a contract, the logic remained simple. Hopefully, by now, the Vyper language has convinced you of its capabilities and readability in writing smart contracts.