WTF Solidity 35. Dutch Auction
Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies.
Twitter: @0xAA_Science | @WTFAcademy_
Community: Discord|Wechat|Website wtf.academy
Codes and tutorials are open source on GitHub: github.com/AmazingAng/WTFSolidity
In this lecture, I will introduce the Dutch Auction and explain how to issue a NFT
using the ERC721
standard through a simplified version of the Azuki
Dutch Auction code.
Dutch Auction
The Dutch Auction is a special type of auction. Also known as a "descending price auction," it refers to an auction where the bidding for the item being auctioned starts high and decreases sequentially until the first bidder bids (reaches or exceeds the bottom price) and it is sold.
In the cryptocurrency world, many NFTs are sold through Dutch auctions, including Azuki
and World of Women
, with Azuki
raising over 8000
ETH
through a Dutch auction.
The project team likes this type of auction for two main reasons:
The price of the Dutch auction slowly decreases from the highest price, allowing the project party to receive the maximum revenue.
The auction lasts a long time (usually more than 6 hours), which can avoid gas wars.
DutchAuction Contract
The code is simplified based on the code of Azuki
. The DutchAuction
contract inherits the ERC721
and Ownable
contracts previously introduced:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/access/Ownable.sol";
import "https://github.com/AmazingAng/WTFSolidity/blob/main/34_ERC721/ERC721.sol";
contract DutchAuction is Ownable, ERC721 {
DutchAuction
State Variables
There are a total of 9
state variables in the contract, of which 6
are related to the auction. They are:
COLLECTION_SIZE
: Total number of NFTs.AUCTION_START_PRICE
: Starting price of the Dutch auction, also the highest price.AUCTION_END_PRICE
: Ending price of the Dutch auction, also the lowest price/floor price.AUCTION_TIME
: Duration of the auction.AUCTION_DROP_INTERVAL
: Time interval when the price drops.auctionStartTime
: Starting time of the auction (blockchain timestamp,block.timestamp
).
uint256 public constant COLLECTOIN_SIZE = 10000; // Total number of NFTs
uint256 public constant AUCTION_START_PRICE = 1 ether; // Starting price (highest price)
uint256 public constant AUCTION_END_PRICE = 0.1 ether; // End price (lowest price/floor price)
uint256 public constant AUCTION_TIME = 10 minutes; // Auction duration. Set to 10 minutes for testing convenience
uint256 public constant AUCTION_DROP_INTERVAL = 1 minutes; // After how long the price will drop once
uint256 public constant AUCTION_DROP_PER_STEP =
(AUCTION_START_PRICE - AUCTION_END_PRICE) /
(AUCTION_TIME / AUCTION_DROP_INTERVAL); // Price reduction per step
uint256 public auctionStartTime; // Auction start timestamp
string private _baseTokenURI; // metadata URI
uint256[] private _allTokens; // Record all existing tokenIds
DutchAuction
Function
There are a total of 9
functions in the Dutch auction contract. We will only introduce the functions related to the auction and not cover the functions related to ERC721
.
- Set auction start time: We declare the current block time as the start time in the constructor. The project owner can also adjust the start time through the
setAuctionStartTime()
function.
The constructor
function initializes a new ERC721
token with the name "WTF Dutch Auction" and the symbol "WTF Dutch Auction". It sets the auctionStartTime
to the current block timestamp.
The setAuctionStartTime
function is a setter function that updates the auctionStartTime
variable. It can only be called by the owner of the contract.
- Get the real-time auction price: The function
getAuctionPrice()
calculates the real-time auction price based on the current block time and relevant auction state variables.
If block.timestamp
is less than the start time, the price is the highest price AUCTION_START_PRICE
;
If block.timestamp
is greater than the end time, the price is the lowest price AUCTION_END_PRICE
;
If block.timestamp
is between the start and end times, the current decay price is calculated.
// Get real-time auction price
function getAuctionPrice()
public
view
returns (uint256)
{
if (block.timestamp < auctionStartTime) {
return AUCTION_START_PRICE;
}else if (block.timestamp - auctionStartTime >= AUCTION_TIME) {
return AUCTION_END_PRICE;
} else {
uint256 steps = (block.timestamp - auctionStartTime) /
AUCTION_DROP_INTERVAL;
return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP);
}
}
- User auctions and mints
NFT
: Users participate in a Dutch auction and mintNFT
by calling theauctionMint()
function to payETH
.
First, the function checks if the auction has started or if the number of NFTs
has exceeded the limit. Then, the contract calculates the auction cost based on the number of minted NFTs
and using the getAuctionPrice()
function. It also checks if the user has enough ETH
to participate. If the user has enough ETH
, the contract mints NFTs
and refunds any excess ETH
. Otherwise, the transaction is reverted.
// the auction mint function
function auctionMint(uint256 quantity) external payable{
uint256 _saleStartTime = uint256(auctionStartTime); // uses local variable to reduce gas
require(
_saleStartTime != 0 && block.timestamp >= _saleStartTime,
"sale has not started yet"
); // checks if the start time of auction has been set and auction has started
require(
totalSupply() + quantity <= COLLECTOIN_SIZE,
"not enough remaining reserved for auction to support desired mint amount"
); // checks if the number of NFTs has exceeded the limit
uint256 totalCost = getAuctionPrice() * quantity; // calculates the cost of mint
require(msg.value >= totalCost, "Need to send more ETH."); // checks if the user has enough ETH to pay
// Mint NFT
for(uint256 i = 0; i < quantity; i++) {
uint256 mintIndex = totalSupply();
_mint(msg.sender, mintIndex);
_addTokenToAllTokensEnumeration(mintIndex);
}
// refund excess ETH
if (msg.value > totalCost) {
payable(msg.sender).transfer(msg.value - totalCost); //please check is there any risk of reentrancy attack
}
}
- Withdrawal of raised
ETH
by the project owner: The project owner can withdraw theETH
raised in the auction by using the functionwithdrawMoney()
.
// the withdraw function, onlyOwner modifier
function withdrawMoney() external onlyOwner {
(bool success, ) = msg.sender.call{value: address(this).balance}(""); // how to use call function please see lession #22
require(success, "Transfer failed.");
}
Demo of Remix
Contract Deployment: First, deploy the
DutchAuction.sol
contract and set the auction start time through thesetAuctionStartTime()
function. In this example, the start time is March 19, 2023, 14:34 am, corresponding to UTC time 1679207640. You can search for the corresponding time on a tool website (such as here) during the experiment.Dutch Auction: Then, you can use the
getAuctionPrice()
function to get the current auction price. It can be observed that the price before the auction starts isstarting price AUCTION_START_PRICE
. As the auction proceeds, the auction price gradually decreases until it reaches thereserve price AUCTION_END_PRICE
, after which it no longer changes.Mint Operation: Complete mint through the
auctionMint()
function. In this example, because the time has exceeded the auction time, only thereserve price
was spent to complete the auction.Withdrawal of
ETH
: You can directly send the raisedETH
to the contract creator's address through thewithdrawMoney()
function.
Summary
In this lecture, we introduced the Dutch auction and explained how to issue ERC721
standard NFT
through Dutch auction
using a simplified version of the Azuki
Dutch auction code. The most expensive NFT
I auctioned was a piece of music NFT
by musician Jonathan Mann
. What about you?