Building a Decentralized English Auction with Sway: A Step-by-Step Guide

Simeon Cholakov & Simão Amaro
28 min read

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.

Explore our website to learn more.

Introduction

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.

Installation

Running fuelup-init

To install the Fuel toolchain, you can use the fuelup-init script. This will install forcforc-clientforc-fmtforc-lspforc-wallet as well as fuel-core in ~/.fuelup/bin.

Run the following script in your terminal.

1curl https://install.fuel.network | sh
Keep in mind that currently Windows is not supported natively. If you wish to use fuelup on Windows, please use Windows Subsystem for Linux.

Installing Rust

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.

1curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Writing the Contract

Before starting writing the contract have a look at its structure to understand it better.

Structure of the English Auction contract

Let’s start by creating a new, empty folder named english-auction, navigate into it, and create a contract project using forc:

1mkdir english-auction
2cd english-auction
3forc new auction-contract
4

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.

1contract;

Modules and imports

Next, we'll declare the modules and imports we will use.

1mod errors;
2mod data_structures;
3mod events;
4mod interface;

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.

1// use ::data_structures::{auction::Auction, state::State};
2use ::data_structures::auction::Auction;
3use ::data_structures::state::State;
4use ::errors::{AccessError, InitError, InputError, UserError};
5use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
6use ::interface::{EnglishAuction, Info};
7use std::{
8    asset::transfer,
9    block::height,
10    call_frames::msg_asset_id,
11    context::msg_amount,
12    hash::Hash,
13};

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

1storage {
2    auctions: StorageMap<u64, Auction> = StorageMap {},
3    total_auctions: u64 = 0,
4}

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

1const EXTENSION_THRESHOLD: u32 = 5; 
2const EXTENSION_DURATION: u32 = 5; 

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:

1contract;
2
3mod errors;
4mod data_structures;
5mod events;
6mod interface;
7
8// use ::data_structures::{auction::Auction, state::State};
9use ::data_structures::auction::Auction;
10use ::data_structures::state::State;
11use ::errors::{AccessError, InitError, InputError, UserError};
12use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
13use ::interface::{EnglishAuction, Info};
14use std::{
15    asset::transfer,
16    block::height,
17    call_frames::msg_asset_id,
18    context::msg_amount,
19    hash::Hash,
20};
21
22storage {
23    auctions: StorageMap<u64, Auction> = StorageMap {},
24    total_auctions: u64 = 0,
25}
26
27const EXTENSION_THRESHOLD: u32 = 5; 
28const EXTENSION_DURATION: u32 = 5;

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:

