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);
}
}