Create your own
Lesson illustration

Implementing an Auction System in the Marketplace Contract

Hello! Welcome back to our course on Ethereum development.

In the last lesson, we built a robust, fixed-price NFT marketplace, implementing secure patterns for listing, buying, and canceling sales. We now have a solid foundation for facilitating trades. However, fixed-price sales are just one side of the story. To create a truly dynamic and engaging ecosystem, we need to introduce competition and price discovery.

This lesson will extend our marketplace to support auctions.

Learning Outcome: By the end of this 60-minute lesson, you will be able to extend the marketplace contract to support a basic auction system, including placing bids and settling the auction.

We will focus on implementing a classic English auction (open, ascending price), where bidders compete to offer the highest price until a set time expires. We'll cover the entire lifecycle: creating an auction, placing bids, refunding outbid participants, and settling the final sale.

1. Types of Auctions & Our Approach

Before we dive into the code, it's useful to know that there are several types of auctions, each with different mechanics.

How to Create a Dutch Auction Smart Contract

This article from QuickNode gives a concise overview of the most common auction types used in smart contracts. Understanding these provides valuable context for why we choose a specific model.

Read the section 'Auctions using smart contract'. Focus on the descriptions of the English, Dutch, and First-price sealed-bid auctions. You don't need to read about the implementation details.

For our marketplace, we will implement the English auction, as it's the most intuitive and common for digital collectibles. The core components we need to add to our NftMarketplace.sol contract are:

  • Data Structures: An Auction struct to hold auction-specific data like the end time, highest bid, and highest bidder.
  • State Management: Mappings to store active auctions and track funds owed to outbid participants.
  • Core Functions:
    • listForAuction(): To create a new auction listing.
    • bid(): A payable function for users to place bids.
    • withdrawBid(): To allow outbid users to retrieve their funds (the "pull" pattern).
    • endAuction(): To finalize the auction, transferring the NFT to the winner and funds to the seller.

2. Building the Auction Contract Logic

We will build our auction logic by following a clear, step-by-step video tutorial. While the video creates a standalone auction contract, we will adapt its principles to extend our existing marketplace. This process of integrating new features into an existing codebase is a common task in smart contract development.

Blockchain For Beginners #4 - Solidity NFT Auction

This video from Tech With Tim provides a detailed walkthrough of building an NFT auction contract from the ground up. It covers all the essential functions and state variables we'll need.

Watch the following segments in order. As you watch, think about how you would integrate these concepts into the NftMarketplace.sol contract from our previous lesson. Auction State & Setup (26:43 - 34:15): Pay attention to the state variables (seller, endAt, highestBid, highestBidder, etc.) and the logic in the start function. This will form the basis of our Auction struct and listForAuction function. The bid Function (42:31 - 46:15): This is the core of the auction. Focus on the require statements that validate a bid and the logic for updating the highestBid and highestBidder. Crucially, notice how it handles the previous bidder's funds, preparing them for withdrawal. The withdraw Function (46:04 - 49:06): This demonstrates the 'pull' pattern for outbid participants. Understand how a user can safely retrieve their funds after being outbid. The end and Settlement Logic (37:04 - 40:22 & 1:01:13 - 1:03:34): This covers settling the auction. The first part sets up the basic end function, and the second part adds the critical logic for transferring the NFT to the winner and paying the seller.

3. A Secure Code Reference: Solidity by Example

The video provides a great practical walkthrough. To reinforce these concepts, let's review a canonical implementation from the official Solidity documentation. This example is concise and clearly demonstrates secure patterns.

Solidity by Example - Simple Open Auction

The 'Solidity by Example' documentation provides a clean, well-commented implementation of a simple open auction. It's an excellent reference for best practices.

Read through the SimpleAuction contract code. Compare its implementation of bid(), withdraw(), and auctionEnd() to the one in the video. Note the use of the pendingReturns mapping, which is a classic and secure way to handle refunds for outbid participants.

The key security takeaway from both resources is how to handle refunds for outbid bidders. Instead of the contract actively pushing funds back (e.g., previousBidder.transfer(amount)), which can be risky, it updates an internal ledger (pendingReturns or bids). The outbid user must then call a separate withdraw function to pull their funds. This is a direct application of the Checks-Effects-Interactions pattern and avoids reentrancy vulnerabilities.

4. Integrating Auctions into Our Marketplace

Now, let's combine everything we've learned into our NftMarketplace.sol contract. We will add the new functionality alongside our existing fixed-price logic.

Here are the required additions and modifications:

  1. New Data Structures: An Auction struct and mappings for auctions and pending bid returns.
  2. New Events and Errors: To provide clear off-chain information about auction activity.
  3. New Functions: listForAuction, bid, endAuction, and withdrawBid.
  4. Modifier Update: The notListed modifier must now check both fixed-price listings and auction listings to prevent an NFT from being sold in two ways at once.