1library;
2
3use ::data_structures::auction::Auction;
4
5abi EnglishAuction {
6    /// Places a bid on the specified auction.
7    ///
8    /// # Arguments
9    ///
10    /// * `auction_id`: [u64] - The id number of the auction.
11    ///
12    /// # Reverts
13    ///
14    /// * When the `auction_id` does not map to an existing auction.
15    /// * When the auction has closed.
16    /// * When the bidding period for the auction has passed.
17    /// * When the asset provided does not match the asset accepted for the auction.
18    /// * When the bidder is the auction's seller.
19    /// * When transferring of the NFT asset to the auction contract failed.
20    /// * When the native asset amount sent and the `bid_asset` enum do not match.
21    /// * When the native asset type sent and the `bid_asset` enum do not match.
22    /// * When the bid amount is less than the initial price.
23    /// * When the bidder's total deposits are not greater than the current bid.
24    /// * When the bidder's total deposits are greater than the reserve price.
25    #[payable]
26    #[storage(read, write)]
27    fn bid(auction_id: u64);
28
29    /// Cancels the specified auction.
30    ///
31    /// # Arguments
32    ///
33    /// * `auction_id`: [u64] - The `u64` id number of the auction.
34    ///
35    /// # Reverts
36    ///
37    /// * When the `auction_id` does not map to an existing auction.
38    /// * When the auction is no longer open.
39    /// * When the sender is not the seller of the auction.
40    #[storage(read, write)]
41    fn cancel(auction_id: u64);
42
43    /// Starts an auction with a seller, selling asset, accepted bid asset, initial price, a
44    /// possible reserve price, and duration of the auction.
45    ///
46    /// This function will return the newly created auction's ID number which is used to
47    /// interact with the auction.
48    ///
49    /// # Arguments
50    ///
51    /// `bid_asset`: [AssetId] - The asset the seller is willing to accept in return for the selling asset.
52    /// `duration`: [u32] - The duration of time the auction should be open.
53    /// `initial_price`: [u64] - The starting price at which the auction should start.
54    /// `reserve_price`: [Option<u64>] - The price at which a buyer may purchase the `sell_asset` outright.
55    /// `seller`: [Identity] - The seller for this auction.
56    ///
57    /// # Returns
58    ///
59    /// * [u64] - The id number of the newly created auction.
60    ///
61    /// # Reverts
62    ///
63    /// * When the `reserve_price` is less than `initial_price` and a reserve is set.
64    /// * When the `duration` of the auction is set to zero.
65    /// * When the `bid_asset` amount is not zero.
66    /// * When the `initial_price` for coins is set to zero.
67    /// * When the native asset amount sent and the `sell_asset` enum do not match.
68    /// * When the native asset type sent and the `sell_asset` enum do not match.
69    /// * When the `initial_price` for NFTs is not one.
70    /// * When transferring of the NFT asset to the contract failed.
71    #[payable]
72    #[storage(read, write)]
73    fn create(
74        bid_asset: AssetId,
75        duration: u32,
76        inital_price: u64,
77        reserve_price: Option<u64>,
78        seller: Identity,
79    ) -> u64;
80
81    /// Allows users to withdraw their owed assets if the auction's bid period has ended, the
82    /// reserve has been met, or the auction has been canceled.
83    ///
84    /// # Additional Information
85    ///
86    /// 1. If the sender is the winning bidder, they will withdraw the selling asset.
87    /// 2. If the sender's bids failed to win the auction, their total deposits will be withdrawn.
88    /// 3. If the sender is the seller and no bids have been made or the auction has been canceled,
89    ///    they will withdraw the selling asset.
90    /// 4. If the sender is the seller and a bid has been made, they will withdraw the winning
91    ///    bidder's total deposits.
92    ///
93    /// # Arguments
94    ///
95    /// * `auction_id`: [u64] - The id number of the auction.
96    ///
97    /// # Reverts
98    ///
99    /// * When the `auction_id` provided does not map to an existing auction.
100    /// * When the bidding period of the auction has not ended.
101    /// * When the auction's `state` is still in the open bidding state.
102    /// * When the sender has already withdrawn their deposit.
103    #[storage(read, write)]
104    fn withdraw(auction_id: u64);
105}
106
107abi Info {
108    /// Returns the auction struct for the corresponding auction id.
109    ///
110    /// # Arguments
111    ///
112    /// * `auction_id`: [u64] - The id number of the auction.
113    ///
114    /// # Returns
115    ///
116    /// * [Option<Auction>] - The auction struct for the corresponding auction id.
117    #[storage(read)]
118    fn auction_info(auction_id: u64) -> Option<Auction>;
119
120    /// Returns the balance of the user's deposits for the specified auction.
121    ///
122    /// # Additional Information
123    ///
124    /// This amount will represent the bidding asset amount for bidders and the
125    /// selling asset for the seller.
126    ///
127    /// # Arguments
128    ///
129    /// * `auction_id`: [u64] - The id number of the auction.
130    /// * `identity`: [Identity] - The user which has deposited assets.
131    ///
132    /// # Returns
133    ///
134    /// * [Option<u64>] - The amount of assets the user has deposited for that auction.
135    #[storage(read)]
136    fn deposit_balance(auction_id: u64, identity: Identity) -> Option<u64>;
137
138    /// Returns the total auctions which have been started using this auction contract.
139    ///
140    /// # Returns
141    ///
142    /// * [u64] - The total number of auctions.
143    #[storage(read)]
144    fn total_auctions() -> u64;
145}

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.

1mkdir data_structures

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).

1library;
2
3use ::data_structures::state::State;

Now we will declare the Auction struct, which defines the properties of the auction.

1pub struct Auction {
2    pub bid_asset: AssetId,
3    pub end_block: u32,
4    pub highest_bid: u64,
5    pub highest_bidder: Option<Identity>,
6    pub initial_price: u64,
7    pub reserve_price: Option<u64>,
8    pub sell_asset: AssetId,
9    pub sell_asset_amount: u64,
10    pub seller: Identity,
11    pub state: State,
12    pub deposits: StorageMap<Identity, u64> = StorageMap {},
13}

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:

1impl Auction {
2    pub fn new(
3        bid_asset: AssetId,
4        end_block: u32,
5        initial_price: u64,
6        reserve_price: Option<u64>,
7        sell_asset: AssetId,
8        sell_asset_amount: u64,
9        seller: Identity,
10    ) -> Self {
11        Auction {
12            bid_asset,
13            end_block,
14            highest_bid: 0,
15            highest_bidder: Option::None,
16            initial_price,
17            reserve_price,
18            sell_asset,
19            sell_asset_amount,
20            seller,
21            state: State::Open,
22            deposits: StorageMap::new(),
23        }
24    }
25}


The auction.sw library should look like this:

1library;
2
3use ::data_structures::state::State;
4
5pub struct Auction {
6		pub bid_asset: AssetId,
7    pub end_block: u32,
8    pub highest_bid: u64,
9    pub highest_bidder: Option<Identity>,
10    pub initial_price: u64,
11    pub reserve_price: Option<u64>,
12    pub sell_asset: AssetId,
13    pub sell_asset_amount: u64,
14    pub seller: Identity,
15    pub state: State,
16    pub deposits: StorageMap<Identity, u64> = StorageMap {},
17}
18
19impl Auction {
20    pub fn new(
21        bid_asset: AssetId,
22        end_block: u32,
23        initial_price: u64,
24        reserve_price: Option<u64>,
25        sell_asset: AssetId,
26        sell_asset_amount: u64,
27        seller: Identity,
28    ) -> Self {
29        Auction {
30            bid_asset,
31            end_block,
32            highest_bid: 0,
33            highest_bidder: Option::None,
34            initial_price,
35            reserve_price,
36            sell_asset,
37            sell_asset_amount,
38            seller,
39            state: State::Open,
40            deposits: StorageMap::new(),
41        }
42    }
43}

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.

