A Decentralized Auction Platform for Rare and Collectible Items on Celo

A Decentralized Auction Platform for Rare and Collectible Items on Celo https://celo.academy/uploads/default/optimized/2X/9/91d758a200c886c81aec6364d0accb12718d66d5_2_1024x576.png
none 0.0 0

INTRODUCTION

In this tutorial, we will guide you through the process of creating a decentralized auction platform on the Celo blockchain. You will learn how to leverage smart contracts to enable transparent and secure auctions for rare and collectible items. We will cover the setup of the development environment, smart contract architecture, auction creation, bidding process, auction finalization, and integration with Celo. By the end, you will have a functional platform empowering users to engage in trustworthy auctions. Let’s get started building this exciting decentralized auction platform on Celo!

REQUIREMENTS

To follow this tutorial, you will require:

  • A code editor or text editor such as Remix.

  • An internet browser and a stable internet connection.

PREREQUISITES

To successfully complete this tutorial, it is recommended that you have:

  • Familiarity with Javascript programming language.

  • A basic understanding of Blockchain technology and its functioning.

  • Basic knowledge of the Solidity programming language used for smart contract development on the blockchain

To initiate the development process, we will utilize the Remix IDE for writing our smart contract. Let’s kick off the tutorial by getting started with Remix IDE!

Click here to get the complete code for this session

The complete code:

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

interface IERC20Token {
    function transfer(address, uint256) external returns (bool);

    function approve(address, uint256) external returns (bool);

    function transferFrom(address, address, uint256) external returns (bool);

    function totalSupply() external view returns (uint256);

    function balanceOf(address) external view returns (uint256);

    function allowance(address, address) external view returns (uint256);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract AuctionPlatform {
    using SafeMath for uint256;

    struct Auction {
        address payable seller;
        string image;
        string brand;
        string color;
        string durability;
        uint256 price;
        uint256 endTime;
        address highestBidder;
        uint256 highestBid;
        bool ended;
    }

    uint256 internal auctionCount = 0;
    address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;

    mapping(uint256 => Auction) internal auctions;

    function createAuction(
        string memory _image,
        string memory _brand,
        string memory _color,
        string memory _durability,
        uint256 _price,
        uint256 _duration
    ) public {
        Auction storage newAuction = auctions[auctionCount];

        newAuction.seller = payable(msg.sender);
        newAuction.image = _image;
        newAuction.brand = _brand;
        newAuction.color = _color;
        newAuction.durability = _durability;
        newAuction.price = _price;
        newAuction.endTime = block.timestamp.add(_duration);
        newAuction.ended = false;

        auctionCount++;
    }

    function getAuction(uint256 _index)
        public
        view
        returns (
            address payable,
            string memory,
            string memory,
            string memory,
            string memory,
            uint256,
            uint256,
            address,
            uint256,
            bool
        )
    {
        Auction storage auction = auctions[_index];
        return (
            auction.seller,
            auction.image,
            auction.brand,
            auction.color,
            auction.durability,
            auction.price,
            auction.endTime,
            auction.highestBidder,
            auction.highestBid,
            auction.ended
        );
    }

    function placeBid(uint256 _index) public payable {
        Auction storage auction = auctions[_index];
        require(!auction.ended, "Auction has ended");
        require(block.timestamp < auction.endTime, "Auction has ended");
        require(msg.value > auction.highestBid, "Bid amount too low");

        if (auction.highestBid > 0) {
            // Refund the previous highest bidder
            require(
                IERC20Token(cUsdTokenAddress).transfer(
                    auction.highestBidder,
                    auction.highestBid
                ),
                "Transfer failed."
            );
        }

        auction.highestBidder = msg.sender;
        auction.highestBid = msg.value;
    }

    function endAuction(uint256 _index) public {
        Auction storage auction = auctions[_index];
        require(!auction.ended, "Auction has already ended");
        require(block.timestamp >= auction.endTime, "Auction has not ended yet");

        auction.ended = true;

        if (auction.highestBid > 0) {
            // Transfer the highest bid amount to the seller
            require(
                IERC20Token(cUsdTokenAddress).transfer(
                    auction.seller,
                    auction.highestBid
                ),
                "Transfer failed."
            );
        }
    }

    function getAuctionCount() public view returns (uint256) {
        return auctionCount;
    }
}

Code Analysis:

Step 1: Solidity Version and License Declaration

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

In this step, we start by specifying the version of the Solidity programming language to be used for compiling our smart contract. We use the directive pragma solidity >=0.7.0 <0.9.0; to define a range of acceptable Solidity versions. This ensures that our contract is compatible with a specific range of compiler versions, from 0.7.0 (inclusive) to 0.9.0 (exclusive).

Additionally, we include the comment // SPDX-License-Identifier: MIT to declare the license under which our smart contract is released. The SPDX-License-Identifier tag specifies the MIT license, which is a widely used permissive open-source license.

Note:

Including the SPDX-License-Identifier comment and specifying the Solidity version is crucial for maintaining clarity and compatibility in our smart contract development. It helps ensure that the contract is compiled with the appropriate Solidity version and provides clear licensing information for others who may want to use or modify our code.

Step 2: Importing Dependencies and Defining an Interface

import "@openzeppelin/contracts/utils/math/SafeMath.sol";

interface IERC20Token {
    function transfer(address, uint256) external returns (bool);

