English Auction is a type of auction in which bidders openly compete against each other, with each subsequent bid being higher than the previous bid. This style of auction is also known as an ascending-price auction.
A smart contract is a self-executing contract that is capable of enforcing the terms of an agreement. In the context of an English Auction, a smart contract can be used to govern the bidding process and the transfer of ownership of an item to the highest bidder, in our case it's an NFT.
In this article, you will learn how to implement an NFT English auction smart contract on the Celo blockchain network. Finally, we will demonstrate how our English smart contract works.
Before you get started with this tutorial, make sure you have a basic understanding of the following:
-
Experience using Remix IDE
To successfully build this smart contract, your computer should have the following tools:
-
A chromium-based web browser
-
An internet connection
-
A Metamask account with Alfajores CELO tokens
-
A Celo extension wallet account with Alfajores CELO tokens
Navigate to The Remix IDE or any other IDE compatible with the Solidity programming language. Then Follow the steps below to create an English auction smart contract.
In the Remix IDE, create a new file. Name it EnglishAuction.sol
. This will contain our smart contract code. Open the file.
Copy the code below, and paste it into the opened EnglishAuction.sol
file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
interface IERC721 {
function safeTransferFrom(address from, address to, uint tokenId) external;
function transferFrom(address, address, uint) external;
}
At the top level of our contract, we have a comment that specifies the license the contract uses. The SPDX license identifier is a standard format that communicates licensing information about a software package.
Next, we defined the version of the Solidity compiler that should be used to compile our smart contract code. In our case, we specified version 0.8.17
.
Next, we declared the IERC721 interface. IERC721 are standards that provide a way to interact with non-fungible tokens (NFTs). The IERC721 interface ensures that contracts and applications that implement the IERC721 interface are compatible with one another. In other words, these contracts and applications can easily transfer NFTs between one another.
Add the code below to EnglishAuction.sol
file.
contract EnglishAuction {
event Start();
event Bid(address indexed sender, uint amount);
event Withdraw(address indexed bidder, uint amount);
event End(address winner, uint amount);
IERC721 public nft;
uint public nftId;
address payable public auctionSeller;
uint public endEnglishAuction;
bool public startedEnglishAuction;
bool public endedEnglishAuction;
address public auctionHighestBidder;
uint public auctionHighestBid;
mapping(address => uint) public auctionTotalBids;
}
The contract defines four events that can be emitted (i.e., triggered) during the execution of the contract. These events are:
-
Start()
-
Bid()
-
Withdraw()
-
End()
.
Events are used to provide feedback to the users of the contract when certain actions are taken or conditions are met.
Next, we have two public variables that define the NFT to be sold in the auction, these public variables are:
-
IERC721 public nft
- This is a public variable that stores the NFT to be auctioned. -
uint public nftId
- This is another public integer variable. It stores the unique token ID of the non-fungible token to be auctioned in this smart contract.
Next, we declared some variables involving the bids. These include:
-
address payable public auctionSeller
: This is a public variable of typeaddress payable
, which stores the address of the seller who created the auction. Thepayable
keyword allows this address to receive funds. -
uint public endEnglishAuction
: Another public variable of typeuint
, which stores the timestamp when the auction will end. -
bool public startedEnglishAuction
: This public boolean variable keeps track of whether the auction has started or not. Its value istrue
if the auction has started, andfalse
otherwise. -
bool public endedEnglishAuction
: This public boolean variable keeps track of whether the auction has ended or not. Its value istrue
if the auction has ended, andfalse
otherwise. -
address public auctionHighestBidder
: This public variable of typeaddress
stores the address of the highest bidder in the auction. -
uint public auctionHighestBid
: Another public variable of typeuint
, which stores the highest bid amount made in the auction so far. -
mapping(address => uint) public auctionTotalBids
: This is a public mapping that maps addresses to their respective bid amounts. Whenever a bidder makes a new bid, theaddress => uint
pair is added to the mapping. Whereaddress
is the address of the bidder anduint
is the amount they bid.
Add the code at the end of EnglishAuction
contract.
constructor(address _nft, uint _nftId, uint _auctionStartingBid) {
nft = IERC721(_nft);
nftId = _nftId;
auctionSeller = payable(msg.sender);
auctionHighestBid = _auctionStartingBid;
}
The above code is a constructor function. It initializes some of our variables during the deployment of our smart contract. Below is a proper breakdown of the code above:
-
The constructor takes three parameters:
-
The
_nft
parameter is an NFT contract address that represents an instance of the ERC-721 token contract. -
The
_nftId
parameter is an unsigned integer that represents the specific token ID of the NFT being auctioned. -
The
_auctionStartingBid
parameter is an unsigned integer that represents the starting bid for the auction.
-
-
The
auctionSeller
variable is a storage variable that stores the address of the user starting the auction. -
The
payable
keyword indicates that theauctionSeller
address can receive Celo. -
The
auctionHighestBid
variable is a storage variable that stores the starting bid for the auction. The value ofauctionHighestBid
changes as higher bids are made.
Add the code at the end of EnglishAuction
contract.
function startEnglishAuction() external {
require(!startedEnglishAuction, "started");
require(msg.sender == auctionSeller, "not auctionSeller");
nft.transferFrom(msg.sender, address(this), nftId);
startedEnglishAuction = true;
endEnglishAuction = uint32(block.timestamp + 2 days);
emit Start();
}
The startEnglishAuction
function starts an auction when called. In the startEnglishAuction
function, we have the following code:
-
require(!startedEnglishAuction, "started")
- This checks whether the auction has already been started. If it has, the function throws an error with the message "started". -
require(msg.sender == auctionSeller, "not auctionSeller")
- This checks whether the caller of the function is the same as theauctionSeller
(the user that initiated the auction). If the caller is not theauctionSeller
, the function throws an error with the message "not auctionSeller". -
nft.transferFrom(msg.sender, address(this), nftId)
- If the function has not thrown any errors, this transfers ownership of the token from theauctionSeller
to the auction contract. -
startedEnglishAuction = true
- ThestartedEnglishAuction
variable is set totrue
to indicate that the auction has been started. -
endEnglishAuction = block.timestamp + 2 days
- TheendEnglishAuction
variable is set to the current time (as represented byblock.timestamp
) plus two days, indicating the time the auction ends. -
emit Start()
- anemit
statement is used to trigger theStart
event, indicating that the auction has been started.
Add the code at the end of EnglishAuction
contract.
function bidAmount() external payable {
require(msg.sender != auctionSeller, "auction seller");
require(startedEnglishAuction, "not started");
require(block.timestamp < endEnglishAuction, "ended");
require(msg.value > auctionHighestBid, "value < highest");
if (auctionHighestBidder != address(0)) {
auctionTotalBids[auctionHighestBidder] += auctionHighestBid;
}
auctionHighestBidder = msg.sender;
auctionHighestBid = msg.value;
emit Bid(msg.sender, msg.value);
}
The bidAmount
function allows potential bidders to bid once the auction starts. In the bidAmount
function, we have the following code:
-
require(startedEnglishAuction, "not started")
- checks whether the auction has started. If it hasn't started, the function throws an error with the message "not started". -
require(block.timestamp < endEnglishAuction, "ended")
- checks whether the auction has ended. If it has ended, the function throws an error with the message "ended". -
require(msg.value > auctionHighestBid, "value < highest")
- checks whether the new bid is greater than the current highest bid (auctionHighestBid
). If it's not greater, the function throws an error with the message "value < highest". -
if (auctionHighestBidder != address(0)) { -auctionTotalBids[auctionHighestBidder] += auctionHighestBid; }
- If the current highest bidder is not the contract owner (meaning that there has already been a bid), theauctionTotalBids
mapping is updated to reflect the new total for that bidder (adding the previous highest bid value). -
auctionHighestBidder = msg.sender
- The current highest bidder (auctionHighestBidder
) is updated to be the address of the caller (msg.sender
). -
auctionHighestBid = msg.value
- TheauctionHighestBid
is set to the new highest bid (msg.value
). -
emit Bid(msg.sender, msg.value)
- Theemit
statement triggers theBid
event, indicating that a new bid has been made and includes the new highest bidder's address and the amount they bid.
Add the code at the end of EnglishAuction
contract.
function withdrawBids() external {
uint balance = auctionTotalBids[msg.sender];
auctionTotalBids[msg.sender] = 0;
payable(msg.sender).transfer(balance);
emit Withdraw(msg.sender, balance);
}
The withdrawBids
function allows bidders who were not the highest bidders to withdraw their bids. In the withdrawBids
function, we have the following code:
-
uint balance = auctionTotalBids[msg.sender]
- This retrieves the total bid amount of the current caller (i.e.,msg.sender
) from theauctionTotalBids
mapping and stores it in a local variable calledbalance
. -
auctionTotalBids[msg.sender] = 0
- This sets the total bid amount of the caller to zero, effectively withdrawing their bid from the auction. -
payable(msg.sender).transfer(balance)
- This uses thepayable()
function to transfer the right amount to the caller's address. -
emit Withdraw(msg.sender, balance)
- Finally, the function emits aWithdraw
event that notifies the user that their bid has been withdrawn by passing inmsg.sender
andbalance
as parameters.
Add the code at the end of EnglishAuction
contract.
function endAuction() external {
require(startedEnglishAuction, "not started");
require(block.timestamp >= endEnglishAuction, "not ended");
require(!endedEnglishAuction, "ended");
endedEnglishAuction = true;
if (auctionHighestBidder != address(0)) {
nft.safeTransferFrom(address(this), auctionHighestBidder, nftId);
auctionSeller.transfer(auctionHighestBid);
} else {
nft.safeTransferFrom(address(this), auctionSeller, nftId);
}
emit End(auctionHighestBidder, auctionHighestBid);
}
The endAuction
function allows auction seller to end the auction. In the endAuction
function, we have the following code:
-
require(startedEnglishAuction, "not started")
- This checks if the English auction has started, and if it has not, it throws an error using therequire()
function with the message "not started". -
require(block.timestamp >= endEnglishAuction, "not ended")
- This checks if the current block's timestamp is greater than or equal to the ending time of the English auction. If the auction has not yet ended, it throws an error with the message "not ended". -
require(!endedEnglishAuction, "ended")
- This checks whether the auction has already ended by checking theendedEnglishAuction
variable. If the auction has already ended, the function throws an error with the message "ended". -
endedEnglishAuction = true
- If none of the above conditions triggers arequire()
error, the function sets theendedEnglishAuction
variable totrue
, effectively marking the end of the English auction. -
if (auctionHighestBidder != address(0)) { nft.safeTransferFrom(address(this), auctionHighestBidder, nftId); auctionSeller.transfer(auctionHighestBid); }
- If there was at least one valid bid on the auction, the function transfers the NFT token withnftId
from the contract to theaddress
of the highest bidder using thesafeTransferFrom()
function. It also transfers the highest bid amount to theauctionSeller
address using thetransfer()
function. -
else { nft.safeTransferFrom(address(this), auctionSeller, nftId); }
- If there was no valid bid on the auction, the function transfers the NFT token back to theauctionSeller
address using thesafeTransferFrom()
function. -
Finally, the function emits an
End
event with theauctionHighestBidder
andauctionHighestBid
as parameters, which notifies the users who participated in the auction about the result.
Now, we have created an English Auction smart contract, your smart contract should look like the one below.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
interface IERC721 {
function safeTransferFrom(address from, address to, uint tokenId) external;
function transferFrom(address, address, uint) external;
}
contract EnglishAuction {
event Start();
event Bid(address indexed sender, uint amount);
event Withdraw(address indexed bidder, uint amount);
event End(address winner, uint amount);
IERC721 public nft;
uint public nftId;
address payable public auctionSeller;
uint public endEnglishAuction;
bool public startedEnglishAuction;
bool public endedEnglishAuction;
address public auctionHighestBidder;
uint public auctionHighestBid;
mapping(address => uint) public auctionTotalBids;
constructor(address _nft, uint _nftId, uint _auctionStartingBid) {
nft = IERC721(_nft);
nftId = _nftId;
auctionSeller = payable(msg.sender);
auctionHighestBid = _auctionStartingBid;
}
function startEnglishAuction() external {
require(!startedEnglishAuction, "started");
require(msg.sender == auctionSeller, "not auctionSeller");
nft.transferFrom(msg.sender, address(this), nftId);
startedEnglishAuction = true;
endEnglishAuction = uint32(block.timestamp + 2 days);
emit Start();
}
function bidAmount() external payable {
require(msg.sender != auctionSeller, "auction seller");
require(startedEnglishAuction, "not started");
require(block.timestamp < endEnglishAuction, "ended");
require(msg.value > auctionHighestBid, "value < highest");
if (auctionHighestBidder != address(0)) {
auctionTotalBids[auctionHighestBidder] += auctionHighestBid;
}
auctionHighestBidder = msg.sender;
auctionHighestBid = msg.value;
emit Bid(msg.sender, msg.value);
}
function withdrawBids() external {
uint balance = auctionTotalBids[msg.sender];
auctionTotalBids[msg.sender] = 0;
payable(msg.sender).transfer(balance);
emit Withdraw(msg.sender, balance);
}
function endAuction() external {
require(startedEnglishAuction, "not started");
require(block.timestamp >= endEnglishAuction, "not ended");
require(!endedEnglishAuction, "ended");
endedEnglishAuction = true;
if (auctionHighestBidder != address(0)) {
nft.safeTransferFrom(address(this), auctionHighestBidder, nftId);
auctionSeller.transfer(auctionHighestBid);
} else {
nft.safeTransferFrom(address(this), auctionSeller, nftId);
}
emit End(auctionHighestBidder, auctionHighestBid);
}
}
Now that we have implemented an English Auction smart contract, let's deploy this smart contract on the Celo Alfajores network. But before we can deploy our English auction smart contract we need to deploy and mint an NFT.
If you don't know how to build and deploy one, go through this tutorial on how to deploy and mint NFTs on the Celo Alfajores network.
After you are done deploying the contract, mint an NFT by calling and passing the necessary parameter on the safeMint or mint function.
Follow the steps below to deploy your English auction on the Celo Alfajores network.
-
Activate the Remix IDE Celo plugin.
-
Click on the Celo plugin icon.
-
Click on connect button to connect the Celo extension wallet.
-
Click on the compile button to compile the smart contract.
-
Enter and fill in the details of the parameters provided. These include:
-
_nft
: This is the contract address of the NFT you want to auction. -
_nftId
: This is a unique identifier or id for the NFT you want to auction. -
__auctionStartingBid
: This is the minimum bid amount that a bidder must place to participate in the auction. Adjusted according to your preference.
-
-
Click on the deploy button to deploy the smart contract.
-
Confirm the completion of the smart contract in the Celo wallet extension.
-
Wait for the contract to be deployed.
-
Take note of the generated contract address and Contract Application Binary Interface (ABI) as we will need it to test our smart contract on Laika.
In this step, you are to approve the transfer of ownership of the NFT minted to the English auction smart contract by following the steps below:
-
Navigate to the deployed
NFT smart contract
. -
Click on the approve function.
-
Enter the address of your English auction smart contract and the NFT's id.
-
Call and approve the transaction.
Laika is a tool for quick interaction with smart contracts. Follow the steps below to test some of the functions of our smart contract.
-
Navigate to Laika in your browser.
-
Click on the connect button to connect your Metamask wallet. Use the same Celo address used in deploying the English auction smart contract.
-
Set the Metamask wallet network to Celo Alfajores.
-
Click on the new requests button close to the collections tab.
-
Click on the new request button on the interface displayed.
-
Copy and paste the smart contract ABI and contract address of the English auction smart contract from Remix IDE into the provided field.
-
Click on the import button.
If the steps are done properly you will see a New Contract folder under collections. Inside the New Contract folder are our smart contract functions.
Let's go ahead to test these functions. You will receive a response for each function you call, these responses can help us debug our code.
-
Click on the
startEnglishAuction
function. -
Click on the send button close to the contract address input field.
-
Confirm the transaction in your Metamask wallet.
-
After doing this you will have two(2) days for the auction bid.
-
Click on the
bidAmount
function. -
Enter an amount higher than the auction starting bid (in CELO) in the input field beside the send button.
-
Click on the send button close to the contract address input field.
-
Confirm the transaction in your Metamask wallet.
-
Click on the
auctionHighestBidder
function. -
Click on the send button close to the contract address input field to get the auction's highest bidder.
-
Confirm the transaction in your Metamask wallet.
You can continually switch wallet accounts to make a bid higher the the previous bids.
Accounts that have been outbid can withdraw their bids with the withdrawBids
function. Test this by following the steps below:
-
Click on the
withdrawBids
function. -
Click on the send button close to the contract address input field to end the auction.
-
Confirm the transaction in your Metamask wallet
After 2 days you can end the auction by following the steps below:
-
Click on the
endAuction
function. -
Click on the send button close to the contract address input field to end the auction.
-
Confirm the transaction in your Metamask wallet
Feel free to try out other functions if you'd like.
This tutorial covered the basics of building an English auction smart contract. You can improve the smart contract by adding other functionalities and building the front-end dApp.
Happy coding!