How to build an NFT Auction Smart Contract

How to build an NFT Auction Smart Contract

Using Solidity.

In this smart contract tutorial, I am going to show you how to build an English NFT Auction Smart Contract.

Prerequisites

Basic JavaScript skills.

Basic smart contract skills.

Basic NFT knowledge.

Basic understanding of how Auctions work.

Basic knowledge of how RPCs work.

Basic understanding of Blockchain Testnets.

Getting started.

The English Auction Smart Contract that we're about to build is an upgraded version of the English Auction Smart Contract found here, but with more functions and variables that I built into it.

These new functions include:

  • Registering users as sellers and granting them executive control over certain functions.

  • Transfer ownership of the contract.

  • Start and Close the contract/application.

  • Self-destruct the contract's bytecode.

What's the difference between an NFT Market Place and An NFT Auction?

Although they are both used to sell and purchase NFTs, an NFT Market Place is different from an NFT Auction in several ways.

  1. An NFT Auction has logic encoded into it that increments the worth of every NFT every time a bid is placed on it.

  2. An NFT auction contract is a more interactive contract compared to the smart contract of an NFT Marketplace.

  3. Auctions are meant to be fast and so an NFT Auction has a limited time range to be sold to the highest bidder. This can increase the worth of the NFT as bidders race to outbid each other before the time runs out.

The Thought Process

Before creating any type of smart contract there are a few things a smart contract developer must do, and that is to create a thought process.

As a developer before getting to write code you must first think of the structure of the code. This is how you form a thought process that helps you visualize the code with all its features, and with that visual in mind, you can start writing your code.

To build an auction smart contract, a few important variables must be present.

  1. A struct to hold the following key variables:

    • The address of the seller.

    • The address of the highest bidder.

    • The highest bid.

    • The address of the NFT.

    • The ID of the NFT (needed for transferring the NFT).

    • The expiration period of the auction item (NFT).

    • A boolean to declare the start of the sale of the NFT.

    • Another boolean to mark the NFT as sold at the end of its expiration period.

        // Struct to hold variables    
        struct AuctionItem {
                address payable seller; // seller of item
                address highestBidder; // highest bidder
                uint highestBid; // highest bid
                address nft; //  address of NFT
                uint nftId; // NFT id
                uint endAt; // expiration period on item 
                bool started; // auction started
                bool sold;  // item sold
            }
      
  2. A mapping to keep track of the total bids of the bidders.

     mapping(address => uint) public bids;
    
  3. A variable to store the total items for auction.

     uint public totalItems = 0;
    
  4. Booleans to open and close the application.

     bool public appStarted; // starts the application
     bool public appClosed; // closes the application
    
  5. A variable to hold the TAX_FEE: sellers are required to pay this tax fee for registering their item for sale in our smart contract. Think of it as a commission.

     uint public constant TAX_FEE = 10 * 1e5;
    
  6. Modifiers: Our code is going to need a few modifiers to limit access control between sellers, and bidders.

     modifier onlyOwner {
             if(msg.sender != owner)
                 revert Auction__NotOwner();
             _;
         }
    
         modifier auctionExists(uint _auctionId) {
         if(_auctionId > auctionItems.length)
             revert Auction__ItemNonExistent();
             _;
         }
    
         modifier open {
             if(appStarted != true) 
                 revert Auction__AppNotStarted();
             _;
         }
    
         modifier onlySeller(uint _auctionId) {
             AuctionItem storage auction = auctionItems[_auctionId];
             if(msg.sender != auction.seller) 
                 revert Auction__NotSeller();
             _;
         }
    
    • onlyOwner: This modifier limits access control to only the owner of the contract. This is usually defined in the constructor as the deployer of the contract but can be transferred through several ways. Once we start writing the code we'll create a function to implement this transfer from the deployer to the desired owner.

    • auctionExists: This modifier ensures that the item being called is an auction that exists in the array of auctions otherwise it reverts with the custom error Auction__ItemNonExistent.

    • open: This modifier ensures that the application has been started by the owner, otherwise it prevents users from calling any of the functions in the smart contract.

    • onlySeller: This modifier limits access control to only users who have paid the TAX_FEE and registered to be sellers.

  7. Error messages: As long as there is an action to be carried out and the possibility of that action being reverted if conditions aren't met then there is a need to save error messages to define those reverts. Errors messages are usually written at the top of the script before the contract definition.

     error Auction__AppNotStarted();
     error Auction__NotStarted();
     error Auction__SaleOver();
     error Auction__ItemSold();
     error Auction__NotOwner();
     error Auction__NoBalance();
     error Auction__NotSeller();
     error Auction__ItemNonExistent();
    

    The error messages listed above are reactions to every revert action existing in the auction NFT smart contract.

    NOTE: error Auction__AppNotStarted() is very much different from error Auction__NotStarted().

    error Auction__AppNotStarted() is the revert message displayed when a function is called without the owner of the contract/application calling the startApp() function that declares the application as 'started'.

    Auction__NotStarted() is the error message displayed when a bidder tries to bid on an NFT in the auction before the seller declares the auction as 'started' using the boolean auction.started = true

  8. Events: Events are used to log when an action happens on the blockchain. Whenever an important action is carried out in our smart contract we're going to log that action as an event and store it on chain.

         /* --> EVENTS <-- */
         event AuctionOpen(address indexed owner);
         event ItemCreated(address indexed seller, uint timestamp, uint _auctionId);
         event AuctionStarted(address indexed seller, uint _auctionId);
         event ItemBidIncreased(address indexed sender, uint bid);
         event BalanceClaimed(address indexed sender, uint bal, uint timestamp);
         event ItemSold(address winner, uint amount, uint timestamp);
         event AuctionClosed(address indexed owner);
    
  9. Interfaces: This contract will be importing two important features of an ERC721 that will permit the contract to transfer the NFTs to the highest bidders at the end of the auction.

     import "./IERC721.sol";
    

