How to Write a Simple Auction Smart Contract in Solidity

Auctions are events where items are spotlighted and put on sale for a period during which people can place bids on them. The highest bidder at the end of the auction period is the winner. The winner now owns the item, while the other bidders get back the money they spent bidding.

Smart contracts are an easy way to streamline auctions. They can ensure that simple problems that come from real-life auctions are avoided. Issues such as:

  • the highest bidder leaving with the item without actually paying for it

  • people actually having the money they are pledging on the item

  • the integrity of the item being bid on

A simple auction smart contract such as the one we will write can be used to auction NFTs and other assets.

Prerequisites

Tl;dr: Jump to the end to see the full contract

This smart contract was written using basic solidity. It is not suitable for production environments and is better used as educational content to practice solidity or Web3 development.

To write and deploy this contract, you will need the following:

  • A development environment (like Remix).

  • An EVM-compatible wallet set to the Sepolia testnet and funded with Sepolia ETH. You can get Sepolia ETH from this faucet.

  • Basic knowledge of solidity.

What will this contract do?

We are writing a simple contract that will allow users to do the following:

  • Create an auction: Users can start an auction for an item by inputting the item name, the auction start price, and how long they want the auction to last.

  • Get the details for a created auction: Users can view auction details by entering the item’s ID.

  • Bid on an auctioned item: Users can bid on different items by entering the item’s ID and calling the bid function with the amount they want to offer.

  • End the auction: The auction creator calls this function when the auction duration is over to officially end the auction, announce the winner, and distribute the other bids to their respective bidders.

Writing the contract

Initialise the global variables

We want people to be able to get the following information about an auctioned item:

  • the creator of the auction

  • the name of the item being auctioned

  • the ID of the item being auctioned

  • the highest bidder on the item

  • the highest amount bid on the item

  • the number of bidders who have bid on an item

  • the time the auction will end

  • the winner of an auction

  • the state of an auction: open/ended

First, define an item struct that holds all the above details:

struct ItemToAuction{
        address auctionCreator;
        string itemName;
        uint256 itemId;
        address highestBidder;
        uint256 highestBid;
        uint256 numberOfBidders;
        uint256 auctionEndTime;
        address winner;
        bool ended;

Next, create an array of items to store the details for each new item we want to auction:

ItemToAuction[] private items;

and a variable that’ll count the bidders on an item. This variable will also be used to set the item ID, so set the value to 1.

uint256 private itemCounter = 1;

Initialise a mapping that binds the bidder addresses to the item ID. Then initialize a nested mapping to bind the bid amount to the bidder addresses and ultimately to the item ID. This allows us to see the bidders who have bid on an item and how much they bid. We can also use this to retrieve the number of bidders.

mapping(uint256 itemId => address[] bidders) public itemToBidders;
mapping(uint256 itemId => mapping(address bidder => uint256 bidAmount)) public itemToBidderAmount;

We want to get alerts for important interactions, such as when a new bid has been placed and when the auction has ended.

event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);

Starting an auction

Create a function that takes the name of the item being auctioned, how long the auction will last, and the starting price as parameters. The auction duration should be set in hours for ease of use. To get the auction end time, add the auction duration to the current time.

Next, define the item's initial details and add them to the auctioned items' array.

