At Three Sigma, we recognize the intricate opportunities within the Web3 space. Based in Lisbon, our team of experts offers premier services in development, security, and economic modeling to drive your project's success. Whether you need precise code audits, advanced economic modeling, or comprehensive blockchain engineering, Three Sigma is your trusted partner.
In this example application, I will show you how to create an English Auction contract using Sway and implement some tests on Harness.
Most of you may already know what an English Auction is, but for those who don’t, here is a basic explanation:
An English Auction is a type of auction where a seller offers an asset with an initial price and a reserve price. Users then bid on the asset until the bidding period ends or the reserve price is met. Upon completion, users will withdraw either their original deposits or their newly purchased assets, depending on the outcome.
The English Auction application implements this idea in a decentralized manner, eliminating the need for a third party and providing strong settlement assurances.
For the lazy persons, here is the GitHub repo with the full code. However, I know you are eager to learn and disciplined, so let’s dive into the step by step example application.
To install the Fuel toolchain, you can use the fuelup-init script. This will install forc, forc-client, forc-fmt, forc-lsp, forc-wallet as well as fuel-core in ~/.fuelup/bin.
Run the following script in your terminal.
Keep in mind that currently Windows is not supported natively. If you wish to use fuelup on Windows, please use Windows Subsystem for Linux.
If you don’t have Rust installed on your machine, run the following command in your shell; this downloads and runs rustup-init.sh, which in turn downloads and runs the correct version of the rustup-init executable for your platform.
Before starting writing the contract have a look at its structure to understand it better.
Let’s start by creating a new, empty folder named english-auction, navigate into it, and create a contract project using forc:
Open your project in a code editor and delete everything in src/main.sw apart from the first line.
Every Sway file must start with a declaration of what type of program the file contains; here, we've declared that this file is a contract. You can learn more about Sway program types in the Sway Book.
Modules and imports
Next, we'll declare the modules and imports we will use.
errors: Contains custom error types that the contract will use.
data_structures: Contains the data structures used in the contract, such as Auction and State.
events: Defines the events that the contract will emit, such as BidEvent, CancelAuctionEvent, CreateAuctionEvent, and WithdrawEvent.
interface: Defines the interface (or ABI) for the contract, such as EnglishAuction and Info.
These lines import the necessary items from the modules and the standard library (std):
Auction and State from the data_structures module.
Various error types (AccessError, InitError, InputError, UserError) from the errors module.
Event types (BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent) from the events module.
Interface traits (EnglishAuction, Info) from the interface module.
Standard library utilities such as transfer (for asset transfers), height (to get the current block height), msg_asset_id (to get the ID of the asset being transferred), msg_amount (to get the amount of the asset being transferred), and Hash.
Storage Declaration
This block defines the storage layout for the contract:
auctions: A StorageMap that maps an auction ID (u64) to an Auction object. This is where all the auction data is stored.
total_auctions: A u64 that keeps track of the total number of auctions that have been created. This is incremented every time a new auction is created.
Constants for Extension Mechanism
These constants define the behavior of the auction extension mechanism:
EXTENSION_THRESHOLD: The number of blocks before the end of the auction within which a new bid will trigger an extension. For example, if this is set to 5, then any bid placed within the last 5 blocks of the auction will extend the auction.
EXTENSION_DURATION: The number of blocks by which the auction is extended if a bid is placed within the extension threshold. For example, if this is set to 5, then the auction is extended by 5 blocks each time a qualifying bid is placed.
After setting up the structure and imports for the contract. Defined modules, imported necessary types and functions, and set up the storage and constants for the auction mechanism our contract should look like this:
ABI
ABI stands for Application Binary Interface. An ABI defines an interface for a contract. A contract must either define or import an ABI declaration.
It is considered best practice to define your ABI in a separate library and import it into your contract. This allows callers of the contract to import and use the ABI more easily.
To follow the best practices let’s create a interface.sw file in the same src folder and define the ABI:
Paste this into the newly created interface.sw file:
Before jumping into the main contract implementing the ABI let’s first create the Data Structures, Errors and Events:
Data Structures
In the src folder, create a data_structures folder.
Create two files: an auction.sw file where we will implement the Auction custom library logic, and a state.sw file where the State custom library logic will be implemented.
Auction Library
Let's start by declaring that this file is a library, meaning it contains reusable code that can be included in other parts of the program. Then, we import the State data structure from the data_structures module, which will be used to represent the state of the auction (open or closed).
Now we will declare the Auction struct, which defines the properties of the auction.
bid_asset: The asset that will be accepted as payment in the auction.
end_block: The block number when the auction will end.
highest_bid: The current highest bid amount.
highest_bidder: The identity of the current highest bidder. This is optional because there might not be a highest bidder initially.
initial_price: The starting price for the auction.
reserve_price: The reserve price, which is the price at which the asset can be bought outright. This is optional.
sell_asset: The asset that is being sold in the auction.
sell_asset_amount: The amount of the asset being auctioned.
seller: The identity of the seller.
state: The state of the auction, indicating if it is open or closed.
deposits: A map to store the deposits made by users. It maps user identities to the amount they have deposited.
And the new function, which creates a new auction with the specified details:
The auction.sw library should look like this:
Continuing from where we left off, let's create the State library which will define the possible states of an auction and provide functionality to compare these states.
Let's start by declaring that this file is a library again. Next, we define the State enum, which represents the state of an auction. An enum is a type that can represent multiple named values, called variants.
Closed: Represents the state when the auction is no longer accepting bids.
Open: Represents the state where bids can be placed on an auction.
To enable comparison between different states, we implement the Eq trait for the State enum. The Eq trait allows us to check if two states are equal.
The Eq trait is used to define equality comparisons. In this implementation, we check if two states are the same.
If both states are Open, they are considered equal.
If both states are Closed, they are considered equal.
Otherwise, they are not equal.
Complete State Library:
And finally, create a data_structures.sw file in the src folder. This file will include the module declarations for state and auction to bring everything together. Each module encapsulates related functionality, making the code easier to maintain, understand, and reuse.
Errors
The errors are simple, we just need to create a errors.sw file in the src folder and fill it with the errors we will use in the contract
Events
As the errors the events are simple, we need to create a events.sw file in the src folder and fill it with the errors we will use in the contract
Implementing the ABI
Now we will move to the main.sw file, and start writing the implementation of the functions defined in the ABI.
But first don’t forget to implement the ABI to your contract:
bid() function
Now, let’s start with the bid() function, which allows a user to place a bid on an auction.
By default, a contract may not receive a Native Asset in a contract call. To allow the transfer of assets to the contract, we will add the #[payable] attribute to the function. Since we want to read from or write to storage, we also need to add the #[storage(read, write)] attribute.
As we want the user to be able to specify to which auction they want to bid, we should add a parameter auction_id of type u64, which is the ID of the auction on which the bid is being placed.
First, we need to retrieve the auction with the specified auction_id from storage. This is important because we need to know the details of the auction before placing a bid. We then check if the auction exists. If it doesn't, we throw an error to ensure users can't bid on non-existent auctions. If the auction exists, we unwrap it to get the actual auction data. The unwrap() function extracts the value from the Option, assuming it is Some.
Next, we retrieve the sender's address and bid information: the asset being bid and the bid amount. This is important because we need to know who is bidding and what they are bidding with.
We need to ensure that the sender is not the auction seller to prevent them from bidding on their own auction. This maintains the integrity of the auction process. We also check if the auction is open and if the auction end block has not been reached. This ensures that bids can only be placed on active auctions. Next, we verify that the bid asset matches the asset required by the auction. This ensures that users are bidding with the correct type of asset.
We then combine the user's previous deposits and the current bid to get the total deposits the user has made to the auction. This is important to keep track of how much each user has bid in total.
What is match ? Unlike an if expression, a match expression asserts at compile time that all possible patterns have been matched. If you don't handle all the patterns, you will get a compiler error indicating that your match expression is non-exhaustive.
What means &sender ? The & symbol is used to create a reference to a value. A reference allows you to borrow a value without taking ownership of it, which is useful for looking up values in data structures without moving or copying the actual data.
After that we ensure that the total bid meets or exceeds the auction's initial price and that is higher than the current highest bid. This ensures that each new bid is an actual improvement over the previous one.
If there is a reserve price set, we check if it has been met or exceeded by the total bid. The reserve price is the minimum amount the seller is willing to accept.
We then check if the bid is placed within the extension threshold. If it is, we extend the auction duration to allow other bidders a chance to respond. This prevents last-minute "sniping" where someone places a bid just before the auction ends without giving others a chance to counter.
Next, we update the auction's information and store the new state. This includes setting the new highest bidder, updating the highest bid, and storing the user's total bid.
Finally, we log the bid event, which includes the amount of the highest bid, the auction ID, and the user who placed the bid. Logging is important for transparency and record-keeping.
In summary the bid function allows users to place bids on auctions. It ensures the auction exists, verifies the bid details, and updates the auction state accordingly. The function also handles reserve prices and auction extensions to prevent last-moment sniping. After updating the auction information, it logs the bid event for transparency.
If every step was followed correctly your main.sw file should look now like this:
create() function
Following the bid function, we have the create function, which allows a user to create a new auction.
Again, we will add the #[payable] and #[storage(read, write)] attributes to the function to allow the transfer of assets to the contract and to enable reading from or writing to storage.
As we want the user to be able to specify the details of the auction, we should add parameters for the asset used for bidding (bid_asset of type AssetId), the duration of the auction (duration of type u32), the initial price (initial_price of type u64), the reserve price (reserve_price of type Option<u64>), and the seller (seller of type Identity).
First, we need to ensure that the reserve price, if provided, is greater than the initial price.
We also need to ensure that the duration of the auction and the initial price are not zero.
Next, we retrieve the asset being sold and its amount from the message context. This is important because we need to know what is being auctioned and its quantity and ensure that the amount of the selling asset is not zero. This prevents creating auctions with zero assets.
Here we create a new auction from the Auction struct defined in /data_structures/auction.sw, we then set up the auction with the provided details. This includes the bid asset, the auction end block, the initial price, the reserve price, the asset being sold, the amount of the selling asset, and the seller.
Next, we store the auction information in the storage. We start by reading the current total number of auctions. We then insert the new auction into the storage map with the current total as the key and increment the total number of auctions by 1 and write it back to storage. This keeps track of how many auctions have been created.
Finally, we log the create auction event, which includes the auction ID, the bid asset, the sell asset, and the amount of the selling asset. Logging is important for transparency and record-keeping.
In summary the create function allows users to create new auctions. It ensures the provided details are valid, sets up the auction with the given parameters, stores the auction information, and logs the event for transparency.
Now your main.sw should look like this:
cancel() function
Following the create function, we have the cancel function, which allows a user to cancel an auction.
We will add the #[storage(read, write)] attribute to the function to enable reading from and writing to storage.
As we want the user to specify the auction they want to cancel, we add a parameter auction_id of type u64, which is the ID of the auction being canceled.
First, we need to retrieve the auction with the specified auction_id from storage. We then check if the auction exists. If it doesn't, we throw an error. This ensures that users can't cancel non-existent auctions and if the auction exists, we unwrap it to get the actual auction data. The unwrap() function extracts the value from the Option, assuming it is Some.
Next, we ensure that the auction is still open and has not ended and that the sender is the seller of the auction. This prevents unauthorized users from canceling the auction.
We then update the auction's information to reflect the cancellation. First we reset the highest bidder to None and change the auction state to Closed and after that we save the updated auction back to storage to ensure the changes are persisted. Finally, we log the cancel auction event, which includes the auction ID
Now the contract includes the cancel function, which enables users to cancel auctions, ensuring the auction exists, verifying the user’s authority to cancel, updating the auction state accordingly, and logging an event.
Here is how your main.sw should look like after the changes:
After the cancel function, we have the withdraw function, which allows a user to withdraw their assets from an auction.
Again to enable reading from and writing to storage we will add the #[storage(read, write)] attribute to the function.
As we want the user to specify the auction from which they want to withdraw, we add a parameter auction_id of type u64, which is the ID of the auction being interacted with.
In conclusion, Fuel and the Sway programming language offer a powerful and user-friendly platform for building high-performance decentralized applications on Ethereum. With Fuel's advanced features and Sway's modern, blockchain-optimized syntax, you can easily create secure and efficient smart contracts.
The English Auction example provided a hands-on look at how to develop, deploy, and test dApps on Fuel. This experience shows how Fuel’s unique capabilities, like its FuelVM and modular architecture, make it an exciting choice for developers looking to push the boundaries of what’s possible on Ethereum.
As you continue to explore and build with Fuel and Sway, you're stepping into a rapidly evolving ecosystem that's designed to make decentralized economies more accessible and efficient. Whether you're a seasoned developer or just getting started, Fuel offers the tools you need to bring your Web3 ideas to life.
First, we need to retrieve the auction with the specified auction_id from storage. We then check if the auction exists. If it doesn't, we throw an error. This ensures that users can't withdraw from non-existent auctions. If the auction exists, we unwrap it to get the actual auction data. The unwrap() function extracts the value from the Option, assuming it is Some.
Next, we ensure that the auction is either closed or its end block has been reached. This prevents users from withdrawing assets from ongoing auctions.
If the auction has ended but is still marked as Open, we update the state to Closed. This ensures the auction is properly closed before withdrawal.
We then retrieve the sender's address, the highest bidder information, and the sender's deposit amount using a reference to the sender. The & symbol is used to create a reference, allowing you to borrow a value without taking ownership of it.
We ensure that the sender has a deposit to withdraw, then we remove the sender's deposit from the auction using a reference to the sender using the & symbol.
We retrieve the withdrawn asset and the withdrawn amount using the * symbol for dereferencing. Dereferencing a reference allows us to access the value that the reference points to.
We then determine the sender's role and withdraw the owed assets accordingly:
Winner or Seller: If the sender is the highest bidder (winner) or if there is no highest bidder and the sender is the seller, they withdraw the original sold assets.
Seller: If the sender is the seller, they withdraw the winning bids.
Other Bidders
Finally, we log the withdraw event, which includes the withdrawn asset, the amount, the auction ID, and the user.
In summary, the withdraw function allows users to withdraw their assets from auctions. It ensures the auction exists, verifies the user's role, updates the auction state if necessary, processes the withdrawal, and logs the event for transparency.
Now your main.sw should look like this:
Read-only methods
First we will implement the Info interface from the interface.sw file. It provides read-only methods to query information about auctions.
Read-only methods, as the name suggests, are functions that only read data from the blockchain and do not make any modifications.
Let's break down each function in this implementation.
auction_info() function
The auction_info function retrieves information about a specific auction.
At the beginning we will add a #[storage(read)] attribute, which specifies that the function will read from the contract's storage.
The function takes an auction ID as a parameter and returns an Option<Auction>, which will contain the auction details if the auction exists.
deposit_balance() function
The deposit_balance function retrieves the deposit balance for a user in a specific auction.
Again at the start we will add a #[storage(read)] attribute, which specifies that the function will read from the contract's storage
The function takes an auction ID and a user identity as parameters and returns an Option<u64>, which will contain the user's deposit balance if it exists.
First, we attempt to retrieve the auction with the specified auction_id from storage. Next, we use a match expression to handle the result of the retrieval.
The possible results are:
Some(auction): If the auction exists, we attempt to get the user's deposit using their identity.
auction.deposits.get(&identity).copied(): If the user has a deposit, return a copy of the deposit amount.
None: If the auction does not exist, return None.
total_auctions() function
The total_auctions function is designed to retrieve the total number of auctions that have been created.
First, we add the #[storage(read)] attribute, indicating that this function will read from the contract's storage. The function does not take any parameters and returns a u64 value representing the total number of auctions.
These read-only functions provide essential information about auctions without altering the contract's state. Here's the complete implementation:
Step-by-Step Testing
Now that the English Auction Application has been developed, it's important to ensure it is free of bugs through thorough testing. Let's implement some tests!
Rust and The Fuel Toolchain have been installed in the Installation section. For the tests, install Cargo generate:
Run this command in your terminal:
Next, implement a test to verify that multiple bids can be placed on an auction.
First, import the necessary modules and functions for creating and bidding on auctions, as well as setting up the test environment and default values.
Define a new module named success to group successful test cases, where the test will be included.
Declare an asynchronous test function using tokio::test and name the function.
Call the setup function asynchronously to initialize the test environment and destructure the returned tuple to extract the seller, buyer1, sell_asset, and buy_asset. Similarly, call the defaults function asynchronously and destructure the returned tuple to extract sell_amount, initial_price, reserve_price, and duration.
Note: The underscores _ indicate ignored values from the tuple that are not needed for this test.
Convert the seller's and buyer1's wallet addresses into Identity types to ensure compatibility with the auction functions.
Create an auction asynchronously with the specified parameters and assign the auction ID to auction_id.
The initial deposit balance of buyer1 in the auction should be None (indicating no deposit has been made yet), so assert that buyer1_deposit is None.
Place a bid by buyer1 with the initial price asynchronously and check that buyer1's deposit balance equals the initial price, the highest bidder is buyer1, and the auction state is Open.
Place another bid by buyer1 with an increment of 1 unit asynchronously. Apply the same checks with the difference that the buyer1's deposit balance should equal the initial price plus 1.
If all the steps were followed correctly, the test should look like this:
In conclusion, Fuel and the Sway programming language offer a powerful and user-friendly platform for building high-performance decentralized applications on Ethereum. With Fuel's advanced features and Sway's modern, blockchain-optimized syntax, you can easily create secure and efficient smart contracts.
The English Auction example provided a hands-on look at how to develop, deploy, and test dApps on Fuel. This experience shows how Fuel’s unique capabilities, like its FuelVM and modular architecture, make it an exciting choice for developers looking to push the boundaries of what’s possible on Ethereum.
As you continue to explore and build with Fuel and Sway, you're stepping into a rapidly evolving ecosystem that's designed to make decentralized economies more accessible and efficient. Whether you're a seasoned developer or just getting started, Fuel offers the tools you need to bring your Web3 ideas to life.
Reach out to Three Sigma, and let our team of seasoned professionals guide you confidently through the Web3 landscape. Our expertise in smart contract security, economic modeling, and blockchain engineering, we will help you secure your project's future.
Contact us today and turn your Web3 vision into reality!