Writing Auction Smart Contracts

Prince Anuragi
9 min readDec 25, 2022
Photo by Erol Ahmed on Unsplash

Auctions and bargains are an integral part of our daily life, we interact with numerous things in a day and we go for bargains, sale or auctioning stuff (it even includes your garage sale). So essentially they are important and yes they are also important in DeFi. DeFi protocols such as uniswap, etc. have many auction methods. Today here we will be building something similar to auctioning your NFTs and the highest bidder to win the auction at the end of the auction timer.

There are no prerequisites to read this article but simple understanding of programming would be helpful. So Let’s get started.

Building the Smart Contract

Create a new file named `auction.sol` in remixIDE, and define the standard template for a solidity contract, defining pragma and the contract class. We will be using openzepplin wizard to speed up things, but essentially we want a access control mechanism in our smart contract so based on different roles other accounts can interact with the contract.


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

import "@openzeppelin/contracts/access/AccessControl.sol";

contract AuctionContract is AccessControl {

bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

constructor() {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(ADMIN_ROLE, msg.sender);
_setRoleAdmin(ADMIN_ROLE, DEFAULT_ADMIN_ROLE);
}
}

Now, we have a role based contract which has a ADMIN_ROLE, which is given to whomever deploys this SC in initial constructor, and later on can be given to others. That’s a good start.

We want functionality to allow auction bidders to bid in ERC20 Tokens as well as Native tokens, but in order to keep track of all the allowed ERC20 Token in our Auction Contract will have to make a erc20TokenAddress => allowed, mapping. (We want this so only allowed tokens can be allowed to be a denomination in an active auction).

   mapping(address => bool) public allowedTokens;

Okay, we now have a mapping now we need two function, one to setAllowedToken and other one to revoke Allowed Token. And to make it helpful later, we also add events so that whenever an ERC20 Token is allowed or revoked on our auction contract it can be logged properly via events, which later on can be attached to services like TheGraph or any custom solution.

// Events
event AllowedTokenAdded(address TokenAddress, address AdminAddress);
event RevokedToken(address TokenAddress, address AdminAddress);

function setAllowedToken(
address _erc20Token
) external onlyRole(ADMIN_ROLE) {
allowedTokens[_erc20Token] = true;

emit AllowedTokenAdded(_erc20Token, msg.sender);
}

function revokeAllowedToken(
address _erc20Token
) external onlyRole(ADMIN_ROLE) {
allowedTokens[_erc20Token] = false;

emit RevokedToken(_erc20Token, msg.sender);
}

Okay, good, with the above snippet now, we can add ERC20 Tokens to our AuctionContract. So we are done half of the way already, now what we want is a way to keep track of auctions first. We can make a struct for this purpose as they keeps properties in a single structure making our lives easier.


enum TypeOfAuction {
NATIVE,
TOKEN
}

struct Auction {
address owner;
uint256 startingPrice;
address tokenAddress;
uint256 nftTokenId;
address nftAddress;
TypeOfAuction typeOfAuction;
uint64 endTime;
}

Now we have an Enum also which you can see above, we want that Enum in order to have functionality to create auction in NATIVE Blockchain currency depending upon the network itself (i.e ETC, MATIC, ONE, etc.) and other one to support TOKEN based (ERC20Tokens) auctions where user can bid on a auction in that ERC20Token.

To keep things simple, we are only considering a few fields which are required in an auction generally. We can add as many as we like and as little, but in Smart Contract Realm storage is a commodity similar to gold, so we want to save it. We also need to track when the auction expires and for that we have an `endTime` in the Auction struct.

Now, we need a way to create an auction and make it available to everyone, so they also create auctions for which they want to sell. We are targeting nfts, so we want to make sure that nftAddress is indeed an ERC721 interfaced contract and indeed the auction creator has the access to this nft Token. When the auction is created the creator’s NFT will be submitted to the AuctionContract and when a winner has won, that will be transferred from AuctionContract to the winningBidder, and the amount for which he has won back to the auctionCreator.

So we will first make sure we can receive NFTs in our AuctionContract and to do that we will implement IERC721Receiever from openzepplin library. You can import it at the top of your file.

import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

And inherit the IERC721Receiver as

contract AuctionContract is AccessControl, IERC721Receiver {

Now, we want to implement its function, which tells other ERC721 Contract that our contract can receive their ERC721 NFT Tokens.

function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}

Alright, good work there, this will make other ERC721 Compatible NFT’s to be used in our AuctionContract. Good stuff.

So, what we require is a way to check if the receiver address in the auction is indeed an ERC721 Contract Address and implements that interface. We can create a utility function for the same.

using ERC165Checker for address;
bytes4 public constant IID_IERC721 = type(IERC721).interfaceId;

/**
* Utility Functions
*/
function isERC721(address _nftAddress) internal view returns (bool) {
return _nftAddress.supportsInterface(IID_IERC721);
}

We need to first override default address, so we can use supportsInterface Function on the address type in solidity, and the constant is just the interface id for ERC721 Contract.

