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 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 | # Open Auction
# Auction params
# Beneficiary receives money from the highest bidder
beneficiary: public(address)
auctionStart: public(timestamp)
auctionEnd: public(timestamp)
# Current state of auction
highestBidder: public(address)
highestBid: public(wei_value)
# Set to true at the end, disallows any change
ended: public(bool)
# Create a simple auction with `_bidding_time`
# seconds bidding time on behalf of the
# beneficiary address `_beneficiary`.
@public
def __init__(_beneficiary: address, _bidding_time: timedelta):
self.beneficiary = _beneficiary
self.auctionStart = block.timestamp
self.auctionEnd = self.auctionStart + _bidding_time
# Bid on the auction with the value sent
# together with this transaction.
# The value will only be refunded if the
# auction is not won.
@public
@payable
def bid():
# Check if bidding period is over.
assert block.timestamp < self.auctionEnd
# Check if bid is high enough
assert msg.value > self.highestBid
if not self.highestBid == 0:
# Sends money back to the previous highest bidder
send(self.highestBidder, self.highestBid)
self.highestBidder = msg.sender
self.highestBid = msg.value
# End the auction and send the highest bid
# to the beneficiary.
@public
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!
# Auction params
# Beneficiary receives money from the highest bidder
beneficiary: public(address)
auctionStart: public(timestamp)
auctionEnd: public(timestamp)
# Current state of auction
highestBidder: public(address)
highestBid: public(wei_value)
# Set to true at the end, disallows any change
ended: public(bool)
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 timestamp
to manage the open auction
period and highestBid
with datatype wei_value
, the smallest
denomination of ether, to manage auction state. The variable ended
is a
boolean to determine whether the auction is officially over.
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.
The contract is initialized with two arguments: _beneficiary
of type
address
and _bidding_time
with type timedelta
, the time difference
between the start and end of the auction. We then store these two pieces of
information into the contract variables self.beneficiary
and
self.auctionEnd
. 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.
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
.
Warning
msg.sender
will change between internal function calls so that
if you’re calling a function from the outside, it’s correct for the first
function call. But then, for the function calls after, msg.sender
will
reference the contract itself as opposed to the sender of the transaction.
Here, we first check whether the current time is before the auction’s end time
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 two 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
.
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 to 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.
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 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 | #Safe Remote Purchase
#Originally from
#https://github.com/ethereum/solidity/blob/develop/docs/solidity-by-example.rst
#ported to vyper and optimized
#Rundown of the transaction:
#1. Seller posts item for sale and posts safety deposit of double the item value.
# Balance is 2*value.
#(1.1. Seller can reclaim deposit and close the sale as long as nothing was purchased.)
#2. Buyer purchases item (value) plus posts an additional safety deposit (Item value).
# Balance is 4*value.
#3. Seller ships item.
#4. Buyer confirms receiving the item. Buyer's deposit (value) is returned.
#Seller's deposit (2*value) + items value is returned. Balance is 0.
value: public(wei_value) #Value of the item
seller: public(address)
buyer: public(address)
unlocked: public(bool)
#@constant
#def unlocked() -> bool: #Is a refund possible for the seller?
# return (self.balance == self.value*2)
@public
@payable
def __init__():
assert (msg.value % 2) == 0
self.value = msg.value / 2 #The seller initializes the contract by
#posting a safety deposit of 2*value of the item up for sale.
self.seller = msg.sender
self.unlocked = True
@public
def abort():
assert self.unlocked #Is the contract still refundable?
assert msg.sender == self.seller #Only the seller can refund
# his deposit before any buyer purchases the item.
selfdestruct(self.seller) #Refunds the seller and deletes the contract.
@public
@payable
def purchase():
assert self.unlocked #Is the contract still open (is the item still up for sale)?
assert msg.value == (2 * self.value) #Is the deposit the correct value?
self.buyer = msg.sender
self.unlocked = False
@public
def received():
assert not self.unlocked #Is the item already purchased and pending confirmation
# from the buyer?
assert msg.sender == self.buyer
send(self.buyer, self.value) #Return the buyer's deposit (=value) to the buyer.
selfdestruct(self.seller) #Return the seller's deposit (=2*value)
# and the 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.
value: public(wei_value) #Value of the item
seller: public(address)
buyer: public(address)
unlocked: public(bool)
Like the other contracts, we begin by declaring our global variables public with
their respective datatypes. Remember that the public
function allows the
variables to be readable by an external caller, but not writeable.
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
.
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.
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.
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 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 | # Setup private variables (only callable from within the contract)
funders: {sender: address, value: wei_value}[int128]
nextFunderIndex: int128
beneficiary: address
deadline: timestamp
goal: wei_value
refundIndex: int128
timelimit: timedelta
# Setup global variables
@public
def __init__(_beneficiary: address, _goal: wei_value, _timelimit: timedelta):
self.beneficiary = _beneficiary
self.deadline = block.timestamp + _timelimit
self.timelimit = _timelimit
self.goal = _goal
# Participate in this crowdfunding campaign
@public
@payable
def participate():
assert block.timestamp < self.deadline
nfi: int128 = self.nextFunderIndex
self.funders[nfi] = {sender: msg.sender, value: msg.value}
self.nextFunderIndex = nfi + 1
# Enough money was raised! Send funds to the beneficiary
@public
def finalize():
assert block.timestamp >= self.deadline and self.balance >= self.goal
selfdestruct(self.beneficiary)
# Not enough money was raised! Refund everyone (max 30 people at a time
# to avoid gas limit issues)
@public
def refund():
assert block.timestamp >= self.deadline and self.balance < self.goal
ind: int128 = self.refundIndex
for i in range(ind, ind + 30):
if i >= self.nextFunderIndex:
self.refundIndex = self.nextFunderIndex
return
send(self.funders[i].sender, self.funders[i].value)
self.funders[i] = None
self.refundIndex = ind + 30
|
Most of this code should be relatively straightforward after going through our previous examples. Let’s dive right in.
# Setup private variables (only callable from within the contract)
funders: {sender: address, value: wei_value}[int128]
nextFunderIndex: int128
beneficiary: address
deadline: timestamp
goal: wei_value
refundIndex: int128
timelimit: timedelta
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 a number,
and the value is a struct representing the contribution of each participant.
This struct contains each participant’s public address and their respective
value contributed to the fund. The key corresponding to each struct in the
mapping will be represented by the variable nextFunderIndex
which is
incremented with each additional contributing participant. Variables initialized
with the int128
type without an explicit value, such as nextFunderIndex
,
defaults to 0
. 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. refundIndex
is a variable for bookkeeping purposes in
order to avoid gas limit issues in the scenario of a refund.
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.
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.
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 thats 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.
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 then loop through
every participant and call send()
to send each participant their respective
contribution. For the sake of gas limits, we group the number of contributors
in batches of 30 and refund them one at a time. Unfortunately, if there’s a
large number of of participants, multiple calls to refund()
may be
necessary.
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 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | # Voting with delegation.
# Information about voters
voters: public({
# weight is accumulated by delegation
weight: int128,
# if true, that person already voted (which includes voting by delegating)
voted: bool,
# person delegated to
delegate: address,
# index of the voted proposal, which is not meaningful unless `voted` is True.
vote: int128
}[address])
# This is a type for a list of proposals.
proposals: public({
# short name (up to 32 bytes)
name: bytes32,
# number of accumulated votes
voteCount: int128
}[int128])
voterCount: public(int128)
chairperson: public(address)
int128Proposals: public(int128)
@public
@constant
def delegated(addr: address) -> bool:
return self.voters[addr].delegate != ZERO_ADDRESS
@public
@constant
def directlyVoted(addr: address) -> bool:
return self.voters[addr].voted and (self.voters[addr].delegate == ZERO_ADDRESS)
# Setup global variables
@public
def __init__(_proposalNames: bytes32[2]):
self.chairperson = msg.sender
self.voterCount = 0
for i in range(2):
self.proposals[i] = {
name: _proposalNames[i],
voteCount: 0
}
self.int128Proposals += 1
# Give a `voter` the right to vote on this ballot.
# This may only be called by the `chairperson`.
@public
def giveRightToVote(voter: address):
# Throws if the sender is not the chairperson.
assert msg.sender == self.chairperson
# Throws if the voter has already voted.
assert not self.voters[voter].voted
# Throws if the voter's voting weight isn't 0.
assert self.voters[voter].weight == 0
self.voters[voter].weight = 1
self.voterCount += 1
# Used by `delegate` below, and can be called by anyone.
@public
def forwardWeight(delegate_with_weight_to_forward: address):
assert self.delegated(delegate_with_weight_to_forward)
# Throw if there is nothing to do:
assert self.voters[delegate_with_weight_to_forward].weight > 0
target: address = self.voters[delegate_with_weight_to_forward].delegate
for i in range(4):
if self.delegated(target):
target = self.voters[target].delegate
# The following effectively detects cycles of length <= 5,
# in which the delegation is given back to the delegator.
# This could be done for any int128ber of loops,
# or even infinitely with a while loop.
# However, cycles aren't actually problematic for correctness;
# they just result in spoiled votes.
# So, in the production version, this should instead be
# the responsibility of the contract's client, and this
# check should be removed.
assert target != delegate_with_weight_to_forward
else:
# Weight will be moved to someone who directly voted or
# hasn't voted.
break
weight_to_forward: int128 = self.voters[delegate_with_weight_to_forward].weight
self.voters[delegate_with_weight_to_forward].weight = 0
self.voters[target].weight += weight_to_forward
if self.directlyVoted(target):
self.proposals[self.voters[target].vote].voteCount += weight_to_forward
self.voters[target].weight = 0
# To reiterate: if target is also a delegate, this function will need
# to be called again, similarly to as above.
# Delegate your vote to the voter `to`.
@public
def delegate(to: address):
# Throws if the sender has already voted
assert not self.voters[msg.sender].voted
# Throws if the sender tries to delegate their vote to themselves or to
# the default address value of 0x0000000000000000000000000000000000000000
# (the latter might not be problematic, but I don't want to think about it).
assert to != msg.sender
assert to != ZERO_ADDRESS
self.voters[msg.sender].voted = True
self.voters[msg.sender].delegate = to
# This call will throw if and only if this delegation would cause a loop
# of length <= 5 that ends up delegating back to the delegator.
self.forwardWeight(msg.sender)
# Give your vote (including votes delegated to you)
# to proposal `proposals[proposal].name`.
@public
def vote(proposal: int128):
# can't vote twice
assert not self.voters[msg.sender].voted
# can only vote on legitimate proposals
assert proposal < self.int128Proposals
self.voters[msg.sender].vote = proposal
self.voters[msg.sender].voted = True
# transfer msg.sender's weight to proposal
self.proposals[proposal].voteCount += self.voters[msg.sender].weight
self.voters[msg.sender].weight = 0
# Computes the winning proposal taking all
# previous votes into account.
@public
@constant
def winningProposal() -> int128:
winning_vote_count: int128 = 0
winning_proposal: int128 = 0
for i in range(2):
if self.proposals[i].voteCount > winning_vote_count:
winning_vote_count = self.proposals[i].voteCount
winning_proposal = i
return winning_proposal
# Calls winningProposal() function to get the index
# of the winner contained in the proposals array and then
# returns the name of the winner
@public
@constant
def winnerName() -> bytes32:
return self.proposals[self.winningProposal()].name
|
As we can see, this is contract of moderate length which we will dissect section by section. Let’s begin!
# Information about voters
voters: public({
# weight is accumulated by delegation
weight: int128,
# if true, that person already voted (which includes voting by delegating)
voted: bool,
# person delegated to
delegate: address,
# index of the voted proposal, which is not meaningful unless `voted` is True.
vote: int128
}[address])
# This is a type for a list of proposals.
proposals: public({
# short name (up to 32 bytes)
name: bytes32,
# number of accumulated votes
voteCount: int128
}[int128])
voterCount: public(int128)
chairperson: public(address)
int128Proposals: 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 datatypes.
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.
Warning
Both msg.sender
and msg.balance
change between internal
function calls so that if you’re calling a function from the outside, it’s
correct for the first function call. But then, for the function calls after,
msg.sender
and msg.balance
reference the contract itself as opposed
to the sender of the transaction.
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.
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
.
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.
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 @constant
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 @constant
decorator, we let the EVM know that this
is a read-only function and we benefit by saving gas fees.
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.
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 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | units: {
currency_value: "Currency Value"
}
# Financial events the contract logs
Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256(currency_value)})
Buy: event({_buyer: indexed(address), _buy_order: uint256(currency_value)})
Sell: event({_seller: indexed(address), _sell_order: uint256(currency_value)})
Pay: event({_vendor: indexed(address), _amount: wei_value})
# Initiate the variables for the company and it's own shares.
company: public(address)
totalShares: public(uint256(currency_value))
price: public(uint256 (wei / currency_value))
# Store a ledger of stockholder holdings.
holdings: uint256(currency_value)[address]
# Set up the company.
@public
def __init__(_company: address, _total_shares: uint256(currency_value),
initial_price: uint256(wei / currency_value) ):
assert _total_shares > 0
assert initial_price > 0
self.company = _company
self.totalShares = _total_shares
self.price = initial_price
# The company holds all the shares at first, but can sell them all.
self.holdings[self.company] = _total_shares
@public
@constant
def stockAvailable() -> uint256(currency_value):
return self.holdings[self.company]
# Give some value to the company and get stock in return.
@public
@payable
def buyStock():
# Note: full amount is given to company (no fractional shares),
# so be sure to send exact amount to buy shares
buy_order: uint256(currency_value) = msg.value / self.price # rounds down
# Check that there are enough shares to buy.
assert self.stockAvailable() >= buy_order
# Take the shares off the market and give them to the stockholder.
self.holdings[self.company] -= buy_order
self.holdings[msg.sender] += buy_order
# Log the buy event.
log.Buy(msg.sender, buy_order)
# Find out how much stock any address (that's owned by someone) has.
@public
@constant
def getHolding(_stockholder: address) -> uint256(currency_value):
return self.holdings[_stockholder]
# Return the amount the company has on hand in cash.
@public
@constant
def cash() -> wei_value:
return self.balance
# Give stock back to the company and get money back as ETH.
@public
def sellStock(sell_order: uint256(currency_value)):
assert sell_order > 0 # Otherwise, this would fail at send() below,
# due to an OOG error (there would be zero value available for gas).
# You can only sell as much stock as you own.
assert self.getHolding(msg.sender) >= sell_order
# Check that the company can pay you.
assert self.cash() >= (sell_order * self.price)
# Sell the stock, send the proceeds to the user
# and put the stock back on the market.
self.holdings[msg.sender] -= sell_order
self.holdings[self.company] += sell_order
send(msg.sender, sell_order * self.price)
# Log the sell event.
log.Sell(msg.sender, sell_order)
# Transfer stock from one stockholder to another. (Assume that the
# receiver is given some compensation, but this is not enforced.)
@public
def transferStock(receiver: address, transfer_order: uint256(currency_value)):
assert transfer_order > 0 # This is similar to sellStock above.
# Similarly, you can only trade as much stock as you own.
assert self.getHolding(msg.sender) >= transfer_order
# Debit the sender's stock and add to the receiver's address.
self.holdings[msg.sender] -= transfer_order
self.holdings[receiver] += transfer_order
# Log the transfer event.
log.Transfer(msg.sender, receiver, transfer_order)
# Allow the company to pay someone for services rendered.
@public
def payBill(vendor: address, amount: wei_value):
# Only the company can pay people.
assert msg.sender == self.company
# Also, it can pay only if there's enough to pay them with.
assert self.cash() >= amount
# Pay the bill!
send(vendor, amount)
# Log the payment event.
log.Pay(vendor, amount)
# Return the amount in wei that a company has raised in stock offerings.
@public
@constant
def debt() -> wei_value:
return (self.totalShares - self.holdings[self.company]) * self.price
# Return the cash holdings minus the debt of the company.
# The share debt or liability only is included here,
# but of course all other liabilities can be included.
@public
@constant
def worth() -> wei_value:
return self.cash() - self.debt()
|
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.
Buy: event({_buyer: indexed(address), _buy_order: uint256(currency_value)})
Sell: event({_seller: indexed(address), _sell_order: uint256(currency_value)})
Pay: event({_vendor: indexed(address), _amount: wei_value})
# Initiate the variables for the company and it's own shares.
company: public(address)
totalShares: public(uint256(currency_value))
We initiate the company
variable to be of type address
that’s public.
The totalShares
variable is of type currency_value
, 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.
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.
We will be seeing a few @constant
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.
Now, lets take a look at a method that lets a person buy stock from the company’s holding.
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?
The getHoldings()
is another @constant
method that takes an address
and returns its corresponding stock holdings by keying into self.holdings
.
To check the ether balance of the company, we can simply call the getter method
cash()
.
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.
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.
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.
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. We can get this value
by calling the debt()
method.
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.