1library;
2
3pub enum State {
4    Closed: (),
5    Open: (),
6}

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.

1impl core::ops::Eq for State {
2    fn eq(self, other: Self) -> bool {
3        match (self, other) {
4            (State::Open, State::Open) => true,
5            (State::Closed, State::Closed) => true,
6            _ => false,
7        }
8    }
9}

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:
1library;
2
3pub enum State {
4    Closed: (),
5    Open: (),
6}
7
8impl core::ops::Eq for State {
9    fn eq(self, other: Self) -> bool {
10        match (self, other) {
11            (State::Open, State::Open) => true,
12            (State::Closed, State::Closed) => true,
13            _ => false,
14        }
15    }
16}

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.

1library;
2
3pub mod state;
4pub mod auction;

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

1library;
2
3/// Errors related to permissions.
4pub enum AccessError {
5    /// The auction is not yet closed.
6    AuctionIsNotClosed: (),
7    /// The auction is not yet open.
8    AuctionIsNotOpen: (),
9    /// The sender is not the auction seller.
10    SenderIsNotSeller: (),
11}
12
13/// Errors related to the initialization of the auction.
14pub enum InitError {
15    /// The auction duration is not provided.
16    AuctionDurationNotProvided: (),
17    /// The initial price cannot be zero.
18    InitialPriceCannotBeZero: (),
19    /// The reserve price cannot be lower than the initial price.
20    ReserveLessThanInitialPrice: (),
21}
22
23/// Errors related to input parameters.
24pub enum InputError {
25    /// The requested auction does not exist.
26    AuctionDoesNotExist: (),
27    /// The initial price of the auction is not met.
28    InitialPriceNotMet: (),
29    /// The incorrect amount of assets were provided.
30    IncorrectAmountProvided: (),
31    /// The incorrect asset was provided.
32    IncorrectAssetProvided: (),
33}
34
35/// Errors made by users.
36pub enum UserError {
37    /// Sellers cannot bid on their own auctions.
38    BidderIsSeller: (),
39    /// The user has already withdrawn their owed assets.
40    UserHasAlreadyWithdrawn: (),
41}

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

1library;
2
3use ::data_structures::auction::Auction;
4
5/// Event for when an auction is canceled.
6pub struct CancelAuctionEvent {
7    /// The auction id of the auction that was canceled.
8    pub auction_id: u64,
9}
10
11/// Event for when an auction is created.
12pub struct CreateAuctionEvent {
13    /// The auction id of the auction that was created.
14    pub auction_id: u64,
15    /// The asset in which bids will be received.
16    pub bid_asset: AssetId,
17    /// The asset to be sold.
18    pub sell_asset: AssetId,
19    /// The amount of the asset being sold.
20    pub sell_asset_amount: u64,
21}
22
23/// Event for when a bid is placed.
24pub struct BidEvent {
25    /// The amount of the bid.
26    pub amount: u64,
27    /// The auction id of the auction that was bid on.
28    pub auction_id: u64,
29    /// The bidder.
30    pub user: Identity,
31}
32
33/// Event for when assets are withdrawn.
34pub struct WithdrawEvent {
35    /// The asset that was withdrawn.
36    pub asset: AssetId,
37    /// The amount of the asset that is withdrawn.
38    pub asset_amount: u64,
39    /// The auction id of the auction that was withdrawn from.
40    pub auction_id: u64,
41    /// The user that withdrew the asset.
42    pub user: Identity,
43}

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:

1impl EnglishAuction for 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.

1#[payable]
2#[storage(read, write)]

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.

1fn bid(auction_id: u64) {}

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.

1let auction = storage.auctions.get(auction_id).try_read();
2require(auction.is_some(), InputError::AuctionDoesNotExist);
3let mut auction = auction.unwrap();

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.

1let sender = msg_sender().unwrap();
2let bid_asset = msg_asset_id();
3let bid_amount = msg_amount();

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.

1require(sender != auction.seller, UserError::BidderIsSeller);
2require(
3        auction.state == State::Open && auction.end_block >= height(),
4        AccessError::AuctionIsNotOpen,
5);
6require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);

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.

1let total_bid = match auction.deposits.get(&sender) {
2        Some(sender_deposit) => bid_amount + sender_deposit,
3        None => bid_amount,
4};

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.

1require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
2require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);

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.

1if auction.reserve_price.is_some() {
2        let reserve_price = auction.reserve_price.unwrap();
3        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);
4
5        if reserve_price == total_bid {
6            auction.state = State::Closed;
7        }
8    }

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.

1 if auction.end_block - height() <= EXTENSION_THRESHOLD {
2        auction.end_block += EXTENSION_DURATION;
3 }

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.

1auction.highest_bidder = Option::Some(sender);
2auction.highest_bid = total_bid;
3auction.deposits.insert(sender, total_bid);
4storage.auctions.insert(auction_id, auction);

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.

1 log(BidEvent {
2        amount: auction.highest_bid,
3        auction_id: auction_id,
4        user: sender,
5    });
6}

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:

