How to Develop and Implement an Auction Smart Contract on Lisk Testnet
In this post, I will walk you through the process of developing an auction smart contract and deploying it on the Lisk testnet.
Development Tools
First, I will list all the tools we will use for the development of the smart contract. They are:
Vscode: Vs code is the development IDE I will be using.
Foundry Forge: The development environment. You can check out the installation process in5 their installation guide.
OpenZeppellin: This library provides an easy way to create ERC20 tokens that will be used in our smart contract. You can learn more about it here.
Then let's get started.
Setting up the environment
First, we will create the folder and create the environment in it.
mkdir auction_contract
cd auction_contract
forge init .
After creating the environment, we will then install the Openzeppellin library
forge install OpenZeppelin/openzeppelin-contracts
Now we can start creating our auction contract in src/auction.sol file.
// auction.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./token.sol";
contract Auction {
struct auctionItem {
bytes32 id;
string title;
string description;
uint256 highestBid;
uint256 minPrice;
bool sold;
address seller;
address highestBidder;
uint time;
}
}
In the contract, I defined a struct to represent an item in the auction. This struct will hold the item's title, description, minimum price, the highest bid, the highest bidder, the time when the auction is added to the contract, and the seller.
Next, I will create a contract for the token that will be used in our contract and also add a function to be used for minting the token.
// token.sol
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract AdeToken is ERC20 {
constructor() ERC20("Ade token", "ADETK") {}
/**
* Mint token
* @param to address to send token to
* @param amount amount of token
*/
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
Let's go on
contract Auction {
struct auctionItem {
bytes32 id;
string title;
string description;
uint256 highestBid;
uint256 minPrice;
bool sold;
address seller;
address highestBidder;
uint time;
}
string public name; // name of the auction
uint public immutable NUM_ITEMS_ALLOWED; // number of items allowed in the auction
uint private bidTime; // time allowed for bidding for items
uint numItems = 0; // number of items added to the auction
// one ether == 50 tokens
uint256 private multiplier = 50; // a multiplier for the amount of token that can be bought per ether
uint256 public totalSupply = 0; // total supply of tokens
bytes[] public listOfAuctionItems; // list of items added to the auction
// registers the auction item to the records with title as key
mapping(string => auctionItem) private auctionRecords;
mapping(address => uint256) public bidders;
mapping(address => uint256) private amountToBePaid;
event numberOfItemsAdded(uint256 numberOfItemsAllowed);
event addedAuctionItem(address creator, string title);
event ItemBought(address buyer, uint256 amount);
event ItemBid(address bidder, uint256 amount);
event ItemAuctionEnded(address winner, uint256 amount);
event sellerPaid(address seller, uint256 amount);
event bidderRegistered(address bidder, uint256 amount);
event returnedBidderMoney(address bidder, uint256 amount);
AdeToken public token = new AdeToken();
Here, I added some parameters to the contract.
mapping(string => auctionItem) private auctionRecords;
: This is a private state variable that maps an item's title to its auctionItem
struct.
mapping(address => uint256) public bidders;
: This is a public state variable that maps a bidder's address to their bid amount.
mapping(address => uint256) private amountToBePaid;
: This is a private state variable that maps an address to the amount to be paid.
event
: These are event declarations. Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listens for these events.
AdeToken public token = new AdeToken();
: This line creates a new instance of the AdeToken
contract and assigns it to the public state variable token
.
After that, I will add a constructor and an ID generator for my auction items. The constructor will mint 2000 tokens ADETK tokens for the contract when the contract is initialized.
constructor(uint256 _number, string memory _name, uint _bidTime) {
NUM_ITEMS_ALLOWED = _number;
name = _name;
bidTime = _bidTime;
emit numberOfItemsAdded(_number);
token.mint(address(this), 2000);
totalSupply += 2000;
}
/**
* creates a bytes32 array from two strings
* @param str1 first string
* @param str2 second string
*/
function generateID(string memory str1, string memory str2) internal pure returns (bytes32) {
return(keccak256(abi.encodePacked(str1, str2)));
}
Then I will create functions to interact with the token from the contract
/**
* @dev Mint token
*/
function mintToken() private {
token.mint(address(this), 2000);
totalSupply += 2000;
}
/**
* @dev transfer token to bidder. If current supply is not enough, it mints more
* @param to - address of the bidder
* @param amount - amount of tokens to be sent
* Emits {returnedBidderMoney} event
*/
function transferToken(address to, uint256 amount) private {
if (totalSupply < amount) {
mintToken();
}
token.transfer(to, amount);
totalSupply -= amount;
}
/**
* @dev withdraw token from the owner and adds it to the total supply
* @param amount amount of tokens to be withdrawn
*/
function withdrawToken(uint256 amount) private {
token.transfer(address(this), amount);
totalSupply += amount;
}
Functions to create a bidder and for the bidder to interact with the auction contract. In the registerBidder and getMoreTokens functions, the user will pay ethers to be converted to tokens for them and added to their account. Their tokens will also be recorded in the bidders
mapping.
/**
* @dev Registers bidder
* Emits {bidderRegistered} event
*/
function registerBidder() payable public {
require(msg.value > 0, "You cannot send zero funds");
uint256 amount = (msg.value / 1 ether) * 50;
transferToken(msg.sender, amount);
bidders[msg.sender] = amount;
emit bidderRegistered(msg.sender, amount);
}
/**
* @dev Bidder get more tokens
*/
function getMoreTokens() payable public {
require(msg.value > 0, "You cannot send zero funds");
require(bidders[msg.sender] > 0, "You have to register first");
uint256 amount = (msg.value / 1 ether) * 50;
transferToken(msg.sender, amount);
bidders[msg.sender] += amount;
}
/**
* @dev Removes the bidders tokens and return their remaining money in ethers
* Emits {returnedBidderMoney} event
*/
function returnBidderMoney() public {
require(bidders[msg.sender] > 0, "We do not have your money");
uint256 amount = (bidders[msg.sender] / 50) * 1 ether;
withdrawToken(bidders[msg.sender]);
bidders[msg.sender] = 0;
payable(msg.sender).transfer(amount);
emit returnedBidderMoney(msg.sender, amount);
}
Functions to add items to the auction, bid for items, and pay for the item when the time of the item bidding has elapsed. In the payMoney function, 90 percent of the highest bid is paid to the seller and the remnant is charged as the commission fee.
/**
* @dev adds items to the auction
* Emits {addedAuctionItem} event
*/
function addItem(string memory _title, string memory _description, uint256 minPrice, address seller) public {
require(numItems <= NUM_ITEMS_ALLOWED, "Slot is full");
bytes32 id = _title.generateID(_description);
auctionItem memory item = auctionItem(id, _title, _description, 0, minPrice, false, seller, address(0), block.timestamp);
auctionRecords[_title] = item;
listOfAuctionItems.push(bytes(_title));
numItems += 1;
emit addedAuctionItem(msg.sender, _title);
}
/**
* @dev bid for item
* Emits {ItemBid} event
*/
function bidForItem(uint256 amount, string memory _item) public {
auctionItem memory item = auctionRecords[_item];
address bidder = msg.sender;
require(amount >= item.minPrice, "Bid must be above the min price");
require(bidders[bidder] >= amount, "Bros !, you no get money!!!");
require(item.time + bidTime > block.timestamp, "Bid for this item is over");
if (amount > item.highestBid) {
item.highestBid = amount;
item.highestBidder = bidder;
auctionRecords[_item] = item;
bidders[bidder] -= amount;
}
emit ItemBid(bidder, amount);
}
/**
* @dev Returns auction item based on its `_title`
*/
function getItem(string memory _title) public view returns(auctionItem memory) {
return (auctionRecords[_title]);
}
/**
* @dev Returns auction item based on its `_title`
*/
function triggerSold(string memory _title) public {
if (block.timestamp < auctionRecords[_title].time + bidTime) {
revert("Auction has not ended");
}
auctionRecords[_title].sold = true;
emit ItemAuctionEnded(auctionRecords[_title].highestBidder, auctionRecords[_title].highestBid);
}
/**
* @dev Returns the winner of an auction `item`
*/
function showWiner(string memory _item) public view returns (address) {
auctionItem memory item = auctionRecords[_item];
require(item.time + bidTime < block.timestamp, "Bid for this item is not over");
return (item.highestBidder);
}
/**
* @dev Pays money for the `_item' won from the auction
* Emits {ItemBought} event
*/
function payMoney(string memory _item) public {
auctionItem memory item = auctionRecords[_item];
require(msg.sender == item.highestBidder, "You are not the highest bidder");
withdrawToken(item.highestBid);
amountToBePaid[item.seller] = item.highestBid - item.highestBid / 10;
bidders[msg.sender] -= item.highestBid;
emit ItemBought(msg.sender, item.highestBid);
}
/**
* @dev Returns amount to be paid to a `seller`
*/
function getSellerAmount(address seller) public view returns(uint256) {
return (amountToBePaid[seller]);
}
Functions for sellers to get their tokens from the smart contract and also to convert their tokens to ethers.
/**
* @dev Pay seller his money
* Emits {sellerPaid} event
*/
function paySeller(address seller) public {
if (amountToBePaid[seller] <= 0) {
revert("You cannot be paid");
}
transferToken(seller, amountToBePaid[seller]);
emit sellerPaid(seller, amountToBePaid[seller]);
amountToBePaid[seller] = 0;
}
/**
* @dev convert the tokens to ethers
* Emits {returnedBidderMoney} event
*/
function cashOut(uint256 amount) public {
uint256 amt = (amount / 50) * 1 ether;
withdrawToken(amount);
payable(msg.sender).transfer(amt);
}
}
We have come to the end of creating our auction smart contract.
Building and Deploying the contract
To run the file you can use
forge build
To deploy the smart contract to the lisk testnet, use this command
forge create --rpc-url https://rpc.sepolia-api.lisk.com \
--etherscan-api-key 123 \
--verify \
--verifier blockscout \
--verifier-url https://sepolia-blockscout.lisk.com/api \
--private-key <PRIVATE_KEY> \
src/auction.sol:Auction --constructor-args 25 "Adeyemi & Co Auction House" 600
You should get something like this
[⠆] Compiling...piling...
[⠃] Compiling 1 files with 0.8.25
[⠊] Solc 0.8.25 finished in 2.50s
Compiler run successful!
Deployer: 0xE265a72c0F8af149492c4d509807b97dE5E6b53B
Deployed to: 0x3876c57dBCDaCfE288d6D5f875268c916C7b0c3f
Transaction hash: 0x36af36861a7c7b89f19bcb9d2d81d9fa7c0e83c9268f272b22acd819b90a060c
Starting contract verification...
Waiting for blockscout to detect contract deployment...
Start verifying contract `0x3876c57dBCDaCfE288d6D5f875268c916C7b0c3f` deployed on 4202
Submitting verification for [src/auction.sol:Auction] 0x3876c57dBCDaCfE288d6D5f875268c916C7b0c3f.
Submitted contract for verification:
Response: `OK`
GUID: `3876c57dbcdacfe288d6d5f875268c916c7b0c3f66662542`
URL: https://sepolia-blockscout.lisk.com/address/0x3876c57dbcdacfe288d6d5f875268c916c7b0c3f
Contract verification status:
Response: `OK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
After the smart contract is deployed and verified, you can interact with it by calling its public functions.
Conclusion
In this post, you have learned how to develop and deploy an auction smart contract on the Lisk testnet using tools like VS Code, Foundry Forge, and OpenZeppelin. The guide covers setting up the development environment, creating the auction and token contracts, implementing key functionalities such as bidding and token management, and finally, deploying and verifying the contract on the Lisk testnet.
Subscribe to my newsletter
Read articles from Adejumo Adeyemi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by