The imported code comes from an already-written smart contract here and this import allows the contract to call the safeTransferFrom and transferFrom functions found in the standard Open Zeppelin ERC721.

What's Next?

Before we get to write the code, you would want to set up your programming environment if you haven't yet.

I use VS-Code, but there are other great code editors out there like Atom.

If you want to code along with me using VS-Code, follow these links to install it on your Mac or Windows.

Once installed, create a new folder and save it as NFTAuctionContract or anything you would prefer. You can create a new folder either from your terminal using mkdir NFTAuctionContract

# create new folder
mkdir NFTAuctionContract
# change directory into folder
cd NFTAuctionContract

or through VS-Code directly. If you're not that familiar with your control terminal then creating the folder through VS-Code would be the best option for you.

Setting up Hardhat

Before we can write code we need to set up an environment. Follow this link to learn how to setup hardhat.

Hardhat is an Ethereum development environment used for developing, testing and deploying smart contracts. There are other Ethereum development environments out there such as Foundry, and Truffle, but Hardhat is the most popular environment with an active community that maintains it.

Read the Hardhat documentation here. If you run into any problems while developing with Hardhat, refer back to the link for a possible solution.

Creating The Contracts

Locate the contract folder that appeared once Hardhat was done installing and create two (2) new .sol files

  • IERC721.sol: where we'll code our open zeppelin interface and

  • NFTAuctionContract.sol: where we'll code the logic for the smart contract.

The IERC721.sol interface

Inside the IERC721.sol file we just created, copy and paste the interface below

// IERC721.sol
//SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

/* --> Interface <-- */
interface IERC721 {
    function safeTransferFrom(
        address sender,
        address nft,
        uint nftId
    ) external;

    function transferFrom(
        address,
        address,
        uint 
    ) external;
}

This interface contains the safeTransferFrom and transferFrom functions that will transfer the NFTs from the contract to the highest bidders at the end of the auction.

The NFTAuctionContract.sol

Now let's build the main contract from scratch to completion in the NFTAuctionContract.sol.

License, Pragma, Interface, Errors

We start by defining the License, in which case I use MIT which authorizes the copy, use, and modification of this software.

The next line defines the version of solidity we're using. The code we're using is in solidity version 0.8.10, which means it will compile with version 0.8.10 compilers.

We import the IERC721.sol contract into the NFTAuctionContract.sol on the next line and define the error messages next.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

/* --> INTERFACE <-- */
import "./IERC721.sol";

/* --> ERRORS <-- */
error Auction__AppNotStarted();
error Auction__NotStarted();
error Auction__SaleOver();
error Auction__ItemSold();
error Auction__NotOwner();
error Auction__NoBalance();
error Auction__NotSeller();
error Auction__ItemNonExistent();