1contract;
2
3mod errors;
4mod data_structures;
5mod events;
6mod interface;
7
8// use ::data_structures::{auction::Auction, state::State};
9use ::data_structures::auction::Auction;
10use ::data_structures::state::State;
11use ::errors::{AccessError, InitError, InputError, UserError};
12use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
13use ::interface::{EnglishAuction, Info};
14use std::{
15    asset::transfer,
16    block::height,
17    call_frames::msg_asset_id,
18    context::msg_amount,
19    hash::Hash,
20};
21
22storage {
23    auctions: StorageMap<u64, Auction> = StorageMap {},
24    total_auctions: u64 = 0,
25}
26
27const EXTENSION_THRESHOLD: u32 = 5; 
28const EXTENSION_DURATION: u32 = 5;
29
30impl EnglishAuction for Contract {
31
32#[payable]
33#[storage(read, write)]
34fn bid(auction_id: u64) {
35
36    let auction = storage.auctions.get(auction_id).try_read();
37    require(auction.is_some(), InputError::AuctionDoesNotExist);
38
39    let mut auction = auction.unwrap();
40
41    let sender = msg_sender().unwrap();
42    let bid_asset = msg_asset_id();
43    let bid_amount = msg_amount();
44
45    require(sender != auction.seller, UserError::BidderIsSeller);
46    require(
47        auction.state == State::Open && auction.end_block >= height(),
48        AccessError::AuctionIsNotOpen,
49    );
50    require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);
51
52    let total_bid = match auction.deposits.get(&sender) {
53        Some(sender_deposit) => bid_amount + sender_deposit,
54        None => bid_amount,
55    };
56
57    require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
58    require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);
59
60    if auction.reserve_price.is_some() {
61        let reserve_price = auction.reserve_price.unwrap();
62        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);
63
64        if reserve_price == total_bid {
65            auction.state = State::Closed;
66        }
67    }
68
69    if auction.end_block - height() <= EXTENSION_THRESHOLD {
70        auction.end_block += EXTENSION_DURATION;
71    }
72
73    auction.highest_bidder = Option::Some(sender);
74    auction.highest_bid = total_bid;
75    auction.deposits.insert(sender, total_bid);
76    storage.auctions.insert(auction_id, auction);
77
78    log(BidEvent {
79        amount: auction.highest_bid,
80        auction_id: auction_id,
81        user: sender,
82    });
83}
84}

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.

1#[payable]
2#[storage(read, write)]

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).

1fn create(
2    bid_asset: AssetId,
3    duration: u32,
4    initial_price: u64,
5    reserve_price: Option<u64>,
6    seller: Identity,
7) -> u64 {}

First, we need to ensure that the reserve price, if provided, is greater than the initial price.

1 require(
2        reserve_price.is_none() || (reserve_price.is_some() && reserve_price.unwrap() > initial_price),
3        InitError::ReserveLessThanInitialPrice,
4    );

We also need to ensure that the duration of the auction and the initial price are not zero.

1require(duration != 0, InitError::AuctionDurationNotProvided);
2require(initial_price != 0, InitError::InitialPriceCannotBeZero);

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.

1let sell_asset = msg_asset_id();
2let sell_asset_amount = msg_amount();
3
4require(sell_asset_amount != 0, InputError::IncorrectAmountProvided);

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.

1let auction = Auction::new(
2        bid_asset,
3        duration + height(),
4        initial_price,
5        reserve_price,
6        sell_asset,
7        sell_asset_amount,
8        seller,
9    );

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.

1let total_auctions = storage.total_auctions.read();
2storage.auctions.insert(total_auctions, auction);
3storage.total_auctions.write(total_auctions + 1);

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.

1log(CreateAuctionEvent {
2        auction_id: total_auctions,
3        bid_asset,
4        sell_asset,
5        sell_asset_amount,
6    });
7
8    total_auctions
9}

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:

1contract;
2
3mod errors;
4mod data_structures;
5mod events;
6mod interface;
7
8// use ::data_structures::{auction::Auction, state::State};
9use ::data_structures::auction::Auction;
10use ::data_structures::state::State;
11use ::errors::{AccessError, InitError, InputError, UserError};
12use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
13use ::interface::{EnglishAuction, Info};
14use std::{
15    asset::transfer,
16    block::height,
17    call_frames::msg_asset_id,
18    context::msg_amount,
19    hash::Hash,
20};
21
22storage {
23    auctions: StorageMap<u64, Auction> = StorageMap {},
24    total_auctions: u64 = 0,
25}
26
27const EXTENSION_THRESHOLD: u32 = 5; 
28const EXTENSION_DURATION: u32 = 5;
29
30impl EnglishAuction for Contract {
31
32#[payable]
33#[storage(read, write)]
34fn bid(auction_id: u64) {
35
36    let auction = storage.auctions.get(auction_id).try_read();
37    require(auction.is_some(), InputError::AuctionDoesNotExist);
38
39    let mut auction = auction.unwrap();
40
41    let sender = msg_sender().unwrap();
42    let bid_asset = msg_asset_id();
43    let bid_amount = msg_amount();
44
45    require(sender != auction.seller, UserError::BidderIsSeller);
46    require(
47        auction.state == State::Open && auction.end_block >= height(),
48        AccessError::AuctionIsNotOpen,
49    );
50    require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);
51
52    let total_bid = match auction.deposits.get(&sender) {
53        Some(sender_deposit) => bid_amount + sender_deposit,
54        None => bid_amount,
55    };
56
57    require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
58    require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);
59
60    if auction.reserve_price.is_some() {
61        let reserve_price = auction.reserve_price.unwrap();
62        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);
63
64        if reserve_price == total_bid {
65            auction.state = State::Closed;
66        }
67    }
68
69    if auction.end_block - height() <= EXTENSION_THRESHOLD {
70        auction.end_block += EXTENSION_DURATION;
71    }
72
73    auction.highest_bidder = Option::Some(sender);
74    auction.highest_bid = total_bid;
75    auction.deposits.insert(sender, total_bid);
76    storage.auctions.insert(auction_id, auction);
77
78    log(BidEvent {
79        amount: auction.highest_bid,
80        auction_id: auction_id,
81        user: sender,
82    });
83}
84
85#[payable]
86#[storage(read, write)]
87fn create(
88    bid_asset: AssetId,
89    duration: u32,
90    initial_price: u64,
91    reserve_price: Option<u64>,
92    seller: Identity,
93) -> u64 {
94    require(
95        reserve_price.is_none() || (reserve_price.is_some() && reserve_price.unwrap() > initial_price),
96        InitError::ReserveLessThanInitialPrice,
97    );
98    require(duration != 0, InitError::AuctionDurationNotProvided);
99    require(initial_price != 0, InitError::InitialPriceCannotBeZero);
100
101    let sell_asset = msg_asset_id();
102    let sell_asset_amount = msg_amount();
103    require(sell_asset_amount != 0, InputError::IncorrectAmountProvided);
104
105    let auction = Auction::new(
106        bid_asset,
107        duration + height(),
108        initial_price,
109        reserve_price,
110        sell_asset,
111        sell_asset_amount,
112        seller,
113    );
114
115    let total_auctions = storage.total_auctions.read();
116    storage.auctions.insert(total_auctions, auction);
117    storage.total_auctions.write(total_auctions + 1);
118
119    log(CreateAuctionEvent {
120        auction_id: total_auctions,
121        bid_asset,
122        sell_asset,
123        sell_asset_amount,
124    });
125
126    total_auctions
127}
128}

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.

1#[storage(read, write)]

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.

1fn cancel(auction_id: u64) {}

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.

1let auction = storage.auctions.get(auction_id).try_read();
2require(auction.is_some(), InputError::AuctionDoesNotExist);
3let mut auction = auction.unwrap();

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.

1require(
2    auction.state == State::Open && auction.end_block >= height(),
3    AccessError::AuctionIsNotOpen,
4);
5require(
6    msg_sender().unwrap() == auction.seller,
7    AccessError::SenderIsNotSeller,
8);

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

1auction.highest_bidder = Option::None;
2auction.state = State::Closed;
3storage.auctions.insert(auction_id, auction);
4
5log(CancelAuctionEvent { 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:

1contract;
2
3mod errors;
4mod data_structures;
5mod events;
6mod interface;
7
8// use ::data_structures::{auction::Auction, state::State};
9use ::data_structures::auction::Auction;
10use ::data_structures::state::State;
11use ::errors::{AccessError, InitError, InputError, UserError};
12use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
13use ::interface::{EnglishAuction, Info};
14use std::{
15    asset::transfer,
16    block::height,
17    call_frames::msg_asset_id,
18    context::msg_amount,
19    hash::Hash,
20};
21
22storage {
23    auctions: StorageMap<u64, Auction> = StorageMap {},
24    total_auctions: u64 = 0,
25}
26
27const EXTENSION_THRESHOLD: u32 = 5; 
28const EXTENSION_DURATION: u32 = 5;
29
30impl EnglishAuction for Contract {
31
32#[payable]
33#[storage(read, write)]
34fn bid(auction_id: u64) {
35
36    let auction = storage.auctions.get(auction_id).try_read();
37    require(auction.is_some(), InputError::AuctionDoesNotExist);
38
39    let mut auction = auction.unwrap();
40
41    let sender = msg_sender().unwrap();
42    let bid_asset = msg_asset_id();
43    let bid_amount = msg_amount();
44
45    require(sender != auction.seller, UserError::BidderIsSeller);
46    require(
47        auction.state == State::Open && auction.end_block >= height(),
48        AccessError::AuctionIsNotOpen,
49    );
50    require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);
51
52    let total_bid = match auction.deposits.get(&sender) {
53        Some(sender_deposit) => bid_amount + sender_deposit,
54        None => bid_amount,
55    };
56
57    require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
58    require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);
59
60    if auction.reserve_price.is_some() {
61        let reserve_price = auction.reserve_price.unwrap();
62        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);
63
64        if reserve_price == total_bid {
65            auction.state = State::Closed;
66        }
67    }
68
69    if auction.end_block - height() <= EXTENSION_THRESHOLD {
70        auction.end_block += EXTENSION_DURATION;
71    }
72
73    auction.highest_bidder = Option::Some(sender);
74    auction.highest_bid = total_bid;
75    auction.deposits.insert(sender, total_bid);
76    storage.auctions.insert(auction_id, auction);
77
78    log(BidEvent {
79        amount: auction.highest_bid,
80        auction_id: auction_id,
81        user: sender,
82    });
83}
84
85#[payable]
86#[storage(read, write)]
87fn create(
88    bid_asset: AssetId,
89    duration: u32,
90    initial_price: u64,
91    reserve_price: Option<u64>,
92    seller: Identity,
93) -> u64 {
94    require(
95        reserve_price.is_none() || (reserve_price.is_some() && reserve_price.unwrap() > initial_price),
96        InitError::ReserveLessThanInitialPrice,
97    );
98    require(duration != 0, InitError::AuctionDurationNotProvided);
99    require(initial_price != 0, InitError::InitialPriceCannotBeZero);
100
101    let sell_asset = msg_asset_id();
102    let sell_asset_amount = msg_amount();
103    require(sell_asset_amount != 0, InputError::IncorrectAmountProvided);
104
105    let auction = Auction::new(
106        bid_asset,
107        duration + height(),
108        initial_price,
109        reserve_price,
110        sell_asset,
111        sell_asset_amount,
112        seller,
113    );
114
115    let total_auctions = storage.total_auctions.read();
116    storage.auctions.insert(total_auctions, auction);
117    storage.total_auctions.write(total_auctions + 1);
118
119    log(CreateAuctionEvent {
120        auction_id: total_auctions,
121        bid_asset,
122        sell_asset,
123        sell_asset_amount,
124    });
125
126    total_auctions
127}
128
129#[storage(read, write)]
130fn cancel(auction_id: u64) {
131    let auction = storage.auctions.get(auction_id).try_read();
132    require(auction.is_some(), InputError::AuctionDoesNotExist);
133
134    let mut auction = auction.unwrap();
135
136    require(
137        auction.state == State::Open && auction.end_block >= height(),
138        AccessError::AuctionIsNotOpen,
139    );
140
141    require(
142        msg_sender().unwrap() == auction.seller,
143        AccessError::SenderIsNotSeller,
144    );
145
146    auction.highest_bidder = Option::None;
147    auction.state = State::Closed;
148    storage.auctions.insert(auction_id, auction);
149
150    log(CancelAuctionEvent { auction_id });
151}
152}

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.

