Auction contract
This tutorial aims to implement a simple auction contract in Fe. Along the way you will learn some foundational Fe concepts.
An open auction is one where prices are determined in real-time by live bidding. The winner is the participant who has made the highest bid at the time the auction ends.
The auction rules
To run an open auction, you need an item for sale, a seller, a pool of buyers and a deadline after which no more bids will be recognized. In this tutorial we will not have an item per se, the buyers are simply bidding to win! The highest bidder is provably crowned the winner, and the value of their bid is passed to the beneficiary. Bidders can also withdraw their bids at any time.
Get Started
To follow this guide you should have Fe installed on your computer. If you haven't installed Fe yet, follow the instructions on the Installation page.
With Fe installed, you can create a project folder, auction
that will act as your project root. In that folder, create an empty file called auction.fe
.
Now you are ready to start coding in Fe!
You will also need Foundry installed to follow the deployment instructions in this guide - you can use your alternative tooling for this if you prefer.
Writing the Contract
You can see the entire contract here. You can refer back to this at any time to check implementation details.
Defining the Contract
and initializing variables
A contract is Fe is defined using the contract
keyword. A contract requires a constructor function to initialize any state variables used by the contract. If no constructor is defined, Fe will add a default with no state variables. The skeleton of the contract can look as follows:
#![allow(unused)] fn main() { contract Auction { pub fn __init__() {} } }
To run the auction you will need several state variables, some of which can be initialized at the time the contract is instantiated. You will need to track the address of the beneficiary so you know who to pay out to. You will also need to keep track of the highest bidder, and the amount they have bid. You will also need to keep track of how much each specific address has sent into the contract, so you can refund them the right amount if they decide to withdraw. You will also need a flag that tracks whether or not the auction has ended. The following list of variables will suffice:
auction_end_time: u256
beneficiary: address
highest_bidder: address
highest_bid: u256
pending_returns: Map<address, u256>
ended: bool
Notice that variables are named using snake case (lower case, underscore separated, like_this
).
Addresses have their own type in Fe - it represents 20 hex-encoded bytes as per the Ethereum specification.
The variables that expect numbers are given the u256
type. This is an unsigned integer of length 256 bits. There are other choices for integers too, with both signed and unsigned integers between 8 and 256 bits in length.
The ended
variable will be used to check whether the auction is live or not. If it has finished ended
will be set to true
. There are only two possible states for this, so it makes sense to declare it as a bool
- i.e. true/false.
The pending_returns
variable is a mapping between N keys and N values, with user addresses as the keys and their bids as values. For this, a Map
type is used. In Fe, you define the types for the key and value in the Map definition - in this case, it is Map<address, u256>
. Keys can be any numeric
type, address
, boolean
or unit
.
Now you should decide which of these variables will have values that are known at the time the contract is instantiated. It makes sense to set the beneficiary
right away, so you can add that to the constructor arguments.
The other thing to consider here is how the contract will keep track of time. On its own, the contract has no concept of time. However, the contract does have access to the current block timestamp which is measured in seconds since the Unix epoch (Jan 1st 1970). This can be used to measure the time elapsed in a smart contract. In this contract, you can use this concept to set a deadline on the auction. By passing a length of time in seconds to the constructor, you can then add that value to the current block timestamp and create a deadline for bidding to end. Therefore, you should add a bidding_time
argument to the constructor. Its type can be u256
.
When you have implemented all this, your contract should look like this:
contract Auction {
// states
auction_end_time: u256
beneficiary: address
highest_bidder: address
highest_bid: u256
pending_returns: Map<address, u256>
ended: bool
// constructor
pub fn __init__(mut self, ctx: Context, bidding_time: u256, beneficiary_addr: address) {
self.beneficiary = beneficiary_addr
self.auction_end_time = ctx.block_timestamp() + bidding_time
}
}
Notice that the constructor receives values for bidding_time
and beneficiary_addr
and uses them to initialize the contract's auction_end_time
and beneficiary
variables.
The other thing to notice about the constructor is that there are two additional arguments passed to the constructor: mut self
and ctx: Context
.
self
self
is used to represent the specific instance of a Contract. It is used to access variables that are owned by that specific instance. This works the same way for Fe contracts as for, e.g. 'self' in the context of classes in Python, or this
in Javascript.
Here, you are not only using self
but you are prepending it with mut
. mut
is a keyword inherited from Rust that indicates that the value can be overwritten - i.e. it is "mutable". Variables are not mutable by default - this is a safety feature that helps protect developers from unintended changes during runtime. If you do not make self
mutable, then you will not be able to update the values it contains.
Context
Context is used to gate access to certain features including emitting logs, creating contracts, reading messages and transferring ETH. It is conventional to name the context object ctx
. The Context
object needs to be passed as the first parameter to a function unless the function also takes self
, in which case the Context
object should be passed as the second parameter. Context
must be expicitly made mutable if it will invoke functions that changes the blockchain data, whereas an immutable reference to Context
can be used where read-only access to the blockchain is needed.
Read more on Context in Fe
In Fe contracts ctx
is where you can find transaction data such as msg.sender
, msg.value
, block.timestamp
etc.
Bidding
Now that you have your contract constructor and state variables, you can implement some logic for receiving bids. To do this, you will create a method called bid
. To handle a bid, you will first need to determine whether the auction is still open. If it has closed then the bid should revert. If the auction is open you need to record the address of the bidder and the amount and determine whether their bid was the highest. If their bid is highest, then their address should be assigned to the highest_bidder
variable and the amount they sent recorded in the highest_bid
variable.
This logic can be implemented as follows:
#![allow(unused)] fn main() { pub fn bid(mut self, mut ctx: Context) { if ctx.block_timestamp() > self.auction_end_time { revert AuctionAlreadyEnded() } if ctx.msg_value() <= self.highest_bid { revert BidNotHighEnough(highest_bid: self.highest_bid) } if self.highest_bid != 0 { self.pending_returns[self.highest_bidder] += self.highest_bid } self.highest_bidder = ctx.msg_sender() self.highest_bid = ctx.msg_value() ctx.emit(HighestBidIncreased(bidder: ctx.msg_sender(), amount: ctx.msg_value())) } }
The method first checks that the current block timestamp is not later than the contract's aution_end_time
variable. If it is later, then the contract reverts. This is triggered using the revert
keyword. The revert
can accept a struct that becomes encoded as revert data. Here you can just revert without any arguments. Add the following definition somewhere in Auction.fe
outside the main contract definition:
struct AuctionAlreadyEnded {
}
The next check is whether the incoming bid exceeds the current highest bid. If not, the bid has failed and it may as well revert. We can repeat the same logic as for AuctionAlreadyEnded
. We can also report the current highest bid in the revert message to help the user reprice if they want to. Add the following to auction.fe
:
struct BidNotHighEnough {
pub highest_bid: u256
}
Notice that the value being checked is
msg.value
which is included in thectx
object.ctx
is where you can access incoming transaction data.
Next, if the incoming transaction is the highest bid, you need to track how much the sender should receive as a payout if their bid ends up being exceeded by another user (i.e. if they get outbid, they get their ETH back). To do this, you add a key-value pair to the pending_returns
mapping, with the user address as the key and the transaction amount as the value. Both of these come from ctx
in the form of msg.sender
and msg.value
.
Finally, if the incoming bid is the highest, you can emit an event. Events are useful because they provide a cheap way to return data from a contract as they use logs instead of contract storage. Unlike other smart contract languages, there is no emit
keyword or Event
type. Instead, you trigger an event by calling the emit
method on the ctx
object. You can pass this method a struct that defines the emitted message. You can add the following struct for this event:
struct HighestBidIncreased {
#indexed
pub bidder: address
pub amount: u256
}
You have now implemented all the logic to handle a bid!
Withdrawing
A previous high-bidder will want to retrieve their ETH from the contract so they can either walk away or bid again. You therefore need to create a withdraw
method that the user can call. The function will lookup the user address in pending_returns
. If there is a non-zero value associated with the user's address, the contract should send that amount back to the sender's address. It is important to first update the value in pending_returns
and then send the ETH to the user, otherwise you are exposing a re-entrancy vulnerability (where a user can repeatedly call the contract and receive the ETH multiple times).
Add the following to the contract to implement the withdraw
method:
#![allow(unused)] fn main() { pub fn withdraw(mut self, mut ctx: Context) -> bool { let amount: u256 = self.pending_returns[ctx.msg_sender()] if amount > 0 { self.pending_returns[ctx.msg_sender()] = 0 ctx.send_value(to: ctx.msg_sender(), wei: amount) } return true } }
Note that in this case
mut
is used withctx
becausesend_value
is making changes to the blockchain (it is moving ETH from one address to another).
End the auction
Finally, you need to add a way to end the auction. This will check whether the bidding period is over, and if it is, automatically trigger the payment to the beneficiary and emit the address of the winner in an event.
First, check the auction is not still live - if the auction is live you cannot end it early. If an attempt to end the auction early is made, it should revert using a AuctionNotYetEnded
struct, which can look as follows:
struct AuctionNotYetEnded {
}
You should also check whether the auction was already ended by a previous valid call to this method. In this case, revert with a AuctionEndAlreadyCalled
struct:
struct AuctionEndAlreadyCalled {}
If the auction is still live, you can end it. First set self.ended
to true
to update the contract state. Then emit the event using ctx.emit()
. Then, send the ETH to the beneficiary. Again, the order is important - you should always send value last to protect against re-entrancy.
Your method can look as follows:
#![allow(unused)] fn main() { pub fn action_end(mut self, mut ctx: Context) { if ctx.block_timestamp() <= self.auction_end_time { revert AuctionNotYetEnded() } if self.ended { revert AuctionEndAlreadyCalled() } self.ended = true ctx.emit(AuctionEnded(winner: self.highest_bidder, amount: self.highest_bid)) ctx.send_value(to: self.beneficiary, wei: self.highest_bid) } }
Congratulations! You just wrote an open auction contract in Fe!
View functions
To help test the contract without having to decode transaction logs, you can add some simple functions to the contract that simply report the current values for some key state variables (specifically, highest_bidder
, highest_bid
and ended
). This will allow a user to use eth_call
to query these values in the contract. eth_call
is used for functions that do not update the state of the blockchain and costs no gas because the queries can be performed on local data.
You can add the following functions to the contract:
#![allow(unused)] fn main() { pub fn check_highest_bidder(self) -> address { return self.highest_bidder; } pub fn check_highest_bid(self) -> u256 { return self.highest_bid; } pub fn check_ended(self) -> bool { return self.ended; } }
Build and deploy the contract
Your contract is now ready to use! Compile it using
fe build auction.fe
You will find the contract ABI and bytecode in the newly created outputs
directory.
Start a local blockchain to deploy your contract to:
anvil
There are constructor arguments (bidding_time: u256
, beneficiary_addr: address
) that have to be added to the contract bytecode so that the contract is instantiated with your desired values. To add constructor arguments you can encode them into bytecode and append them to the contract bytecode.
First, hex encode the value you want to pass to bidding_time
. In this case, we will use a value of 10:
cast --to_hex(10)
>> 0xa // this is 10 in hex
Ethereum addresses are already hex, so there is no further encoding required. The following command will take the constructor function and the hex-encoded arguments and concatenate them into a contiguous hex string and then deploy the contract with the constructor arguments.
cast send --from <your-address> --private-key <your-private-key> --create $(cat output/Auction/Auction.bin) $(cast abi-encode "__init__(uint256,address)" 0xa 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720)
You will see the contract address reported in your terminal.
Now you can interact with your contract. Start by sending an initial bid, let's say 100 ETH. For contract address 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35
:
cast send 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "bid()" --value "100ether" --private-key <your-private-key> --from 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
You can check whether this was successful by calling the check_highest_bidder()
function:
cast call 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "check_highest_bidder()"
You will see a response looking similar to:
0x000000000000000000000000a0Ee7A142d267C1f36714E4a8F75612F20a79720
The characters after the leading zeros are the address for the highest bidder (notice they match the characters after the 0x in the bidding address).
You can do the same to check the highest bid:
cast call 0x700b6A60ce7EaaEA56F065753d8dcB9653dbAD35 "check_highest_bid()"
This returns:
0x0000000000000000000000000000000000000000000000056bc75e2d63100000
Converting the non-zero characters to binary gives the decimal value of your bid (in wei - divide by 1e18 to get the value in ETH):
cast --to-dec 56bc75e2d63100000
>> 100000000000000000000 // 100 ETH in wei
Now you can repeat this process, outbidding the initial bid from another address and check the highest_bidder()
and highest_bid()
to confirm. Do this a few times, then call end_auction()
to see the value of the highest bid get transferred to the beneficiary_addr
. You can always check the balance of each address using:
cast balance <address>
And check whether the auction open time has expired using
cast <contract-address> "check_ended()"
Summary
Congratulations! You wrote an open auction contract in Fe and deployed it to a local blockchain!
If you are using a local Anvil blockchain, you can use the ten ephemeral addresses created when the network started to simulate a bidding war!
By following this tutorial, you learned:
- basic Fe types, such as
bool
,address
,map
andu256
- basic Fe styles, such as snake case for variable names
- how to create a
contract
with a constructor - how to
revert
- how to handle state variables
- how to avoid reentrancy
- how to use
ctx
to handle transaction data - how to emit events using
ctx.emit
- how to deploy a contract with constructor arguments using Foundry
- how to interact with your contract