The Contract: State variables, modifiers and functions

Now we can start working on the contract. First, define the state variable, events, and modifiers by copying and pasting the snippet below.

contract Auction {

    /* --> STATE VARIABLES <-- */
    address public owner; 
    uint public totalItems = 0; // Amount of items created for auction
    uint public constant TAX_FEE = 1e5; // fee for registration

    // for starting application ! auction
    bool public appStarted;
    bool public appClosed;

    mapping(address => uint) public bids;

    struct AuctionItem {
        address payable seller; // seller of item
        address highestBidder; // highest bidder
        uint highestBid; // highest bid
        address nft; //  address of NFT
        uint nftId; // NFT id
        uint endAt; // expiration period on item 
        bool started; // auction started = true
        bool sold;  // item sold = true
    }
    AuctionItem[] public auctionItems;

    /* --> EVENTS <-- */
    event AuctionOpen(address indexed owner);
    event ItemCreated(address indexed seller, uint timestamp, uint _auctionId);
    event AuctionStarted(address indexed seller, uint _auctionId);
    event ItemBidIncreased(address indexed sender, uint bid);
    event BalanceClaimed(address indexed sender, uint bal, uint timestamp);
    event ItemSold(address winner, uint amount, uint timestamp);
    event AuctionClosed(address indexed owner);

    /* --> MODIFIERS <-- */
    modifier onlyOwner {
        if(msg.sender != owner)
            revert Auction__NotOwner();
        _;
    }

    modifier auctionExists(uint _auctionId) {
    if(_auctionId > auctionItems.length)
        revert Auction__ItemNonExistent();
        _;
    }

    modifier open {
        if(appStarted != true) 
            revert Auction__AppNotStarted();
        _;
    }

    modifier onlySeller(uint _auctionId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        if(msg.sender != auction.seller) 
            revert Auction__NotSeller();
        _;
    }

Constructor

Constructors are used optional functions used to initialize state variables of a contract.

We're going to use a simple constructor that defines the owner of the contract at deployment.

    constructor() {
        owner = payable(msg.sender);
    }

Functions

Important note: It's always best practice to arrange public functions above, then the external functions followed by the internal functions and lastly private functions at the bottom (if any).

Public Functions:

    function startApp() public onlyOwner {
        appStarted = true;
        // emit event
        emit AuctionOpen(msg.sender);
    }

    function register(address _nft, uint _nftId, uint highestBid, address payable seller) public payable open {
        require(msg.value >= TAX_FEE, "warning: insufficient registration funds");
        auctionItems.push(AuctionItem({
            seller: payable(seller),
            nft: _nft,
            nftId: _nftId,
            highestBidder: address(0),
            highestBid: highestBid,
            endAt: block.timestamp + 7 days,
            started: false,
            sold: false
        }));
        totalItems += 1;
        IERC721(_nft).transferFrom(seller, address(this), _nftId);
        // emit event
        emit ItemCreated(msg.sender, block.timestamp, totalItems+1);
    }

    function startAuction(uint _auctionId) public auctionExists(_auctionId) onlySeller(_auctionId) open {
        AuctionItem storage auction = auctionItems[_auctionId];
        require(auction.sold != true, "Item sold");
        auction.started = true;
        // emit event
        emit AuctionStarted(_auctionId);
    }

    function bid(uint _auctionId) public auctionExists(_auctionId) payable open returns (bool)  {
        AuctionItem storage auction = auctionItems[_auctionId];
        if(!auction.started)
            revert Auction__NotStarted();
        if(auction.sold)
            revert Auction__ItemSold();
        if(block.timestamp >= auction.endAt)
            revert Auction__SaleOver();
        require(msg.value > auction.highestBid, "Bid higher");
        auction.highestBidder = msg.sender;
        auction.highestBid = msg.value;
        if(auction.highestBidder != address(0)) {
            bids[auction.highestBidder] += auction.highestBid;
        }
        // emit event
        emit ItemBidIncreased(msg.sender, msg.value);
        return true;
    }
  • startApp(): This function has a modifier of onlyOwner which means it can only be called by whoever is the owner of the contract/application.

    The AuctionStarted event is emitted when the owner calls this function, and the application is declared as started, functions can now be called on by users.

  • register(): Users call this function to register their NFTs in the application for auctioning. Once the users pay the TAX_FEE they become sellers and now have access to some extra functions in the smart contract.

    It emits the event ItemCreated(msg.sender, block.timestamp, totalItems+1) which logs the msg.sender, timestamp as block.timestamp and totalItems for sale in the contract.

    Every auction lasts for at least 7 days before expiring, after the due date there can be no more bidding on the NFT and the seller is required to call another function that transfers the NFT to the highest bidder.

  • startAuction(). Whenever a seller is ready to start the auction on their NFT, this is the function they call. It is limited to only addresses saved as sellers and would revert if called by the average user. This function emits the auction ID AuctionStarted(uint indexed _auctionId).

  • bid() users call this function to bid on the NFT for sale. The user has to stake a higher bid every time he calls this function to pass the require statement require(msg.value > auction.highestBid, "Bid higher").

    The function emits the event BidIncreased and logs the msg.sender (bidder) and msg.value (bid).

External Functions:

We're done writing our public functions, next let us write the next set of functions that can be called from outside the contract.

function claimBalance(uint _auctionId) external auctionExists(_auctionId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        uint bal = bids[msg.sender];
        bids[msg.sender] = 0;
        if(msg.sender != auction.highestBidder) {
            payable(msg.sender).transfer(bal);
        } else {
        revert Auction__NoBalance();
        }
        // emit event
        emit BalanceClaimed(msg.sender, bal, block.timestamp);
    }

    function transferItem(address nft, uint nftId, uint _auctionId) external onlySeller(_auctionId) open auctionExists(_auctionId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        require(block.timestamp >= auction.endAt, "warning: Auction not due");
        auction.sold = true;
        if(auction.highestBidder != address(0)) {
            IERC721(nft).safeTransferFrom(address(this), auction.highestBidder, nftId);
        auction.seller.transfer(auction.highestBid);
        } else {
            // transfer item back to seller
            IERC721(nft).safeTransferFrom(address(this), auction.seller, nftId);
        }
        // emit event
        emit ItemSold(auction.highestBidder, auction.highestBid, block.timestamp);
    }

    /**
    * @dev function transfers ownership for repossesion of contract.
    */
    function transferOwnership(address payable newOwner) external {
        require(!appStarted, "warning: app already started");
        require(newOwner != address(0), "invalid address");
        owner = payable(newOwner);
    }

    function closeApplication() external onlyOwner {
        require(address(this).balance == 0, "warning: Funds still in application");
        appClosed = true;
        selfdestruct(payable (owner));
        emit AuctionClosed(msg.sender);
    }

claimBalance: When users bid a higher amount of eth, they lock that eth in the contract to be saved as the new highestBid but if they get outbid they call this function to get a refund on their eth, so they can bid higher and try to outbid the user that just outbid them. The function emits the event BalanceClaimed(address indexed bidder, uint bal which logs the user and the balance claimed.

transferItem: This function can only be called by the seller after the expiration time set for the auction of the auction item. It transfers the NFT to the highest bidder by calling the safeTransferFrom imported into the contract from the IERC721.sol contract. It emits an event ItemSold(address winner, uint amount, uint timestamp) which logs the address of the highest bidder, the highest bid, and the timestamp.

transferOwnership: This function is the only way to transfer ownership from the developer that deployed the contract (initial owner) to a new owner of the application. It uses a require statement to ensure that it is only called before the startApp() function has been called to ensure a smooth exchange of ownership that wouldn't affect the auctions.

closeApplication: This function closes the application and calls the selfdestruct function in Solidity which erases the bytecode and prevents any further function call on the contract. To prevent malicious use of this function, it can only be called if there is no ether trapped in the contract that belonged to a user.

Getter Functions

Getter functions are functions that return a value. Creating getter functions help in making it easier to retrieve information from the contract.

Let's create a few getters for the NFTAuctionContract.sol

    function getHighestBid(uint _auctionId) public 
    view
    returns (uint highestBid) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.highestBid);
    }

    function getHighestBidder(uint _auctionId) public view returns (address highestBidder)
    {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.highestBidder);
    }

    function getAuctionItemState(uint _auctionId) public view returns (bool started, uint endAt, bool sold) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.started, auction.endAt, auction.sold);
    }

    function getSeller(uint _auctionId) public view returns (address seller) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.seller);
    }

    function getNftId(uint _auctionId) public view returns (uint nftId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.nftId);
    }

    function getAuctionItems() public view returns (AuctionItem[] memory) {
        return auctionItems;
    }

    function getItemInfo(uint _auctionId) public view returns (AuctionItem memory) {
        return auctionItems[_auctionId - 1];
    }

