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 specificERC20 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 clickingConnect to wallet
. Selectauction
from theContract
dropdown menu. Click theDeploy
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.