1#[storage(read, write)]

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.

1fn withdraw(auction_id: u64) {}

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.

1let auction = storage.auctions.get(auction_id).try_read();
2require(auction.is_some(), InputError::AuctionDoesNotExist);
3let mut auction = auction.unwrap();

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.

1require(
2    auction.state == State::Closed || auction.end_block <= height(),
3    AccessError::AuctionIsNotClosed,
4);

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.

1if auction.end_block <= height() && auction.state == State::Open {
2    auction.state = State::Closed;
3    storage.auctions.insert(auction_id, auction);
4}

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.

1let sender = msg_sender().unwrap();
2let bidder = auction.highest_bidder;
3let sender_deposit = auction.deposits.get(&sender);

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.

1require(sender_deposit.is_some(), UserError::UserHasAlreadyWithdrawn);
2auction.deposits.remove(&sender);

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.

1let mut withdrawn_amount = *sender_deposit.unwrap();
2let mut withdrawn_asset = auction.bid_asset;

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
1if (bidder.is_some() && sender == bidder.unwrap()) || (bidder.is_none() && sender == auction.seller) {
2    transfer(sender, auction.sell_asset, auction.sell_asset_amount);
3    withdrawn_asset = auction.sell_asset;
4    withdrawn_amount = auction.sell_asset_amount;
5} else if sender == auction.seller {
6    transfer(sender, auction.bid_asset, auction.highest_bid);
7    withdrawn_amount = auction.highest_bid;
8} else {
9    transfer(sender, withdrawn_asset, withdrawn_amount);
10}

Finally, we log the withdraw event, which includes the withdrawn asset, the amount, the auction ID, and the user.

1log(WithdrawEvent {
2    asset: withdrawn_asset,
3    asset_amount: withdrawn_amount,
4    auction_id,
5    user: sender,
6});

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:

1contract;
2
3mod errors;
4mod data_structures;
5mod events;
6mod interface;
7
8// use ::data_structures::{auction::Auction, state::State};
9use ::data_structures::auction::Auction;
10use ::data_structures::state::State;
11use ::errors::{AccessError, InitError, InputError, UserError};
12use ::events::{BidEvent, CancelAuctionEvent, CreateAuctionEvent, WithdrawEvent};
13use ::interface::{EnglishAuction, Info};
14use std::{
15    asset::transfer,
16    block::height,
17    call_frames::msg_asset_id,
18    context::msg_amount,
19    hash::Hash,
20};
21
22storage {
23    auctions: StorageMap<u64, Auction> = StorageMap {},
24    total_auctions: u64 = 0,
25}
26
27const EXTENSION_THRESHOLD: u32 = 5; 
28const EXTENSION_DURATION: u32 = 5;
29
30impl EnglishAuction for Contract {
31
32#[payable]
33#[storage(read, write)]
34fn bid(auction_id: u64) {
35
36    let auction = storage.auctions.get(auction_id).try_read();
37    require(auction.is_some(), InputError::AuctionDoesNotExist);
38
39    let mut auction = auction.unwrap();
40
41    let sender = msg_sender().unwrap();
42    let bid_asset = msg_asset_id();
43    let bid_amount = msg_amount();
44
45    require(sender != auction.seller, UserError::BidderIsSeller);
46    require(
47        auction.state == State::Open && auction.end_block >= height(),
48        AccessError::AuctionIsNotOpen,
49    );
50    require(bid_asset == auction.bid_asset, InputError::IncorrectAssetProvided);
51
52    let total_bid = match auction.deposits.get(&sender) {
53        Some(sender_deposit) => bid_amount + sender_deposit,
54        None => bid_amount,
55    };
56
57    require(total_bid >= auction.initial_price, InputError::InitialPriceNotMet);
58    require(total_bid > auction.highest_bid, InputError::IncorrectAmountProvided);
59
60    if auction.reserve_price.is_some() {
61        let reserve_price = auction.reserve_price.unwrap();
62        require(reserve_price >= total_bid, InputError::IncorrectAmountProvided);
63
64        if reserve_price == total_bid {
65            auction.state = State::Closed;
66        }
67    }
68
69    if auction.end_block - height() <= EXTENSION_THRESHOLD {
70        auction.end_block += EXTENSION_DURATION;
71    }
72
73    auction.highest_bidder = Option::Some(sender);
74    auction.highest_bid = total_bid;
75    auction.deposits.insert(sender, total_bid);
76    storage.auctions.insert(auction_id, auction);
77
78    log(BidEvent {
79        amount: auction.highest_bid,
80        auction_id: auction_id,
81        user: sender,
82    });
83}
84
85#[payable]
86#[storage(read, write)]
87fn create(
88    bid_asset: AssetId,
89    duration: u32,
90    initial_price: u64,
91    reserve_price: Option<u64>,
92    seller: Identity,
93) -> u64 {
94    require(
95        reserve_price.is_none() || (reserve_price.is_some() && reserve_price.unwrap() > initial_price),
96        InitError::ReserveLessThanInitialPrice,
97    );
98    require(duration != 0, InitError::AuctionDurationNotProvided);
99    require(initial_price != 0, InitError::InitialPriceCannotBeZero);
100
101    let sell_asset = msg_asset_id();
102    let sell_asset_amount = msg_amount();
103    require(sell_asset_amount != 0, InputError::IncorrectAmountProvided);
104
105    let auction = Auction::new(
106        bid_asset,
107        duration + height(),
108        initial_price,
109        reserve_price,
110        sell_asset,
111        sell_asset_amount,
112        seller,
113    );
114
115    let total_auctions = storage.total_auctions.read();
116    storage.auctions.insert(total_auctions, auction);
117    storage.total_auctions.write(total_auctions + 1);
118
119    log(CreateAuctionEvent {
120        auction_id: total_auctions,
121        bid_asset,
122        sell_asset,
123        sell_asset_amount,
124    });
125
126    total_auctions
127}
128
129#[storage(read, write)]
130fn cancel(auction_id: u64) {
131    let auction = storage.auctions.get(auction_id).try_read();
132    require(auction.is_some(), InputError::AuctionDoesNotExist);
133
134    let mut auction = auction.unwrap();
135
136    require(
137        auction.state == State::Open && auction.end_block >= height(),
138        AccessError::AuctionIsNotOpen,
139    );
140
141    require(
142        msg_sender().unwrap() == auction.seller,
143        AccessError::SenderIsNotSeller,
144    );
145
146    auction.highest_bidder = Option::None;
147    auction.state = State::Closed;
148    storage.auctions.insert(auction_id, auction);
149
150    log(CancelAuctionEvent { auction_id });
151}
152
153#[storage(read, write)]
154fn withdraw(auction_id: u64) {
155    let auction = storage.auctions.get(auction_id).try_read();
156    require(auction.is_some(), InputError::AuctionDoesNotExist);
157
158    let mut auction = auction.unwrap();
159    require(
160        auction.state == State::Closed || auction.end_block <= height(),
161        AccessError::AuctionIsNotClosed,
162    );
163
164    if auction.end_block <= height() && auction.state == State::Open {
165        auction.state = State::Closed;
166        storage.auctions.insert(auction_id, auction);
167    }
168
169    let sender = msg_sender().unwrap();
170    let bidder = auction.highest_bidder;
171    let sender_deposit = auction.deposits.get(&sender);
172
173    require(sender_deposit.is_some(), UserError::UserHasAlreadyWithdrawn);
174    auction.deposits.remove(&sender);
175    let mut withdrawn_amount = *sender_deposit.unwrap();
176    let mut withdrawn_asset = auction.bid_asset;
177
178    if (bidder.is_some() && sender == bidder.unwrap()) || (bidder.is_none() && sender == auction.seller) {
179        transfer(sender, auction.sell_asset, auction.sell_asset_amount);
180        withdrawn_asset = auction.sell_asset;
181        withdrawn_amount = auction.sell_asset_amount;
182    } else if sender == auction.seller {
183        transfer(sender, auction.bid_asset, auction.highest_bid);
184        withdrawn_amount = auction.highest_bid;
185    } else {
186        transfer(sender, withdrawn_asset, withdrawn_amount);
187    }
188
189    log(WithdrawEvent {
190        asset: withdrawn_asset,
191        asset_amount: withdrawn_amount,
192        auction_id,
193        user: sender,
194    });
195}
196}

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.

1#[storage(read)]

The function takes an auction ID as a parameter and returns an Option<Auction>, which will contain the auction details if the auction exists.