getHighestBid: Returns the current highest bid.

getHighestBidder: Returns the highest bidder.

getAuctionItemState: Returns the state of an auction item. It answers the user's question has it started? when does it end? and has it been sold?

getSeller: Returns the address of the seller.

getNftId: Returns the ID of the NFT.

getAuctionItems: Returns the items saved in the array of auctions.

Congratulations!

We have successfully built an English NFT Auction capable of holding and selling multiple NFTs at once.

On your terminal, compile the contract to make sure there are no errors and bugs by typing:

npx hardhat compile or yarn compile

This will compile all .sol files in the hardhat environment and would return successfully if no bugs are found.

Successfully compiled contracts.

Full Code:

// NrFTAuctionContract.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

/* --> INTERFACE <-- */
import "./IERC721.sol";

/* --> ERRORS <-- */
error Auction__AppNotStarted();
error Auction__NotStarted();
error Auction__SaleOver();
error Auction__ItemSold();
error Auction__NotOwner();
error Auction__NoBalance();
error Auction__NotSeller();
error Auction__ItemNonExistent();

contract NFTAuctionContract {

    /* --> STATE VARIABLES <-- */
    address public owner; 
    uint public totalItems = 0; // Amount of items created for auction
    uint public constant TAX_FEE = 1e5; // fee for registration

    // for starting application ! auction
    bool public appStarted;
    bool public appClosed;

    mapping(address => uint) public bids;

    struct AuctionItem {
        address payable seller; // seller of item
        address highestBidder; // highest bidder
        uint highestBid; // highest bid
        address nft; //  address of NFT
        uint nftId; // NFT id
        uint endAt; // expiration period on item 
        bool started; // auction started = true
        bool sold;  // item sold = true
    }
    AuctionItem[] public auctionItems;

    /* --> EVENTS <-- */
    event AuctionOpen(address indexed owner);
    event ItemCreated(address indexed seller, uint timestamp, uint _auctionId);
    event AuctionStarted(address indexed seller, uint _auctionId);
    event ItemBidIncreased(address indexed sender, uint bid);
    event BalanceClaimed(address indexed sender, uint bal, uint timestamp);
    event ItemSold(address winner, uint amount, uint timestamp);
    event AuctionClosed(address indexed owner);

    /* --> MODIFIERS <-- */
    modifier onlyOwner {
        if(msg.sender != owner)
            revert Auction__NotOwner();
        _;
    }

    modifier auctionExists(uint _auctionId) {
    if(_auctionId > auctionItems.length)
        revert Auction__ItemNonExistent();
        _;
    }

    modifier open {
        if(appStarted != true) 
            revert Auction__AppNotStarted();
        _;
    }

    modifier onlySeller(uint _auctionId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        if(msg.sender != auction.seller) 
            revert Auction__NotSeller();
        _;
    }

    /* --> CONSTRUCTOR <-- */
    constructor() {
        owner = payable(msg.sender);
    }

    /* --> PUBLIC FUNCTIONS <-- */
    // function for generally starting up the auction application.
    function startApp() public onlyOwner {
        appStarted = true;
        emit AuctionOpen(msg.sender);
    }

    function register(address _nft, uint _nftId, uint highestBid, address payable seller) public payable open {
        require(msg.value >= TAX_FEE, "warning: insufficient registration funds");
        auctionItems.push(AuctionItem({
            seller: payable(seller),
            nft: _nft,
            nftId: _nftId,
            highestBidder: address(0),
            highestBid: highestBid,
            endAt: block.timestamp + 7 days,
            started: false,
            sold: false
        }));
        totalItems += 1;
        IERC721(_nft).transferFrom(seller, address(this), _nftId);
        // emit event
        emit ItemCreated(msg.sender, block.timestamp, totalItems+1);
    }

    function startAuction(uint _auctionId) public auctionExists(_auctionId) onlySeller(_auctionId) open {
        AuctionItem storage auction = auctionItems[_auctionId];
        require(auction.sold != true, "Item sold");
        auction.started = true;
        // emit event
        emit AuctionStarted(seller, _auctionId);
    }

    function bid(uint _auctionId) public auctionExists(_auctionId) payable open returns (bool)  {
        AuctionItem storage auction = auctionItems[_auctionId];
        if(!auction.started)
            revert Auction__NotStarted();
        if(auction.sold)
            revert Auction__ItemSold();
        if(block.timestamp >= auction.endAt)
            revert Auction__SaleOver();
        require(msg.value > auction.highestBid, "Bid higher");
        auction.highestBidder = msg.sender;
        auction.highestBid = msg.value;
        if(auction.highestBidder != address(0)) {
            bids[auction.highestBidder] += auction.highestBid;
        }
        return true;
        // emit event
        emit ItemBidIncreased(msg.sender, msg.value);
    }

    /* --> EXTERNAL FUNCTIONS <-- */  
    function claimBalance(uint _auctionId) external auctionExists(_auctionId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        uint bal = bids[msg.sender];
        bids[msg.sender] = 0;
        if(msg.sender != auction.highestBidder) {
            payable(msg.sender).transfer(bal);
        } else {
        revert Auction__NoBalance();
        }
        // emit event
        emit BalanceClaimed(msg.sender, bal, block.timestamp);
    }

    function transferItem(address nft, uint nftId, uint _auctionId) external onlySeller(_auctionId) open auctionExists(_auctionId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        require(block.timestamp >= auction.endAt, "warning: Auction not due");
        auction.sold = true;
        if(auction.highestBidder != address(0)) {
            IERC721(nft).safeTransferFrom(address(this), auction.highestBidder, nftId);
        auction.seller.transfer(auction.highestBid);
        } else {
            // transfer item back to seller
            IERC721(nft).safeTransferFrom(address(this), auction.seller, nftId);
        }
        // emit event
        emit ItemSold(auction.highestBidder, auction.highestBid, block.timestamp);
    }

    /**
    * @dev function transfers ownership for repossesion of contract.
    */
    function transferOwnership(address payable newOwner) external {
        require(!appStarted, "warning: app already started");
        require(newOwner != address(0), "invalid address");
        owner = payable(newOwner);
    }

    function closeApplication() external onlyOwner {
        require(address(this).balance == 0, "warning: Funds still in application");
        appClosed = true;
        selfdestruct(payable (owner));
        emit AuctionClosed(msg.sender);
    }

    /* --> GETTER FUNCTIONS <-- */
    function getHighestBid(uint _auctionId) public 
    view
    returns (uint highestBid) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.highestBid);
    }

    function getHighestBidder(uint _auctionId) public view returns (address highestBidder)
    {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.highestBidder);
    }

    function getAuctionItemState(uint _auctionId) public view returns (bool started, uint endAt, bool sold) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.started, auction.endAt, auction.sold);
    }

    function getSeller(uint _auctionId) public view returns (address seller) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.seller);
    }

    function getNftId(uint _auctionId) public view returns (uint nftId) {
        AuctionItem storage auction = auctionItems[_auctionId];
        return(auction.nftId);
    }

    function getAuctionItems() public view returns (AuctionItem[] memory) {
        return auctionItems;
    }
}