Below is the extended NftMarketplace.sol. The new code is clearly marked.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";

contract NftMarketplace is ReentrancyGuard {
    // --- FIXED-PRICE LISTING STRUCTS ---
    struct Listing {
        uint256 price;
        address seller;
    }

    // --- AUCTION STRUCTS (NEW) ---
    struct Auction {
        address seller;
        uint256 startingPrice;
        uint256 endAt;
        address highestBidder;
        uint256 highestBid;
    }

    // --- STATE VARIABLES ---
    mapping(address => mapping(uint256 => Listing)) private s_listings;
    mapping(address => uint256) private s_proceeds;

    // --- NEW AUCTION STATE VARIABLES ---
    mapping(address => mapping(uint256 => Auction)) private s_auctions;
    mapping(address => uint256) private s_pendingBidReturns;

    // --- EVENTS ---
    event ItemListed(address indexed seller, address indexed nftAddress, uint256 indexed tokenId, uint256 price);
    event ItemBought(address indexed buyer, address indexed nftAddress, uint256 indexed tokenId, uint256 price);
    event ItemCanceled(address indexed seller, address indexed nftAddress, uint256 indexed tokenId);
    
    // --- NEW AUCTION EVENTS ---
    event AuctionCreated(address indexed seller, address indexed nftAddress, uint256 indexed tokenId, uint256 startingPrice, uint256 endAt);
    event NewBid(address indexed bidder, address indexed nftAddress, uint256 indexed tokenId, uint256 amount);
    event AuctionSettled(address indexed winner, address indexed nftAddress, uint256 indexed tokenId, uint256 amount);
    event AuctionCanceled(address indexed nftAddress, uint256 indexed tokenId);


    // --- ERRORS ---
    error AlreadyListed(address nftAddress, uint256 tokenId);
    error NotOwner();
    error NotListed(address nftAddress, uint256 tokenId);
    error PriceMustBeAboveZero();
    error NotApprovedForMarketplace();
    error PriceNotMet(address nftAddress, uint256 tokenId, uint256 price);
    error NoProceeds();
    error TransferFailed();

    // --- NEW AUCTION ERRORS ---
    error AuctionNotActive();
    error AuctionEnded();
    error BidTooLow(uint256 highestBid);
    error NoBidsToWithdraw();
    error AuctionNotEnded();

    // --- MODIFIERS ---
    // MODIFIED to check both listings and auctions
    modifier notListed(address nftAddress, uint256 tokenId) {
        if (s_listings[nftAddress][tokenId].price > 0) { revert AlreadyListed(nftAddress, tokenId); }
        if (s_auctions[nftAddress][tokenId].endAt > 0) { revert AlreadyListed(nftAddress, tokenId); }
        _;
    }

    modifier isListed(address nftAddress, uint256 tokenId) {
        if (s_listings[nftAddress][tokenId].price == 0) { revert NotListed(nftAddress, tokenId); }
        _;
    }
    
    // NEW modifier for auctions
    modifier isAuctioned(address nftAddress, uint256 tokenId) {
        if (s_auctions[nftAddress][tokenId].endAt == 0) { revert NotListed(nftAddress, tokenId); }
        _;
    }

    modifier isOwner(address nftAddress, uint256 tokenId, address spender) {
        if (IERC721(nftAddress).ownerOf(tokenId) != spender) { revert NotOwner(); }
        _;
    }

    // --- FIXED-PRICE FUNCTIONS (from previous lesson, unchanged) ---
    function listItem(address nftAddress, uint256 tokenId, uint256 price)
        external notListed(nftAddress, tokenId) isOwner(nftAddress, tokenId, msg.sender) {
        // ... implementation from previous lesson
    }

    function buyItem(address nftAddress, uint256 tokenId)
        external payable nonReentrant isListed(nftAddress, tokenId) {
        // ... implementation from previous lesson
    }

    function cancelListing(address nftAddress, uint256 tokenId)
        external nonReentrant isListed(nftAddress, tokenId) {
        // ... implementation from previous lesson
    }

    function withdrawProceeds() external nonReentrant {
        // ... implementation from previous lesson
    }

    // --- NEW AUCTION FUNCTIONS ---

    function listForAuction(address nftAddress, uint256 tokenId, uint256 startingPrice, uint256 duration)
        external
        notListed(nftAddress, tokenId)
        isOwner(nftAddress, tokenId, msg.sender)
    {
        if (startingPrice == 0) { revert PriceMustBeAboveZero(); }
        if (IERC721(nftAddress).getApproved(tokenId) != address(this)) { revert NotApprovedForMarketplace(); }

        uint256 endAt = block.timestamp + duration;
        s_auctions[nftAddress][tokenId] = Auction(msg.sender, startingPrice, endAt, address(0), startingPrice);
        
        emit AuctionCreated(msg.sender, nftAddress, tokenId, startingPrice, endAt);
        
        IERC721(nftAddress).transferFrom(msg.sender, address(this), tokenId);
    }

    function bid(address nftAddress, uint256 tokenId)
        external
        payable
        nonReentrant
        isAuctioned(nftAddress, tokenId)
    {
        Auction storage auction = s_auctions[nftAddress][tokenId];
        if (block.timestamp >= auction.endAt) { revert AuctionEnded(); }
        if (msg.value <= auction.highestBid) { revert BidTooLow(auction.highestBid); }

        // Refund the previous highest bidder by adding to their withdrawable balance
        if (auction.highestBidder != address(0)) {
            s_pendingBidReturns[auction.highestBidder] += auction.highestBid;
        }

        // Update auction state
        auction.highestBidder = msg.sender;
        auction.highestBid = msg.value;

        emit NewBid(msg.sender, nftAddress, tokenId, msg.value);
    }

    function withdrawBid(address nftAddress, uint256 tokenId) external nonReentrant {
        // Note: This function allows withdrawing *any* pending returns, not just for a specific auction.
        // This is a simpler and more gas-efficient design.
        uint256 amount = s_pendingBidReturns[msg.sender];
        if (amount == 0) { revert NoBidsToWithdraw(); }

        s_pendingBidReturns[msg.sender] = 0; // Checks-Effects-Interactions

        (bool success, ) = payable(msg.sender).call{value: amount}("");
        if (!success) {
            s_pendingBidReturns[msg.sender] = amount; // Revert state on failure
            revert TransferFailed();
        }
    }

    function endAuction(address nftAddress, uint256 tokenId)
        external
        nonReentrant
        isAuctioned(nftAddress, tokenId)
    {
        Auction memory auction = s_auctions[nftAddress][tokenId];
        if (block.timestamp < auction.endAt) { revert AuctionNotEnded(); }

        delete s_auctions[nftAddress][tokenId];

        if (auction.highestBidder == address(0)) {
            // No bids, return NFT to seller
            emit AuctionCanceled(nftAddress, tokenId);
            IERC721(nftAddress).transferFrom(address(this), auction.seller, tokenId);
        } else {
            // There was a winner
            // Handle royalties (same as buyItem)
            uint256 royaltyAmount = 0;
            try IERC2981(nftAddress).royaltyInfo(tokenId, auction.highestBid) returns (address receiver, uint256 amount) {
                (bool success, ) = payable(receiver).call{value: amount}("");
                if(success) royaltyAmount = amount;
            } catch {}

            // Add seller's cut to their proceeds balance (pull pattern)
            s_proceeds[auction.seller] += (auction.highestBid - royaltyAmount);
            
            emit AuctionSettled(auction.highestBidder, nftAddress, tokenId, auction.highestBid);
            
            // Transfer NFT to winner
            IERC721(nftAddress).transferFrom(address(this), auction.highestBidder, tokenId);
        }
    }

    // --- GETTER FUNCTIONS ---
    function getListing(address nftAddress, uint256 tokenId) external view returns (Listing memory) {
        return s_listings[nftAddress][tokenId];
    }

    function getAuction(address nftAddress, uint256 tokenId) external view returns (Auction memory) {
        return s_auctions[nftAddress][tokenId];
    }

    function getProceeds(address seller) external view returns (uint256) {
        return s_proceeds[seller];
    }

    function getPendingBidReturns(address bidder) external view returns (uint256) {
        return s_pendingBidReturns[bidder];
    }
}

Conclusion

Excellent work! You have successfully extended the marketplace to support a full-featured English auction system. By integrating this new logic, you've made the platform significantly more versatile and engaging for users.

Key Takeaways:

  • Auction Lifecycle: You implemented the complete on-chain flow for an auction: creation (listForAuction), participation (bid), and finalization (endAuction).
  • Safe Fund Handling: You applied the secure "pull-over-push" pattern not only for seller proceeds but also for refunding outbid participants via the s_pendingBidReturns mapping and withdrawBid function.
  • State Management: You cleanly separated the state for fixed-price listings and auctions using distinct structs and mappings, while modifying shared logic like the notListed modifier to ensure contract-wide consistency.
  • Modularity: The new features were added as modular components, a crucial skill for maintaining and upgrading complex smart contracts over time.

Next Steps:

So far, our NFTs have been static—their appearance and properties are fixed. But what if an NFT could evolve based on on-chain events? In our next lesson, we will explore a fascinating and creative concept: implementing a dynamic NFT that changes its metadata based on on-chain state or external events.

Can't find a good explanation? Sign up and we'll make it for you

Sign up