Arbitrum Stylus logo

Stylus by Example

English Auction

An Arbitrum Stylus version implementation of Solidity English Auction.

Auction

  1. Seller of NFT deploys this contract.
  2. Auction lasts for 7 days.
  3. Participants can bid by depositing ETH greater than the current highest bidder.
  4. All bidders can withdraw their bid if it is not the current highest bid.

After the auction

  1. Highest bidder becomes the new owner of NFT.
  2. The seller receives the highest bid of ETH.

Here is the interface for English Auction.

1interface IEnglishAuction {
2    function nft() external view returns (address);
3
4    function nftId() external view returns (uint256);
5
6    function seller() external view returns (address);
7
8    function endAt() external view returns (uint256);
9
10    function started() external view returns (bool);
11
12    function ended() external view returns (bool);
13
14    function highestBidder() external view returns (address);
15
16    function highestBid() external view returns (uint256);
17
18    function bids(address bidder) external view returns (uint256);
19
20    function initialize(address nft, uint256 nft_id, uint256 starting_bid) external;
21
22    function start() external;
23
24    function bid() external payable;
25
26    function withdraw() external;
27
28    function end() external;
29
30    error AlreadyInitialized();
31
32    error AlreadyStarted();
33
34    error NotSeller();
35
36    error AuctionEnded();
37
38    error BidTooLow();
39
40    error NotStarted();
41
42    error NotEnded();
43}
1interface IEnglishAuction {
2    function nft() external view returns (address);
3
4    function nftId() external view returns (uint256);
5
6    function seller() external view returns (address);
7
8    function endAt() external view returns (uint256);
9
10    function started() external view returns (bool);
11
12    function ended() external view returns (bool);
13
14    function highestBidder() external view returns (address);
15
16    function highestBid() external view returns (uint256);
17
18    function bids(address bidder) external view returns (uint256);
19
20    function initialize(address nft, uint256 nft_id, uint256 starting_bid) external;
21
22    function start() external;
23
24    function bid() external payable;
25
26    function withdraw() external;
27
28    function end() external;
29
30    error AlreadyInitialized();
31
32    error AlreadyStarted();
33
34    error NotSeller();
35
36    error AuctionEnded();
37
38    error BidTooLow();
39
40    error NotStarted();
41
42    error NotEnded();
43}

Example implementation of a English Auction contract written in Rust.

src/lib.rs