Let's Deploy!

This time we are going to write a script to deploy this contract to any of the existing testnets. Since Goerli recently got deprecated we're going to use Sepolia so this guide stays relevant.

To deploy and interact with the deployed contract on a testnet you would need some testnet ether.

Follow this link to request some free Sepolia testnet ether.

Creating the NFTAuctionDeploy.js

Next, let's create a new folder and name deploy. Inside that new deploy folder, we are going to create a new file as well and call it NFTAuctionDeploy.js.

The deploy script is written in Javascript, so it would be nice to know some Javascript to understand what's going on before continuing, but if you don't you can just copy and paste the code.

const { getNamedAccounts, deployments, network, run, ethers } = require("hardhat");
const { networkConfig, developmentChains } = require("../helper-hardhat-config");

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  const chainId = network.config.chainId;

  const arguments = [];
  const NftAuctionContract = await deploy("NFTAuctionContract", {
    from: deployer,
    args: arguments,
    log: true,
    waitConfirmations: waitBlockConfirmations || 1,
  });
  const networkName = network.name == "hardhat" ? "localhost" : network.name;
  log(`npx hardhat run scripts/NFTAuctionDeploy.js --network ${networkName}`);
  log("----------------------------------");
};

module.exports.tags = ["all", "auction"];

Helper-hardhat-config.js

