Skip to main content

Create an NFT auction

The 'English Auction' consists of two parts:

  • A contract representing an NFT
  • A contract containing the auction logic

Draft the NFT contract

The full code of the NFT contract:

pragma solidity ^0.8.0;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";
import "@nilfoundation/smart-contracts/contracts/NilCurrencyBase.sol";

/**
* @title NFT
* @author =nil; Foundation
* @notice The contract represents an NFT that can be minted and transferred.
*/
contract NFT is NilCurrencyBase {
/**
* @dev The property locks down the contract after the NFT has been transferred.
*/
bool private hasBeenSent = false;

/**
* @notice A 'wrapper' over mintCurrencyInternal(). Only one NFT can be minted.
*/
function mintNFT() public {
require(totalSupply == 0, "NFT has already been minted");
require(!hasBeenSent, "NFT has already been sent");
mintCurrencyInternal(1);
}

/**
* @notice The function sends the NFT to the provided address.
* @param dst The address to which the NFT must be sent.
*/
function sendNFT(address dst) public {
require(!hasBeenSent, "NFT has already been sent");
Nil.Token[] memory nft = new Nil.Token[](1);
nft[0].id = getCurrencyId();
nft[0].amount = 1;
Nil.asyncCallWithTokens(
dst,
msg.sender,
msg.sender,
0,
Nil.FORWARD_REMAINING,
0,
nft,
""
);
hasBeenSent = true;
}

/**
*
* @notice The empty override ensures that the NFT can only be minted via mintNFT().
*/
function mintCurrency(uint256 amount) public override onlyExternal {}

/**
*
* @notice The empty override ensures that the NFT cannot be burned.
*/
function burnCurrency(uint256 amount) public override onlyExternal {}
}

The contract overrides the inherited onlyExternal methods for minting and burning, ensuring fine control over the total supply of the NFT. It also provides a 'wrapper' method for transferring a minted NFT to another contract.

Implement the auction contract

Contract definition

The initial contract definition and import statements:

pragma solidity ^0.8.0;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title EnglishAuction
* @author =nil; Foundation
* @notice This contract implements an auction where contracts can place bids
* @notice and the contract owner decides when to start and end the auction.
*/
contract EnglishAuction is Ownable {}

In addition to Nil.sol, the contract imports Ownable.sol. This is done so that only the contract owner can manage when the auction starts and end.

Contract properties and constructor

The auction contract has the following constructor and properties:

/**
* @notice These properties store the address of the NFT contract
* and check whether the auction is still going.
*/
address private nft;
bool public isOngoing;

/**
* @notice These properties store information about all bids as well as
* the current highest bid and bidder.
*/
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public bids;

/**
* @notice The constructor stores the address of the NFT contract
* and accepts the initial bid.
* @param _nft The address of the NFT contract.
*/
constructor(address _nft) payable Ownable(msg.sender) {
nft = _nft;
isOngoing = false;
highestBid = msg.value;
}

The auction is deployed by taking the address of the NFT contract and setting the initial highestBid. Bid information is kept in the bids mapping while the highestBidder is a separate property for convenience.

Auction logic

The contract contains three functions responsible for handling the auction logic:

/**
* @notice The function submits a bid for the auction.
*/
function bid() public payable {
require(isOngoing, "the auction has not started");
require(
msg.value > highestBid,
"the bid does not exceed the current highest bid"
);

if (highestBidder != address(0)) {
bids[highestBidder] += highestBid;
}

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

emit Bid(msg.sender, msg.value);
}

/**
* @notice This function exists so a bidder can withdraw their funds
* if they change their mind.
*/
function withdraw() public {
uint256 bal = bids[msg.sender];
bids[msg.sender] = 0;

Nil.asyncCall(msg.sender, address(this), bal, "");

emit Withdraw(msg.sender, bal);
}

/**
* @notice This function ends the auction and requests the NFT contract
* to provide the NFT to the winner.
*/
function end() public onlyOwner {
require(isOngoing, "the auction has not started");

isOngoing = false;

Nil.asyncCall(
nft,
address(this),
address(this),
0,
Nil.FORWARD_REMAINING,
0,
abi.encodeWithSignature("sendNFT(address)", highestBidder)
);

emit End(highestBidder, highestBid);
}

The start() and end() functions are marked as Ownable so that only the contract owner can start and end the auction. When the auction beings, the contract sends a message for minting the NFT and starts accepting bets. After the auction concludes, a new async call is sent so that the NFT is transferred to the winner.

Full code

Here is the full code of the auction contract:

pragma solidity ^0.8.0;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title EnglishAuction
* @author =nil; Foundation
* @notice This contract implements an auction where contracts can place bids
* @notice and the contract owner decides when to start and end the auction.
*/
contract EnglishAuction is Ownable {
event Start();
event Bid(address indexed sender, uint256 amount);
event Withdraw(address indexed bidder, uint256 amount);
event End(address winner, uint256 amount);

/**
* @notice These properties store the address of the NFT contract
* and check whether the auction is still going.
*/
address private nft;
bool public isOngoing;

/**
* @notice These properties store information about all bids as well as
* the current highest bid and bidder.
*/
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public bids;

/**
* @notice The constructor stores the address of the NFT contract
* and accepts the initial bid.
* @param _nft The address of the NFT contract.
*/
constructor(address _nft) payable Ownable(msg.sender) {
nft = _nft;
isOngoing = false;
highestBid = msg.value;
}

/**
* @notice This function starts the auction and sends a message
* for minting the NFT.
*/
function start() public onlyOwner {
require(!isOngoing, "the auction has already started");

Nil.asyncCall(
nft,
address(this),
address(this),
0,
Nil.FORWARD_REMAINING,
0,
abi.encodeWithSignature("mintNFT()")
);

isOngoing = true;

emit Start();
}

/**
* @notice The function submits a bid for the auction.
*/
function bid() public payable {
require(isOngoing, "the auction has not started");
require(
msg.value > highestBid,
"the bid does not exceed the current highest bid"
);

if (highestBidder != address(0)) {
bids[highestBidder] += highestBid;
}

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

emit Bid(msg.sender, msg.value);
}

/**
* @notice This function exists so a bidder can withdraw their funds
* if they change their mind.
*/
function withdraw() public {
uint256 bal = bids[msg.sender];
bids[msg.sender] = 0;

Nil.asyncCall(msg.sender, address(this), bal, "");

emit Withdraw(msg.sender, bal);
}

/**
* @notice This function ends the auction and requests the NFT contract
* to provide the NFT to the winner.
*/
function end() public onlyOwner {
require(isOngoing, "the auction has not started");

isOngoing = false;

Nil.asyncCall(
nft,
address(this),
address(this),
0,
Nil.FORWARD_REMAINING,
0,
abi.encodeWithSignature("sendNFT(address)", highestBidder)
);

emit End(highestBidder, highestBid);
}
}