Okay good, let’s keep going and make a function to create an auction now. We would require two functions, first one to create an auction for NATIVE cryptocurrency and the other one to create for ERC20 Token based Auction.

In order to keep track of auctions Id, we can make use of counters from openzepplin. And also a mapping from auctionId to the Auction Structure we have built above.

import "@openzeppelin/contracts/utils/Counters.sol";

and the use inside AuctionContract

using Counters for Counters.Counter;

Counters.Counter private _auctionIdCounter;
mapping(uint256 => Auction) public auctions;

Now, functions for creating the auction itself.

function createAuctionNative(
address _nftAddress,
uint256 _nftTokenId,
uint256 _startingPrice,
uint64 _endTime
) external {
require(
isERC721(_nftAddress),
"NFT Address Supplied doesn't not implement ERC721 Interface"
);

require(
_endTime > block.timestamp,
"endTime must be greater than current block.timestamp"
);

bool success = IERC721(_nftAddress).isApprovedForAll(
msg.sender,
address(this)
);
require(success, "Auction Contract is not Approved.");

// Transfer Token to this Contract
IERC721(_nftAddress).safeTransferFrom(
msg.sender,
address(this),
_nftTokenId
);

_createAuction(
msg.sender,
_startingPrice,
address(0),
_nftTokenId,
_nftAddress,
TypeOfAuction.NATIVE,
_endTime
);
}

function createAuctionToken(
address _nftAddress,
uint256 _nftTokenId,
uint256 _startingPrice,
address _erc20Token,
uint64 _endTime
) external {
require(
allowedTokens[_erc20Token],
"ERC20 Token not allowed to Participate."
);

require(
_endTime > block.timestamp,
"endTime must be greater than current block.timestamp"
);

require(
isERC721(_nftAddress),
"NFT Address Supplied doesn't not implement ERC721 Interface"
);

bool success = IERC721(_nftAddress).isApprovedForAll(
msg.sender,
address(this)
);

require(success, "Auction Contract is not Approved.");

// Transfer Token to this Contract
IERC721(_nftAddress).safeTransferFrom(
msg.sender,
address(this),
_nftTokenId
);

_createAuction(
msg.sender,
_startingPrice,
_erc20Token,
_nftTokenId,
_nftAddress,
TypeOfAuction.TOKEN,
_endTime
);
}

function _createAuction(
address _owner,
uint256 _startingPrice,
address _erc20Token,
uint256 _nftTokenId,
address _nftAddress,
TypeOfAuction typeOfAuction,
uint64 _endTime
) internal {
uint256 auctionId = _auctionIdCounter.current();
_auctionIdCounter.increment();
Auction memory newAuction = Auction(
_owner,
_startingPrice,
_erc20Token,
_nftTokenId,
_nftAddress,
typeOfAuction,
_endTime
);

auctions[auctionId] = newAuction;

emit AuctionCreated(
msg.sender,
_startingPrice,
_erc20Token,
_nftTokenId,
_nftAddress,
typeOfAuction,
_endTime
);
}

And since we are at it, let’s write the events for the same.

event AuctionCreated(
address owner,
uint256 startingPrice,
address tokenAddress,
uint256 nftTokenId,
address nftAddress,
TypeOfAuction typeOfAuction,
uint64 endTime
);
event AuctionCompleted(
uint256 auctionId,
address owner,
address winner,
address nftAddress,
uint256 nftTokenId,
uint256 amount
);

Now, our smart contract is able to create auctions, with a startingPrice. Which we need to check when someone bids on that auction that he sends at least that much amount in order to bid on that auction.

In creation of auction we simple check for correct nftAddress first, then we check for endtime of the auction to be in future and then we require , that nft is indeed successfully transferred to our AuctionContract first. And once all is good, we increase the auctionCounter, and store the newly created auction and emit that AuctionCreated event from internal method `_createAuction`.

Good, now we want a way to keep track of bids, there are numerous ways to do it, but most of them requires alot of storage space on the smart contract’s part which can make it a little expensive to operate on that smart contract. So, what we can conclude from a auction is that we wait for highest bidder and always the highest bidder wons. So essentially we only need to keep track for latest highest bidder, and whenever someone bids even higher, the losingBidder will get his funds back from the auction. This process keeps going on until the endTime, and after which the owner of the auction can call completeAuction which transfers nfts and tokens to their destinations.

Let’s start by making the bids structure to keep track of winning bids.

 enum BidState {
ACTIVE,
WINNER
}

struct Bid {
address bidder;
uint256 amount;
BidState state;
}

Now let’s create the mapping which maps from auction to the winningBid.

 mapping(uint256 => Bid) public winningBid;

And create a function to make bidders bid on active auctions. Few steps will happen whenever someone bids.

If there are no winningBids, the winningBid will be created.

If there’s already a winner for that auction, his funds will be transferred back and the new bidder will have the winningBid.

Since we will be interacting with ERC20 tokens, let’s import the interface for it so we can transfer easily and make cross contract calls.

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