The code above requires some extra code imported from a file called helper-hardhat-config.js.

To get this, create a new file with the same name helper-hardhat-config.js and paste the following code into that file.

const networkConfig = {
    default: {
        name: "hardhat",
    },
    31337: {
        name: "localhost",
    },
    11155111: {
        name: "sepolia",
    },
  }
  const developmentChains = ["hardhat", "localhost"]
  module.exports = {
    networkConfig,
    developmentChains
  }

This code configures the Sepolia network and localhost to be used for deployment

Hardhat.config.js

When we installed hardhat it came with a file called hardhat.config.js, we're going to make some changes to that file so it suits our deployment. Copy and paste the following code into your hardhat.config.js

require("@nomiclabs/hardhat-waffle")
require("@nomiclabs/hardhat-ethers");
require("hardhat-deploy")
require("dotenv").config()

/**
 * @type import('hardhat/config').HardhatUserConfig
 */

const SEPOLIA_RPC_URL = process.env.SEPOLIA_RPC_URL 
const PRIVATE_KEY = process.env.PRIVATE_KEY

module.exports = {
    defaultNetwork: "hardhat",
    networks: {
        hardhat: {
            chainId: 31337,
        },
        localhost: {
            chainId: 31337,
        },
        sepolia: {
            url: SEPOLIA_RPC_URL,
            accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
            saveDeployments: true,
            chainId: 11155111,
        },
    },
    namedAccounts: {
        deployer: {
            default: 0, // default 
            1: 0,
        },
        player: {
            default: 1,
        },
    },
    solidity: {
        compilers: [
            {
                version: "0.8.10",
            },
        ],
    },
    mocha: {
        timeout: 20000,
    },
}