1// Allow `cargo stylus export-abi` to generate a main function.
2#![cfg_attr(not(feature = "export-abi"), no_main)]
3extern crate alloc;
4
5/// Use an efficient WASM allocator.
6#[global_allocator]
7static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
8
9/// Import items from the SDK. The prelude contains common traits and macros.
10use stylus_sdk::{alloy_primitives::{Address, U256}, block, call::{transfer_eth, Call}, contract, evm, msg, prelude::*};
11use alloy_sol_types::sol;
12
13// Import the IERC721 interface.
14sol_interface! {
15    interface IERC721 {
16        // Required methods.
17        function safeTransferFrom(address from, address to, uint256 token_id) external;
18        function transferFrom(address, address, uint256) external;
19    }
20}
21
22// Define the events and errors for the contract.
23sol!{
24    // Define the events for the contract.
25    event Start(); // Start the auction.
26    event Bid(address indexed sender, uint256 amount); // Bid on the auction.
27    event Withdraw(address indexed bidder, uint256 amount); // Withdraw a bid.
28    event End(address winner, uint256 amount); // End the auction.
29
30    // Define the errors for the contract.
31    error AlreadyInitialized(); // The contract has already been initialized.
32    error AlreadyStarted(); // The auction has already started.
33    error NotSeller(); // The sender is not the seller.
34    error AuctionEnded(); // The auction has ended.
35    error BidTooLow(); // The bid is too low.
36    error NotStarted(); // The auction has not started.
37    error NotEnded(); // The auction has not ended.
38}
39
40
41#[derive(SolidityError)]
42pub enum EnglishAuctionError {
43    // Define the errors for the contract.
44    AlreadyInitialized(AlreadyInitialized),
45    AlreadyStarted(AlreadyStarted),
46    NotSeller(NotSeller),
47    AuctionEnded(AuctionEnded),
48    BidTooLow(BidTooLow),
49    NotStarted(NotStarted),
50    NotEnded(NotEnded),
51}
52
53// Define some persistent storage using the Solidity ABI.
54// `Counter` will be the entrypoint.
55sol_storage! {
56    #[entrypoint]
57    pub struct EnglishAuction {
58        address nft_address; // The address of the NFT contract.
59        uint256 nft_id; // The ID of the NFT.
60
61        address seller; // The address of the seller.
62        uint256 end_at; // The end time of the auction.
63        bool started; // The auction has started or not.
64        bool ended; // The auction has ended or not.
65
66        address highest_bidder; // The address of the highest bidder.
67        uint256 highest_bid; // The highest bid.
68        mapping(address => uint256) bids; // The bids of the bidders.
69    }
70}
71
72/// Declare that `Counter` is a contract with the following external methods.
73#[external]
74impl EnglishAuction {
75    pub const ONE_DAY: u64 = 86400; // 1 day = 24 hours * 60 minutes * 60 seconds = 86400 seconds.
76    
77    // Get nft address
78    pub fn nft(&self) -> Result<Address, EnglishAuctionError> {
79        Ok(self.nft_address.get())
80    }
81
82    // Get nft id
83    pub fn nft_id(&self) -> Result<U256, EnglishAuctionError> {
84        Ok(self.nft_id.get())
85    }
86    // Get seller address
87    pub fn seller(&self) -> Result<Address, EnglishAuctionError> {
88        Ok(self.seller.get())
89    }
90
91    // Get end time
92    pub fn end_at(&self) -> Result<U256, EnglishAuctionError> {
93        Ok(self.end_at.get())
94    }
95
96    // Get started status
97    pub fn started(&self) -> Result<bool, EnglishAuctionError> {
98        Ok(self.started.get())
99    }
100
101    // Get ended status
102    pub fn ended(&self) -> Result<bool, EnglishAuctionError> {
103        Ok(self.ended.get())
104    }
105
106    // Get highest bidder address
107    pub fn highest_bidder(&self) -> Result<Address, EnglishAuctionError> {
108        Ok(self.highest_bidder.get())
109    }
110
111    // Get highest bid amount
112    pub fn highest_bid(&self) -> Result<U256, EnglishAuctionError> {
113        Ok(self.highest_bid.get())
114    }
115
116    // Get bid amount of a bidder
117    pub fn bids(&self, bidder: Address) -> Result<U256, EnglishAuctionError> {
118        Ok(self.bids.getter(bidder).get())
119    }
120
121    // Initialize program
122    pub fn initialize(&mut self, nft: Address, nft_id: U256, starting_bid: U256) -> Result<(), EnglishAuctionError> {
123        // Check if the contract has already been initialized.
124        if self.seller.get() != Address::default() {
125            // Return an error if the contract has already been initialized.
126            return Err(EnglishAuctionError::AlreadyInitialized(AlreadyInitialized{}));
127        }
128        
129        // Initialize the contract with the NFT address, the NFT ID, the seller, and the starting bid.
130        self.nft_address.set(nft);
131        self.nft_id.set(nft_id);
132        self.seller.set(msg::sender());
133        self.highest_bid.set(starting_bid);
134        Ok(())
135    }
136
137    pub fn start(&mut self) -> Result<(), EnglishAuctionError> {
138        // Check if the auction has already started.
139        if self.started.get() {
140            return Err(EnglishAuctionError::AlreadyStarted(AlreadyStarted{}));
141        }
142        
143        // Check if the sender is the seller.
144        if self.seller.get() != msg::sender() {
145            // Return an error if the sender is the seller.
146            return Err(EnglishAuctionError::NotSeller(NotSeller{}));
147        }
148        
149        // Create a new instance of the IERC721 interface.
150        let nft = IERC721::new(*self.nft_address);
151        // Get the NFT ID.
152        let nft_id = self.nft_id.get();
153
154        // Transfer the NFT to the contract.
155        let config = Call::new();
156        let result = nft.transfer_from(config, msg::sender(), contract::address(), nft_id);
157        
158        match result {
159            // If the transfer is successful, start the auction.
160            Ok(_) => {
161                self.started.set(true);
162                // Set the end time of the auction to 7 days from now.
163                self.end_at.set(U256::from(block::timestamp() + 7 * Self::ONE_DAY));
164                // Log the start event.
165                evm::log(Start {});
166                Ok(())
167            },
168            // If the transfer fails, return an error.
169            Err(_) => {
170                return Err(EnglishAuctionError::NotSeller(NotSeller{}));
171            }
172            
173        }
174    }
175
176    // The bid method allows bidders to place a bid on the auction.
177    #[payable]
178    pub fn bid(&mut self) -> Result<(), EnglishAuctionError> {
179        // Check if the auction has started.
180        if !self.started.get() {
181            // Return an error if the auction has not started.
182            return Err(EnglishAuctionError::NotSeller(NotSeller{}));
183        }
184        
185        // Check if the auction has ended.
186        if U256::from(block::timestamp()) >= self.end_at.get() {
187            // Return an error if the auction has ended.
188            return Err(EnglishAuctionError::AuctionEnded(AuctionEnded{}));
189        }
190        
191        // Check if the bid amount is higher than the current highest bid.
192        if msg::value() <= self.highest_bid.get() {
193            // Return an error if the bid amount is too low.
194            return Err(EnglishAuctionError::BidTooLow(BidTooLow{}));
195        }
196        
197        // Refund the previous highest bidder. (But will not transfer back at this call, needs bidders to call withdraw() to get back the fund.
198        if self.highest_bidder.get() != Address::default() {
199            let mut bid = self.bids.setter(self.highest_bidder.get());
200            let current_bid = bid.get();
201            bid.set(current_bid + self.highest_bid.get());
202        }
203        
204        // Update the highest bidder and the highest bid.
205        self.highest_bidder.set(msg::sender());
206        self.highest_bid.set(msg::value());
207
208        // Update the bid of the current bidder.
209        evm::log(Bid {
210            sender: msg::sender(),
211            amount: msg::value(),
212        });
213        Ok(())
214    }
215
216    // The withdraw method allows bidders to withdraw their bid.
217    pub fn withdraw(&mut self) -> Result<(), EnglishAuctionError> {
218        // Get the current bid of the bidder.
219        let mut current_bid = self.bids.setter(msg::sender());
220        let bal = current_bid.get();
221        // Set the record of this bidder to 0 and transfer back tokens.
222        current_bid.set(U256::from(0));
223        let _ = transfer_eth(msg::sender(), bal);
224
225        // Log the withdraw event.
226        evm::log(Withdraw {
227            bidder: msg::sender(),
228            amount: bal,
229        });
230        Ok(())
231    }
232
233    // The end method allows the seller to end the auction.
234    pub fn end(&mut self) -> Result<(), EnglishAuctionError> {
235        // Check if the auction has started.
236        if !self.started.get() {
237            // Return an error if the auction has not started.
238            return Err(EnglishAuctionError::NotStarted(NotStarted{}));
239        }
240        
241        // Check if the auction has ended.
242        if U256::from(block::timestamp()) < self.end_at.get() {
243            // Return an error if the auction has not ended.
244            return Err(EnglishAuctionError::NotEnded(NotEnded{}));
245        }
246        
247        // Check if the auction has already ended.
248        if self.ended.get() {
249            // Return an error if the auction has already ended.
250            return Err(EnglishAuctionError::AuctionEnded(AuctionEnded{}));
251        }
252        
253        // End the auction and transfer the NFT and the highest bid to the winner.
254        self.ended.set(true);
255
256        let seller_address = self.seller.get();
257        let highest_bid = self.highest_bid.get();
258        let highest_bidder = self.highest_bidder.get();
259        let nft_id = self.nft_id.get();
260        let config = Call::new();
261        let nft = IERC721::new(*self.nft_address);
262        
263        // Check if there is highest bidder.
264        if self.highest_bidder.get() != Address::default() {
265            // If there is a highest bidder, transfer the NFT to the highest bidder.
266            let _ = nft.safe_transfer_from(config, contract::address(), highest_bidder, nft_id);
267            // Transfer the highest bid to the seller.
268            let _ = transfer_eth(seller_address, highest_bid);
269        } else {
270            // If there is no highest bidder, transfer the NFT back to the seller.
271            let _ = nft.safe_transfer_from(config, contract::address(), seller_address, nft_id);
272        }
273
274        // Log the end event.
275        evm::log(End {
276            winner: highest_bidder,
277            amount: highest_bid,
278        });
279        Ok(())
280    }
281}
1// Allow `cargo stylus export-abi` to generate a main function.
2#![cfg_attr(not(feature = "export-abi"), no_main)]
3extern crate alloc;
4
5/// Use an efficient WASM allocator.
6#[global_allocator]
7static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT;
8
9/// Import items from the SDK. The prelude contains common traits and macros.
10use stylus_sdk::{alloy_primitives::{Address, U256}, block, call::{transfer_eth, Call}, contract, evm, msg, prelude::*};
11use alloy_sol_types::sol;
12
13// Import the IERC721 interface.
14sol_interface! {
15    interface IERC721 {
16        // Required methods.
17        function safeTransferFrom(address from, address to, uint256 token_id) external;
18        function transferFrom(address, address, uint256) external;
19    }
20}
21
22// Define the events and errors for the contract.
23sol!{
24    // Define the events for the contract.
25    event Start(); // Start the auction.
26    event Bid(address indexed sender, uint256 amount); // Bid on the auction.
27    event Withdraw(address indexed bidder, uint256 amount); // Withdraw a bid.
28    event End(address winner, uint256 amount); // End the auction.
29
30    // Define the errors for the contract.
31    error AlreadyInitialized(); // The contract has already been initialized.
32    error AlreadyStarted(); // The auction has already started.
33    error NotSeller(); // The sender is not the seller.
34    error AuctionEnded(); // The auction has ended.
35    error BidTooLow(); // The bid is too low.
36    error NotStarted(); // The auction has not started.
37    error NotEnded(); // The auction has not ended.
38}
39
40
41#[derive(SolidityError)]
42pub enum EnglishAuctionError {
43    // Define the errors for the contract.
44    AlreadyInitialized(AlreadyInitialized),
45    AlreadyStarted(AlreadyStarted),
46    NotSeller(NotSeller),
47    AuctionEnded(AuctionEnded),
48    BidTooLow(BidTooLow),
49    NotStarted(NotStarted),
50    NotEnded(NotEnded),
51}
52
53// Define some persistent storage using the Solidity ABI.
54// `Counter` will be the entrypoint.
55sol_storage! {
56    #[entrypoint]
57    pub struct EnglishAuction {
58        address nft_address; // The address of the NFT contract.
59        uint256 nft_id; // The ID of the NFT.
60
61        address seller; // The address of the seller.
62        uint256 end_at; // The end time of the auction.
63        bool started; // The auction has started or not.
64        bool ended; // The auction has ended or not.
65
66        address highest_bidder; // The address of the highest bidder.
67        uint256 highest_bid; // The highest bid.
68        mapping(address => uint256) bids; // The bids of the bidders.
69    }
70}
71
72/// Declare that `Counter` is a contract with the following external methods.
73#[external]
74impl EnglishAuction {
75    pub const ONE_DAY: u64 = 86400; // 1 day = 24 hours * 60 minutes * 60 seconds = 86400 seconds.
76    
77    // Get nft address
78    pub fn nft(&self) -> Result<Address, EnglishAuctionError> {
79        Ok(self.nft_address.get())
80    }
81
82    // Get nft id
83    pub fn nft_id(&self) -> Result<U256, EnglishAuctionError> {
84        Ok(self.nft_id.get())
85    }
86    // Get seller address
87    pub fn seller(&self) -> Result<Address, EnglishAuctionError> {
88        Ok(self.seller.get())
89    }
90
91    // Get end time
92    pub fn end_at(&self) -> Result<U256, EnglishAuctionError> {
93        Ok(self.end_at.get())
94    }
95
96    // Get started status
97    pub fn started(&self) -> Result<bool, EnglishAuctionError> {
98        Ok(self.started.get())
99    }
100
101    // Get ended status
102    pub fn ended(&self) -> Result<bool, EnglishAuctionError> {
103        Ok(self.ended.get())
104    }
105
106    // Get highest bidder address
107    pub fn highest_bidder(&self) -> Result<Address, EnglishAuctionError> {
108        Ok(self.highest_bidder.get())
109    }
110
111    // Get highest bid amount
112    pub fn highest_bid(&self) -> Result<U256, EnglishAuctionError> {
113        Ok(self.highest_bid.get())
114    }
115
116    // Get bid amount of a bidder
117    pub fn bids(&self, bidder: Address) -> Result<U256, EnglishAuctionError> {
118        Ok(self.bids.getter(bidder).get())
119    }
120
121    // Initialize program
122    pub fn initialize(&mut self, nft: Address, nft_id: U256, starting_bid: U256) -> Result<(), EnglishAuctionError> {
123        // Check if the contract has already been initialized.
124        if self.seller.get() != Address::default() {
125            // Return an error if the contract has already been initialized.
126            return Err(EnglishAuctionError::AlreadyInitialized(AlreadyInitialized{}));
127        }
128        
129        // Initialize the contract with the NFT address, the NFT ID, the seller, and the starting bid.
130        self.nft_address.set(nft);
131        self.nft_id.set(nft_id);
132        self.seller.set(msg::sender());
133        self.highest_bid.set(starting_bid);
134        Ok(())
135    }
136
137    pub fn start(&mut self) -> Result<(), EnglishAuctionError> {
138        // Check if the auction has already started.
139        if self.started.get() {
140            return Err(EnglishAuctionError::AlreadyStarted(AlreadyStarted{}));
141        }
142        
143        // Check if the sender is the seller.
144        if self.seller.get() != msg::sender() {
145            // Return an error if the sender is the seller.
146            return Err(EnglishAuctionError::NotSeller(NotSeller{}));
147        }
148        
149        // Create a new instance of the IERC721 interface.
150        let nft = IERC721::new(*self.nft_address);
151        // Get the NFT ID.
152        let nft_id = self.nft_id.get();
153
154        // Transfer the NFT to the contract.
155        let config = Call::new();
156        let result = nft.transfer_from(config, msg::sender(), contract::address(), nft_id);
157        
158        match result {
159            // If the transfer is successful, start the auction.
160            Ok(_) => {
161                self.started.set(true);
162                // Set the end time of the auction to 7 days from now.
163                self.end_at.set(U256::from(block::timestamp() + 7 * Self::ONE_DAY));
164                // Log the start event.
165                evm::log(Start {});
166                Ok(())
167            },
168            // If the transfer fails, return an error.
169            Err(_) => {
170                return Err(EnglishAuctionError::NotSeller(NotSeller{}));
171            }
172            
173        }
174    }
175
176    // The bid method allows bidders to place a bid on the auction.
177    #[payable]
178    pub fn bid(&mut self) -> Result<(), EnglishAuctionError> {
179        // Check if the auction has started.
180        if !self.started.get() {
181            // Return an error if the auction has not started.
182            return Err(EnglishAuctionError::NotSeller(NotSeller{}));
183        }
184        
185        // Check if the auction has ended.
186        if U256::from(block::timestamp()) >= self.end_at.get() {
187            // Return an error if the auction has ended.
188            return Err(EnglishAuctionError::AuctionEnded(AuctionEnded{}));
189        }
190        
191        // Check if the bid amount is higher than the current highest bid.
192        if msg::value() <= self.highest_bid.get() {
193            // Return an error if the bid amount is too low.
194            return Err(EnglishAuctionError::BidTooLow(BidTooLow{}));
195        }
196        
197        // Refund the previous highest bidder. (But will not transfer back at this call, needs bidders to call withdraw() to get back the fund.
198        if self.highest_bidder.get() != Address::default() {
199            let mut bid = self.bids.setter(self.highest_bidder.get());
200            let current_bid = bid.get();
201            bid.set(current_bid + self.highest_bid.get());
202        }
203        
204        // Update the highest bidder and the highest bid.
205        self.highest_bidder.set(msg::sender());
206        self.highest_bid.set(msg::value());
207
208        // Update the bid of the current bidder.
209        evm::log(Bid {
210            sender: msg::sender(),
211            amount: msg::value(),
212        });
213        Ok(())
214    }
215
216    // The withdraw method allows bidders to withdraw their bid.
217    pub fn withdraw(&mut self) -> Result<(), EnglishAuctionError> {
218        // Get the current bid of the bidder.
219        let mut current_bid = self.bids.setter(msg::sender());
220        let bal = current_bid.get();
221        // Set the record of this bidder to 0 and transfer back tokens.
222        current_bid.set(U256::from(0));
223        let _ = transfer_eth(msg::sender(), bal);
224
225        // Log the withdraw event.
226        evm::log(Withdraw {
227            bidder: msg::sender(),
228            amount: bal,
229        });
230        Ok(())
231    }
232
233    // The end method allows the seller to end the auction.
234    pub fn end(&mut self) -> Result<(), EnglishAuctionError> {
235        // Check if the auction has started.
236        if !self.started.get() {
237            // Return an error if the auction has not started.
238            return Err(EnglishAuctionError::NotStarted(NotStarted{}));
239        }
240        
241        // Check if the auction has ended.
242        if U256::from(block::timestamp()) < self.end_at.get() {
243            // Return an error if the auction has not ended.
244            return Err(EnglishAuctionError::NotEnded(NotEnded{}));
245        }
246        
247        // Check if the auction has already ended.
248        if self.ended.get() {
249            // Return an error if the auction has already ended.
250            return Err(EnglishAuctionError::AuctionEnded(AuctionEnded{}));
251        }
252        
253        // End the auction and transfer the NFT and the highest bid to the winner.
254        self.ended.set(true);
255
256        let seller_address = self.seller.get();
257        let highest_bid = self.highest_bid.get();
258        let highest_bidder = self.highest_bidder.get();
259        let nft_id = self.nft_id.get();
260        let config = Call::new();
261        let nft = IERC721::new(*self.nft_address);
262        
263        // Check if there is highest bidder.
264        if self.highest_bidder.get() != Address::default() {
265            // If there is a highest bidder, transfer the NFT to the highest bidder.
266            let _ = nft.safe_transfer_from(config, contract::address(), highest_bidder, nft_id);
267            // Transfer the highest bid to the seller.
268            let _ = transfer_eth(seller_address, highest_bid);
269        } else {
270            // If there is no highest bidder, transfer the NFT back to the seller.
271            let _ = nft.safe_transfer_from(config, contract::address(), seller_address, nft_id);
272        }
273
274        // Log the end event.
275        evm::log(End {
276            winner: highest_bidder,
277            amount: highest_bid,
278        });
279        Ok(())
280    }
281}

Cargo.toml

1[package]
2name = "stylus-auction-example"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "0.3.1"
8alloy-sol-types = "0.3.1"
9mini-alloc = "0.4.2"
10stylus-sdk = { version = "0.5.0", features = ["docs"] }
11hex = "0.4.3"
12
13[features]
14export-abi = ["stylus-sdk/export-abi"]
15debug = ["stylus-sdk/debug"]
16
17[lib]
18crate-type = ["lib", "cdylib"]
19
20[profile.release]
21codegen-units = 1
22strip = true
23lto = true
24panic = "abort"
25opt-level = "s"
1[package]
2name = "stylus-auction-example"
3version = "0.1.0"
4edition = "2021"
5
6[dependencies]
7alloy-primitives = "0.3.1"
8alloy-sol-types = "0.3.1"
9mini-alloc = "0.4.2"
10stylus-sdk = { version = "0.5.0", features = ["docs"] }
11hex = "0.4.3"
12
13[features]
14export-abi = ["stylus-sdk/export-abi"]
15debug = ["stylus-sdk/debug"]
16
17[lib]
18crate-type = ["lib", "cdylib"]
19
20[profile.release]
21codegen-units = 1
22strip = true
23lto = true
24panic = "abort"
25opt-level = "s"