Now, Let’s create events for the Bid Creation

 event BidCreated(uint256 auctionId, address bidder, uint256 amount);

And for the final Creating Bids functions.

function bidAuction(uint256 _auctionId, uint256 _amount) external payable {
Auction memory auction = auctions[_auctionId];

require(auction.owner != address(0), "Auction doesn't exists yet !!");

require(
auction.startingPrice <= _amount,
"Can't bid less than the starting price!!"
);

require(
auction.endTime > block.timestamp,
"Auction is expired!! can't bid"
);

// Transfer ERC20 Tokens to this contract
// or
// if native check attached value;

Bid memory losingBid = winningBid[_auctionId];

require(
losingBid.amount < _amount,
"Amount is less than the winning Amount"
);

if (auction.typeOfAuction == TypeOfAuction.NATIVE) {
require(
msg.value == _amount,
"Attached value is less than bidding amount!!"
);

if (losingBid.bidder != address(0)) {
// Transfer the amount back to the winner back
payable(losingBid.bidder).transfer(losingBid.amount);
}

_createBid(_auctionId, msg.sender, _amount);
}

if (auction.typeOfAuction == TypeOfAuction.TOKEN) {
uint256 allowanceTokens = IERC20(auction.tokenAddress).allowance(
msg.sender,
address(this)
);
require(
allowanceTokens >= _amount,
"Not enough Allowance to Auctions contract"
);

if (losingBid.bidder != address(0)) {
bool successLoserBidder = IERC20(auction.tokenAddress).transfer(
losingBid.bidder,
losingBid.amount
);

require(
successLoserBidder,
"Failed to tranfer back ERC20 Tokens of bidding loser"
);
}

bool success = IERC20(auction.tokenAddress).transferFrom(
msg.sender,
address(this),
_amount
);

require(
success,
"Failed to transfer ERC20 Tokens to Auction Contract"
);

_createBid(_auctionId, msg.sender, _amount);
}
}

function _createBid(
uint256 _auctionId,
address _bidder,
uint256 _amount
) internal {
Bid memory bid = Bid(_bidder, _amount, BidState.ACTIVE);
winningBid[_auctionId] = bid;

emit BidCreated(_auctionId, _bidder, _amount);
}

In the function itself, we sequentially check for the amount and type of auction. Existence of auction itself and once all is good, we proceed to transfers of tokens or native currency and finally at last we create the winningBid itself and emit the BidCreated event.

Now, our contract is almost finished, what we require is a final function which the owner of the auction can call only after the auction expires and once it is called, it will transfer nft to winner and tokens back to the auctionOwner.

function completeAuction(uint256 _auctionId) external {
Auction memory auction = auctions[_auctionId];

require(auction.owner != address(0), "Auction doesn't exists yet !!");
require(
auction.endTime < block.timestamp,
"Auction is not expired yet!!"
);

require(
auction.owner == msg.sender || hasRole(ADMIN_ROLE, msg.sender),
"You are not owner for auction"
);

Bid memory highestBid = winningBid[_auctionId];

if (auction.typeOfAuction == TypeOfAuction.NATIVE) {
// Transfer NFT to the Winner
IERC721(auction.nftAddress).safeTransferFrom(
address(this),
highestBid.bidder,
auction.nftTokenId
);

// Transfer Amount to the auction owner
payable(auction.owner).transfer(highestBid.amount);

// Mark Bid as WINNER
highestBid.state = BidState.WINNER;
winningBid[_auctionId] = highestBid;

emit AuctionCompleted(
_auctionId,
auction.owner,
highestBid.bidder,
auction.nftAddress,
auction.nftTokenId,
highestBid.amount
);
}

if (auction.typeOfAuction == TypeOfAuction.TOKEN) {
// Transfer NFT to the Winner
IERC721(auction.nftAddress).safeTransferFrom(
address(this),
highestBid.bidder,
auction.nftTokenId
);

// Transfer ERC20 Tokens to the Auction Owner
bool success = IERC20(auction.tokenAddress).transfer(
auction.owner,
highestBid.amount
);

require(success, "Failed to Transfer ERC20 Tokens!!");

// Mark Bid as WINNER
highestBid.state = BidState.WINNER;
winningBid[_auctionId] = highestBid;

emit AuctionCompleted(
_auctionId,
auction.owner,
highestBid.bidder,
auction.nftAddress,
auction.nftTokenId,
highestBid.amount
);
}
}

And now, we have a contract which can conduct auctions on the blockchain. There can be many use cases for this type of auction from trading nft’s to transferring documents, the sky’s the limit for the imagination. You can also extend this auction and make it better for your use case.

Testing is a very strong part of smart contract building and it is very necessary to write contracts for the same. You can check the github repository linked for all the tests, and the code itself.

Github Repo: https://github.com/prix0007/Auction_Contract.git

Thanks for reading this, I hope you have learned something. If you want to contact me for any questions you have, then you can mail me prix0007@gmail.com.

--

--