RPCs

To deploy this contract you will need to connect to an RPC.

For this I use Alchemy.

Follow the link to fetch the Alchemy RPC URL, but feel free to use any RPC you're more familiar with.

Once you've fetched your RPC URL, create a new file called .env and install it to your hardhat environment using npm install --save-dev dotenv or yarn add dotenv

When that's setup, copy and paste the following code into your .env file

# Paste your RPC URL and developer accounts private key into the quotation marks
SEPOLIA_RPC_URL = ""
PRIVATE_KEY = ""

PRIVATE KEY SECURITY

Private keys are sensitive information, if they ever leaked to the public your wallet and assets stand the risk of being hijacked. DO NOT post your private key on the internet. If you want to push your code to GitHub, create a new file called .gitignore and inside it paste the following

.env
node_modules
cache

This ensures that when you push your code to GitHub, your private key as well as other files nested in it isn't uploaded as well.

Deploying To Sepolia Test Network

We are done writing our smart contracts and deploy scripts. Now it's time to deploy. To deploy this contract to a live testnet network use the command

# for npm
npx hardhat deploy --network sepolia
# for yarn
yarn deploy --network sepolia

This operation first runs the compiler to check for errors in our code then deploys the contract live on the Sepolia testnet.

If all the steps were followed correctly, the contract should be live on the Sepolia testnet and we can view it using the Sepolia block explorer on Etherscan.

Conclusion

We have successfully built an English NFT Auction. We built the IERC721.sol, NFTAuctionContract.sol, NFTAuctionDeploy.js, helper-hardhat-config.js and even wrote a .env script for private keys and Alchemy RPC URL.

I hope you had as much fun coding along as I had when I initially built this software.

If you seem to be running into any problems that wouldn't go away, clone the project directly from my GitHub repo here and redeploy again.

Note: When you clone the repo be sure to update your packages using:

# for npm
npm install
# for yarn
yarn install

If you have any further questions be sure to leave them in the comments and I will be with you as soon as I can. If you can't wait for me then feel free to reach out to me on any of my socials.

About the Author

Hey there, my name is Gabriel Isobara but I'm more famously known by my alias Jamaltheatlantean.

I am a Blockchain developer, as well as a Technical and Research writer. I love to create and build things through my writing, from software to articles and I have been developing software since 2019.

If you liked this tutorial feel free to leave a like and follow me up for more.

I am available to create technical content for any potential client and you can reach me on Twitter, Linkedin or any of my socials found here.