1fn auction_info(auction_id: u64) -> Option<Auction> {
2        storage.auctions.get(auction_id).try_read()
3    }

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

1#[storage(read)]

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.

1fn deposit_balance(auction_id: u64, identity: Identity) -> Option<u64> {}

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.

1let auction = storage.auctions.get(auction_id).try_read();
2
3match auction {
4    Some(auction) => auction.deposits.get(&identity).copied(),
5    None => None,
6}

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.

1#[storage(read)]
2fn total_auctions() -> u64 {
3        storage.total_auctions.read()
4    }

These read-only functions provide essential information about auctions without altering the contract's state. Here's the complete implementation:

1impl Info for Contract {
2    #[storage(read)] 
3    fn auction_info(auction_id: u64) -> Option<Auction> {
4        storage.auctions.get(auction_id).try_read() 
5    }
6
7    #[storage(read)] 
8    fn deposit_balance(auction_id: u64, identity: Identity) -> Option<u64> {
9        let auction = storage.auctions.get(auction_id).try_read(); 
10        match auction {
11            Some(auction) => auction.deposits.get(&identity).copied(),
12            None => None, 
13        }
14    }
15
16    #[storage(read)] 
17    fn total_auctions() -> u64 {
18        storage.total_auctions.read() 
19    }
20}

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:

1cargo install cargo-generate --locked

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.

1use crate::utils::{
2    interface::core::auction::{bid, create},
3    setup::{defaults, setup},
4};
5use fuels::types::Identity;

Define a new module named success to group successful test cases, where the test will be included.

1mod success {
2    use super::*;
3    use crate::utils::{
4        interface::info::{auction_info, deposit_balance},
5        setup::{Auction, State},
6    };
7}

Declare an asynchronous test function using tokio::test and name the function.

1   #[tokio::test]
2    async fn places_multiple_bids() {}

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.
1let (_, seller, buyer1, _, _, sell_asset, buy_asset) = setup().await;
2let (sell_amount, initial_price, reserve_price, duration, _initial_wallet_amount) = defaults().await;

Convert the seller's and buyer1's wallet addresses into Identity types to ensure compatibility with the auction functions.

1let seller_identity = Identity::Address(seller.wallet.address().into());
2let buyer1_identity = Identity::Address(buyer1.wallet.address().into());let seller_identity = Identity::Address(seller.wallet.address().into());
3let buyer1_identity = Identity::Address(buyer1.wallet.address().into());

Create an auction asynchronously with the specified parameters and assign the auction ID to auction_id.

1     let auction_id = create(
2            buy_asset,
3            &seller.auction,
4            duration,
5            initial_price,
6            Some(reserve_price),
7            seller_identity,
8            sell_asset,
9            sell_amount,
10        )
11        .await;

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.

1let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await;
2assert!(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.

1bid(auction_id, buy_asset, initial_price, &buyer1.auction).await;
2
3let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
4            
5let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
6
7assert_eq!(buyer1_deposit, initial_price);
8assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
9assert_eq!(auction.state, State::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.

1bid(auction_id, buy_asset, 1, &buyer1.auction).await;
2
3let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
4
5let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
6
7assert_eq!(buyer1_deposit, initial_price + 1);
8assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
9assert_eq!(auction.state, State::Open);

If all the steps were followed correctly, the test should look like this:

1use crate::utils::{
2    interface::core::auction::{bid, create},
3    setup::{defaults, setup},
4};
5use fuels::types::Identity;
6
7mod success {
8    use super::*;
9    use crate::utils::{
10        interface::info::{auction_info, deposit_balance},
11        setup::{Auction, State},
12    };
13    
14    #[tokio::test]
15    async fn places_multiple_bids() {
16    
17    let (_, seller, buyer1, _, _, sell_asset, buy_asset) = setup().await;
18		let (sell_amount, initial_price, reserve_price, duration, _initial_wallet_amount) = defaults().await;
19    
20    let seller_identity = Identity::Address(seller.wallet.address().into());
21		let buyer1_identity = Identity::Address(buyer1.wallet.address().into());
22    
23    let auction_id = create(
24            buy_asset,
25            &seller.auction,
26            duration,
27            initial_price,
28            Some(reserve_price),
29            seller_identity,
30            sell_asset,
31            sell_amount,
32        ).await;
33    
34    let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await;
35		assert!(buyer1_deposit.is_none());
36		
37		bid(auction_id, buy_asset, initial_price, &buyer1.auction).await;
38
39		let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
40            
41		let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
42
43		assert_eq!(buyer1_deposit, initial_price);
44		assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
45		assert_eq!(auction.state, State::Open);
46		
47		bid(auction_id, buy_asset, 1, &buyer1.auction).await;
48
49		let buyer1_deposit = deposit_balance(auction_id, &buyer1.auction, buyer1_identity).await.unwrap();
50
51		let auction: Auction = auction_info(auction_id, &seller.auction).await.unwrap();
52
53		assert_eq!(buyer1_deposit, initial_price + 1);
54		assert_eq!(auction.highest_bidder.unwrap(), buyer1_identity);
55		assert_eq!(auction.state, State::Open);
56    }
57}

Now it’s your turn to implement more tests!

For reference you can check the test folder of the repo.

Conclusion

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.

References: Fuel, Fuel Docs

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!