Finally, increase the number of items created by 1, so the next item can have the subsequent figure as its ID.

    function startAuction(string memory _itemName, uint256 _auctionDuration, uint256 _startPrice) public {
        require(_startPrice != 0, "Initial price must be greater than zero");

        _auctionDuration *= 3600;
        uint256 _auctionEndTime = block.timestamp + _auctionDuration;

        items.push(ItemToAuction({
            auctionCreator: msg.sender,
            itemName: _itemName,
            itemId: itemCounter,
            highestBidder: address(0),
            highestBid: _startPrice,
            numberOfBidders: 0,
            auctionEndTime: _auctionEndTime,
            winner: address(0),
            ended: false
        }));
        itemCounter++;

Modifiers

  • Check if an input ID is valid
    modifier validItemId(uint256 _itemId) {
        require(_itemId > 0 && _itemId <= items.length, "Invalid item ID");
        _;
    }
  • Check if an auction has ended
    modifier ended(uint256 _itemId) {
        require(block.timestamp < items[_itemId - 1].auctionEndTime, "Auction has ended");
        _;
    }
  • Ensure the caller is the auction creator
modifier onlyCreator(uint256 _itemId) {
  require(msg.sender == items[_itemId - 1].auctionCreator, "Only the auction creator can end the auction");
}

Check if a bidder has bid

For users that bid more than once, we need to ensure their address is not added multiple times to the itemToBidders mapping, and that their current bid on an item is added to any previous bid on the same item. Create a function that checks if an address has already previously bid on an item.

    function addresshasBid(address[] memory addresses, address addy) private pure returns (bool) {
        for (uint256 i = 0; i < addresses.length; i++) {
            if (addresses[i] == addy) {
                return true;
            }
        }
        return false;
    }

Placing a bid

Create a payable function that accepts an item ID as a parameter. Check that the input ID is valid and the caller is not the auction creator; an auction creator should not be allowed to bid on their own item. If the auction has ended, the user should also be unable to bid.

Calculate the total amount a caller has bid on the selected item and ensure that it is higher than the item’s current highest bid. Update the itemToBidderAmount mapping with the caller’s new bid amount.

Update the current highest bidder and the highest bid. If the caller has not bid before, update the total number of bidders as well.

Emit an event to notify participants about the new bid.

    function bid(uint256 _itemId) public validItemId(_itemId) ended(_itemId) payable {
        ItemToAuction storage item = items[_itemId - 1];
        require(msg.sender != item.auctionCreator, "Auction creator cannot bid");

        if (block.timestamp >= item.auctionEndTime) {
            endAuction(item.itemId);
        } else {

            uint256 totalBid = msg.value + itemToBidderAmount[_itemId][msg.sender];

            require(totalBid > item.highestBid, "Total bid must be greater than current highest bid");

            itemToBidderAmount[_itemId][msg.sender] = totalBid;
            item.highestBidder = msg.sender;
            item.highestBid = totalBid

            if(!addressHasBid(itemToBidders[_itemId], msg.sender)) {
                itemToBidders[_itemId].push(msg.sender);
                item.numberOfBidders++;

            }
            emit HighestBidIncreased(msg.sender, totalBid);
        }
    }

Get the details of an auctioned item

This function will accept an item ID as a parameter and return the details for that item

function getAuctionDetails(uint256 _itemId) validItemId(_itemId) public view returns(address, string memory, uint256, uint256, address, uint256, uint256, address, bool) {
  ItemToAuction storage item = items[_itemId - 1]; 
  // define item on the global scope as a private function
  return(
    item.auctionCreator,
    item.itemName,
    item.itemId,
    item.numberOfBidders,
    item.highestBidder,
    item.highestBid,
    item.auctionEndTime,
    item.winner,
    item.ended
  );
}

Ending an auction

Only the auction creator will be able to call this function to end an auction by passing their item ID as a parameter. This function can only be called if the auction duration is over.

Set the item’s state to ended and the auction winner to the highest bidder. Next, iterate through the itemToBidders mapping to get the address for each bidder. Then refund the bid placed by each bidder, unless the bidder is the auction winner. Transfer the winner’s bid to the auction creator. Finally, reset the address for the highest bidder.

    function endAuction(uint256 _itemId) public validItemId(_itemId) onlyCreator(_itemId) {
        ItemToAuction storage item = items[_itemId - 1];

        require(block.timestamp >= item.auctionEndTime, "Auction time has not finished yet");
        require(!item.ended, "Auction has already been ended");

        item.ended = true;
        item.winner = item.highestBidder;

        // Transfer highest bid to the auction creator
        payable(item.auctionCreator).transfer(item.highestBid);

        // Refund bid amounts to non-winning bidders
        for (uint256 i = 0; i < itemToBidders[_itemId].length; i++) {
            address bidder = itemToBidders[_itemId][i];
            if (bidder != item.winner) {
                uint256 refundAmount = itemToBidderAmount[_itemId][bidder];
                payable(bidder).transfer(refundAmount);
            }
        }
        item.highestBidder = address(0);
    }

Testing the Smart Contract

The smart contract is deployed on the Sepolia testnet and you can view the full contract here. If you followed the steps correctly, your smart contract should look like the one below. Feel free to copy/paste this into Remix, create your own auctions and try out all its features.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Auction {
    struct ItemToAuction{
        address auctionCreator;
        string itemName;
        uint256 itemId;
        address highestBidder;
        uint256 highestBid;
        uint256 numberOfBidders;
        uint256 auctionEndTime;
        address winner;
        bool ended;
    }

    ItemToAuction[] private items;

    uint256 private itemCounter = 1;

    mapping(uint256 itemId => address[] bidders) public itemToBidders;
    mapping(uint256 itemId => mapping(address bidders => uint256 bid)) public itemToBidderAmount;

    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    function startAuction(string memory _itemName, uint256 _auctionDuration, uint256 _startPrice) public {
        require(_startPrice != 0, "Initial price must be greater than zero");

        _auctionDuration *= 3600;
        uint256 _auctionEndTime = block.timestamp + _auctionDuration;

        items.push(ItemToAuction({
            auctionCreator: msg.sender,
            itemName: _itemName,
            itemId: itemCounter,
            highestBidder: address(0),
            highestBid: _startPrice,
            numberOfBidders: 0,
            auctionEndTime: _auctionEndTime,
            winner: address(0),
            ended: false
        }));
        itemCounter++;
    }

    function bid(uint256 _itemId) public validItemId(_itemId) ended(_itemId) payable {
        ItemToAuction storage item = items[_itemId - 1];
        require(msg.sender != item.auctionCreator, "Auction creator cannot bid");

        if (block.timestamp >= item.auctionEndTime) {
            endAuction(item.itemId);
        } else {

            uint256 totalBid = msg.value + itemToBidderAmount[_itemId][msg.sender];

            require(totalBid > item.highestBid, "Total bid must be greater than current highest bid");

            itemToBidderAmount[_itemId][msg.sender] = totalBid;
            item.highestBidder = msg.sender;
            item.highestBid = totalBid;

            if(!addresshasBid(itemToBidders[_itemId], msg.sender)) {
                itemToBidders[_itemId].push(msg.sender);
                item.numberOfBidders++;

            }
            emit HighestBidIncreased(msg.sender, totalBid);
        }
    }

    function addresshasBid(address[] memory addresses, address addy) private pure returns (bool) {
        for (uint256 i = 0; i < addresses.length; i++) {
            if (addresses[i] == addy) {
                return true;
            }
        }
        return false;
    }

    function getAuctionDetails(uint256 _itemId) validItemId(_itemId) public view returns(address, string memory, uint256, uint256, address, uint256, uint256, address, bool) {
        ItemToAuction storage item = items[_itemId - 1];

        return (
            item.auctionCreator,
            item.itemName,
            item.itemId,
            item.numberOfBidders,
            item.highestBidder,
            item.highestBid,
            item.auctionEndTime,
            item.winner,
            item.ended
        );
    }

    function endAuction(uint256 _itemId) public validItemId(_itemId) onlyCreator(_itemId) {
        ItemToAuction storage item = items[_itemId - 1];

        require(block.timestamp >= item.auctionEndTime, "Auction time has not finished yet");
        require(!item.ended, "Auction has already been ended");

        item.ended = true;
        item.winner = item.highestBidder;

        payable(item.auctionCreator).transfer(item.highestBid);

        // Refund bid amounts to non-winning bidders
        for (uint256 i = 0; i < itemToBidders[_itemId].length; i++) {
            address bidder = itemToBidders[_itemId][i];
            if (bidder != item.winner) {
                uint256 refundAmount = itemToBidderAmount[_itemId][bidder];
                payable(bidder).transfer(refundAmount);
            }
        }
        item.highestBidder = address(0);
    }

    modifier validItemId(uint256 _itemId) {
        require(_itemId > 0 && _itemId <= items.length, "Invalid item ID");
        _;
    }

    modifier ended(uint256 _itemId) {
        require(block.timestamp < items[_itemId - 1].auctionEndTime, "Auction has ended");
        _;
    }

    modifier onlyCreator(uint256 _itemId) {
        require(msg.sender == items[_itemId - 1].auctionCreator, "Only the auction creator can end the auction");
        _;
    }
}