    function approve(address, uint256) external returns (bool);

    function transferFrom(address, address, uint256) external returns (bool);

    function totalSupply() external view returns (uint256);

    function balanceOf(address) external view returns (uint256);

    function allowance(address, address) external view returns (uint256);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

In this step, we import the SafeMath library from the OpenZeppelin contract library. The SafeMath library provides secure arithmetic operations to prevent common vulnerabilities such as overflow and underflow in mathematical calculations.

We also define an interface called IERC20Token. An interface is a way to define a contract’s external-facing functions without implementing the full contract. The IERC20Token interface specifies the required functions and events for interacting with an ERC20 token contract.

By importing the SafeMath library and defining the IERC20Token interface, we ensure that our smart contract can safely perform mathematical operations and interact with ERC20 tokens.

Note:

Including the necessary dependencies and interfaces is essential for utilizing external functionalities and interacting with other contracts. In this case, we import SafeMath to handle secure mathematical operations and define the IERC20Token interface to interact with ERC20 tokens. These steps promote code reusability and maintain best practices in contract development.

Step 3: Defining the Auction Structure and Variables

contract AuctionPlatform {
    using SafeMath for uint256;

    struct Auction {
        address payable seller;
        string image;
        string brand;
        string color;
        string durability;
        uint256 price;
        uint256 endTime;
        address highestBidder;
        uint256 highestBid;
        bool ended;
    }

    uint256 internal auctionCount = 0;
    address internal cUsdTokenAddress = 0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;

    mapping(uint256 => Auction) internal auctions;

In this step, we define the structure Auction, which represents an individual auction on our platform. It includes various attributes such as the seller's address (seller), the item’s image (image), brand (brand), color (color), durability (durability), the desired price (price), the end time of the auction (endTime), the highest bidder's address (highestBidder), the highest bid amount (highestBid), and a flag indicating if the auction has ended (ended).

We also declare two important variables: auctionCount, which keeps track of the number of auctions created, and cUsdTokenAddress, which represents the address of the cUSD token contract. The auctionCount variable is initialized to 0, indicating no auctions have been created yet.

Note:

Defining the Auction structure and necessary variables is a crucial step in building our auction platform. These components store essential information about each auction and facilitate the management and tracking of bids, bidders, and the auction state. Importantly, incorporating the SafeMath library ensures the integrity of mathematical operations, promoting secure and reliable calculations within our smart contract.

Step 4: Creating an Auction

  function createAuction(
        string memory _image,
        string memory _brand,
        string memory _color,
        string memory _durability,
        uint256 _price,
        uint256 _duration
    ) public {
        Auction storage newAuction = auctions[auctionCount];

        newAuction.seller = payable(msg.sender);
        newAuction.image = _image;
        newAuction.brand = _brand;
        newAuction.color = _color;
        newAuction.durability = _durability;
        newAuction.price = _price;
        newAuction.endTime = block.timestamp.add(_duration);
        newAuction.ended = false;

        auctionCount++;
    }

In this step, we define the createAuction function, which allows users to create a new auction on the platform. The function takes several parameters, including the item’s image (_image), brand (_brand), color (_color), durability (_durability), desired price (_price), and the duration of the auction (_duration).

Inside the function, we store the new auction details in the Auction mapping. We access the storage location of the new auction using Auction storage newAuction = auctions[auctionCount];.

We assign the values provided by the user to the respective attributes of the newAuction struct, including the seller’s address (msg.sender), the item’s image, brand, color, durability, price, and the end time of the auction. The end time is calculated by adding the current block timestamp (block.timestamp) to the specified duration.

Lastly, we increment the auctionCount variable to keep track of the number of auctions created on the platform.

Note:

The createAuction function is a crucial part of our auction platform as it allows users to initiate new auctions. By storing the auction details in the Auction mapping, we maintain a record of each auction’s attributes. It’s important to ensure that the input parameters are correctly assigned to the respective struct attributes to accurately represent the auction. The incremented auctionCount variable enables us to track the total number of auctions created on the platform.

Step 5: The getFunction

    function getAuction(uint256 _index)
        public
        view
        returns (
            address payable,
            string memory,
            string memory,
            string memory,
            string memory,
            uint256,
            uint256,
            address,
            uint256,
            bool
        )
    {
        Auction storage auction = auctions[_index];
        return (
            auction.seller,
            auction.image,
            auction.brand,
            auction.color,
            auction.durability,
            auction.price,
            auction.endTime,
            auction.highestBidder,
            auction.highestBid,
            auction.ended
        );
    }

In step 5, we have the getAuction function, which allows us to retrieve information about a specific auction by providing its index.

Using the _index parameter, we access the corresponding auction from the auctions storage array. We then return various details of the auction, including:

  • The seller’s address (auction.seller).

  • Image, brand, color, and durability of the item being auctioned (auction.image, auction.brand, auction.color, auction.durability).

  • The initial price set for the auction (auction.price).

  • The end time of the auction (auction.endTime).

  • The address of the highest bidder (auction.highestBidder).

  • The highest bid amount (auction.highestBid).

  • The status of the auction, indicating whether it has ended or not (auction.ended).

By calling this function and providing the desired auction index, we can retrieve all the relevant information about the auction, facilitating transparency and visibility for participants.

Step 6: The PlaceBid function

    function placeBid(uint256 _index) public payable {
        Auction storage auction = auctions[_index];
        require(!auction.ended, "Auction has ended");
        require(block.timestamp < auction.endTime, "Auction has ended");
        require(msg.value > auction.highestBid, "Bid amount too low");

        if (auction.highestBid > 0) {
            // Refund the previous highest bidder
            require(
                IERC20Token(cUsdTokenAddress).transfer(
                    auction.highestBidder,
                    auction.highestBid
                ),
                "Transfer failed."
            );
        }

        auction.highestBidder = msg.sender;
        auction.highestBid = msg.value;
    }

In step 6, we have the placeBid function, which allows participants to place a bid on a specific auction.

Using the _index parameter, we access the desired auction from the auctions storage array. Before placing a bid, we perform several validation checks:

  • We ensure that the auction has not ended (!auction.ended) and that the current block timestamp is before the auction’s end time (block.timestamp < auction.endTime).

  • We verify that the bid amount is higher than the current highest bid (msg.value > auction.highestBid).

  • If the bid passes all the validation checks, we proceed with the bidding process:

  • If there was a previous highest bidder (auction.highestBid > 0), we refund the previous highest bidder by transferring the bid amount back to their address.

  • The current bidder (msg.sender) becomes the new highest bidder (auction.highestBidder), and the bid amount (msg.value) becomes the new highest bid (auction.highestBid)`.

By calling this function and providing the index of the auction, participants can place bids, compete with other bidders, and potentially become the highest bidder for the auction item.

Step 7: Ending Auction and getAuctionCount function

function endAuction(uint256 _index) public {
        Auction storage auction = auctions[_index];
        require(!auction.ended, "Auction has already ended");
        require(block.timestamp >= auction.endTime, "Auction has not ended yet");

        auction.ended = true;

        if (auction.highestBid > 0) {
            // Transfer the highest bid amount to the seller
            require(
                IERC20Token(cUsdTokenAddress).transfer(
                    auction.seller,
                    auction.highestBid
                ),
                "Transfer failed."
            );
        }
    }

    function getAuctionCount() public view returns (uint256) {
        return auctionCount;
    }
}

In step 7, we have two functions: endAuction and getAuctionCount.

In the endAuction function, we finalize an auction by performing the following steps:

  • We retrieve the auction details from the auctions storage array based on the provided _index.

  • We ensure that the auction has not already ended (!auction.ended) and that the current block timestamp is greater than or equal to the auction’s end time (block.timestamp >= auction.endTime).

  • If the conditions are met, we mark the auction as ended by setting auction.ended to true.

  • If there is a highest bid amount (auction.highestBid > 0), we transfer that amount from the contract’s balance of a specific ERC20 token (cUsdTokenAddress) to the seller’s address using the transfer function.

  • If the transfer fails, an exception is thrown with the error message Transfer failed.

The getAuctionCount function is used to retrieve the total number of auctions that have been created. It simply returns the value of the auctionCount variable.

By calling the endAuction function, we can finalize an ongoing auction, and the highest bid amount is transferred to the seller. The getAuctionCount function provides the count of all auctions created, allowing us to keep track of the total number of auctions available.

CONTRACT DEPLOYMENT

To deploy the Auction platform smart contract on the Celo blockchain, follow the steps below:

  • Install Celo Extension Wallet: Download and install the Celo Extension Wallet from the Google Chrome store. Create a wallet and securely store your key phrase.

  • Fund your wallet: Copy your wallet address and paste it into the Celo Faucet. Confirm the transaction to receive Celo tokens in your wallet.

  • Open Remix and create a new Solidity file: Paste the insurance contract code into the file. Ensure that the Solidity compiler is set to version 0.8.7 or later.

  • Compile the contract: Click the auctiom.sol button in the Solidity Compiler tab in Remix.

  • Deploy the contract: In the Deploy & Run Transactions tab, select the Celo network from the dropdown menu. Connect your wallet to Remix by clicking Connect to wallet. Select auction from the Contract dropdown menu. Click the Deploy button, confirm the transaction in your wallet, and wait for the transaction to be confirmed on the Celo blockchain.

  • Interact with the contract: Once the transaction is confirmed, the auction contract will be deployed on the Celo blockchain. You can interact with it using Remix.

Conclusion

This tutorial covered the implementation of an auction contract. We explained the steps involved in creating and managing auctions, including auction initialization, bidding, expiration, status retrieval, finalization, and additional functionality. By following this tutorial, you can learn how to build an auction system using smart contracts.

NEXT STEPS

Great job! It’s always helpful to provide additional resources for further learning. Don’t hesitate to reach out if you have any more questions or if you need further assistance. Happy learning!

About the author

My name is Ogoyi Thompson, and I’m a web3 developer based in Nigeria, you can connect with me on twitter. I am enthusiastic about working with blockchain technology.

5 Likes

Interesting tutorial, what token standard would you be using for this tutorial?

1 Like

Congratulations on being among the top voted proposals this week! This is now approved by @Celo_Academy for you to get started whenever you’d like. :mortar_board: :seedling:

1 Like

Hi @thompsonogoyi1t i will be reviewing this

@thompsonogoyi1t please add a github repo

@Ikanji i have made the changes

ok, you can move to publish

Great work😎