# 荷兰拍NFT

By [马赫斯.7](https://paragraph.com/@0xmaahes) · 2023-06-05

---

发币模式
----

### IEO

发行在中心化交易所，上币流程麻烦，中心化。

### IDO

发行在去中心交易所，上币需要流动性成本高，面临价格和gas飙升、机器人/巨鲸等问题、发行效率低不利于价格发现。

### LBP(Liquidity bootstrapping pool)

自定义流动性池的比例 [balancer](https://balancer.fi/)，属于荷兰拍的变种。

荷兰拍是什么
------

是一种特殊的拍卖形式，也称为“减价拍卖”，在荷兰拍卖中，拍卖标的的竞价由高到低依次递减直到第一个竞买人应价。 荷兰式拍卖有两个显著特点。

*   一，价格随着一定的时间间隔，按照**事先确定的降价阶梯，由高到低递减。**
    
*   二，所有买受人(即买到物品的人)都\*\*以最后的竞价(即所有买受人中的最低出价)成交。\*\*NFT荷兰拍的实现
    

![](https://storage.googleapis.com/papyrus_images/0b0f3b1c210ad489acfd5a5fae45ca53d963b86c46cfde1d82b0ac8263a0448a.png)

NFT荷兰拍的实现
---------

    
    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.;
    
    import "@openzeppelin/contracts/access/Ownable.sol";
    import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
    import "./ERC721A.sol";
    import "@openzeppelin/contracts/utils/Strings.sol";
    
    contract Azuki is Ownable, ERC721A, ReentrancyGuard {
      uint256 public immutable maxPerAddressDuringMint;
      uint256 public immutable amountForDevs;
      uint256 public immutable amountForAuctionAndDev;
    
      struct SaleConfig {
        uint32 auctionSaleStartTime;
        uint32 publicSaleStartTime;
        uint64 mintlistPrice;
        uint64 publicPrice;
        uint32 publicSaleKey;
      }
    
      SaleConfig public saleConfig;
    
      mapping(address => uint256) public allowlist;
    
      constructor(
        uint256 maxBatchSize_,
        uint256 collectionSize_,
        uint256 amountForAuctionAndDev_,
        uint256 amountForDevs_
      ) ERC721A("Azuki", "AZUKI", maxBatchSize_, collectionSize_) {
        maxPerAddressDuringMint = maxBatchSize_;
        amountForAuctionAndDev = amountForAuctionAndDev_;
        amountForDevs = amountForDevs_;
        require(
          amountForAuctionAndDev_ <= collectionSize_,
          "larger collection size needed"
        );
      }
    
      modifier callerIsUser() {
        require(tx.origin == msg.sender, "The caller is another contract");
        _;
      }
    
      function auctionMint(uint256 quantity) external payable callerIsUser {
        uint256 _saleStartTime = uint256(saleConfig.auctionSaleStartTime);
        require(
          _saleStartTime != 0 && block.timestamp >= _saleStartTime,
          "sale has not started yet"
        );
        require(
          totalSupply() + quantity <= amountForAuctionAndDev,
          "not enough remaining reserved for auction to support desired mint amount"
        );
        require(
          numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint,
          "can not mint this many"
        );
        uint256 totalCost = getAuctionPrice(_saleStartTime) * quantity;
        _safeMint(msg.sender, quantity);
        refundIfOver(totalCost);
      }
    
      function allowlistMint() external payable callerIsUser {
        uint256 price = uint256(saleConfig.mintlistPrice);
        require(price != 0, "allowlist sale has not begun yet");
        require(allowlist[msg.sender] > 0, "not eligible for allowlist mint");
        require(totalSupply() + 1 <= collectionSize, "reached max supply");
        allowlist[msg.sender]--;
        _safeMint(msg.sender, 1);
        refundIfOver(price);
      }
    
      function publicSaleMint(uint256 quantity, uint256 callerPublicSaleKey)
        external
        payable
        callerIsUser
      {
        SaleConfig memory config = saleConfig;
        uint256 publicSaleKey = uint256(config.publicSaleKey);
        uint256 publicPrice = uint256(config.publicPrice);
        uint256 publicSaleStartTime = uint256(config.publicSaleStartTime);
        require(
          publicSaleKey == callerPublicSaleKey,
          "called with incorrect public sale key"
        );
    
        require(
          isPublicSaleOn(publicPrice, publicSaleKey, publicSaleStartTime),
          "public sale has not begun yet"
        );
        require(totalSupply() + quantity <= collectionSize, "reached max supply");
        require(
          numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint,
          "can not mint this many"
        );
        _safeMint(msg.sender, quantity);
        refundIfOver(publicPrice * quantity);
      }
    
      function refundIfOver(uint256 price) private {
        require(msg.value >= price, "Need to send more ETH.");
        if (msg.value > price) {
          payable(msg.sender).transfer(msg.value - price);
        }
      }
    
      function isPublicSaleOn(
        uint256 publicPriceWei,
        uint256 publicSaleKey,
        uint256 publicSaleStartTime
      ) public view returns (bool) {
        return
          publicPriceWei != 0 &&
          publicSaleKey != 0 &&
          block.timestamp >= publicSaleStartTime;
      }
    
      uint256 public constant AUCTION_START_PRICE = 1 ether;
      uint256 public constant AUCTION_END_PRICE = 0.15 ether;
      uint256 public constant AUCTION_PRICE_CURVE_LENGTH = 340 minutes;
      uint256 public constant AUCTION_DROP_INTERVAL = 20 minutes;
      uint256 public constant AUCTION_DROP_PER_STEP =
        (AUCTION_START_PRICE - AUCTION_END_PRICE) /
          (AUCTION_PRICE_CURVE_LENGTH / AUCTION_DROP_INTERVAL);
    
      function getAuctionPrice(uint256 _saleStartTime)
        public
        view
        returns (uint256)
      {
        if (block.timestamp < _saleStartTime) {
          return AUCTION_START_PRICE;
        }
        if (block.timestamp - _saleStartTime >= AUCTION_PRICE_CURVE_LENGTH) {
          return AUCTION_END_PRICE;
        } else {
          uint256 steps = (block.timestamp - _saleStartTime) /
            AUCTION_DROP_INTERVAL;
          return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP);
        }
      }
    
      function endAuctionAndSetupNonAuctionSaleInfo(
        uint64 mintlistPriceWei,
        uint64 publicPriceWei,
        uint32 publicSaleStartTime
      ) external onlyOwner {
        saleConfig = SaleConfig(
          0,
          publicSaleStartTime,
          mintlistPriceWei,
          publicPriceWei,
          saleConfig.publicSaleKey
        );
      }
    
      function setAuctionSaleStartTime(uint32 timestamp) external onlyOwner {
        saleConfig.auctionSaleStartTime = timestamp;
      }
    
      function setPublicSaleKey(uint32 key) external onlyOwner {
        saleConfig.publicSaleKey = key;
      }
    
      function seedAllowlist(address[] memory addresses, uint256[] memory numSlots)
        external
        onlyOwner
      {
        require(
          addresses.length == numSlots.length,
          "addresses does not match numSlots length"
        );
        for (uint256 i = 0; i < addresses.length; i++) {
          allowlist[addresses[i]] = numSlots[i];
        }
      }
    
      // For marketing etc.
      function devMint(uint256 quantity) external onlyOwner {
        require(
          totalSupply() + quantity <= amountForDevs,
          "too many already minted before dev mint"
        );
        require(
          quantity % maxBatchSize == 0,
          "can only mint a multiple of the maxBatchSize"
        );
        uint256 numChunks = quantity / maxBatchSize;
        for (uint256 i = 0; i < numChunks; i++) {
          _safeMint(msg.sender, maxBatchSize);
        }
      }
    
      // // metadata URI
      string private _baseTokenURI;
    
      function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
      }
    
      function setBaseURI(string calldata baseURI) external onlyOwner {
        _baseTokenURI = baseURI;
      }
    
      function withdrawMoney() external onlyOwner nonReentrant {
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success, "Transfer failed.");
      }
    
      function setOwnersExplicit(uint256 quantity) external onlyOwner nonReentrant {
        _setOwnersExplicit(quantity);
      }
    
      function numberMinted(address owner) public view returns (uint256) {
        return _numberMinted(owner);
      }
    
      function getOwnershipData(uint256 tokenId)
        external
        view
        returns (TokenOwnership memory)
      {
        return ownershipOf(tokenId);
      }
    }
    

### Azuki荷兰拍规则

[拍卖的第一阶段是荷兰拍卖会，将于太平洋标准时间 1 月 12 日星期三上午 10:00 开始。大约 8700 个 Azukis 将以 1 ETH 的起始价格用于铸造。价格将每 20 分钟下降 0.05 ETH，直到达到 0.15 ETH。](https://mirror.xyz/gamfi.eth/JfXEXEpdxww8_STrYNw2Dk6xTQcyFAkN6g3eyrAfKAo) Azuki的荷兰拍模式本质上是一种变种荷兰拍，最后的价格停留在最低价0.15 ETH，这能提高底价，让Azuki获得大量的收入以及避免gas战争。

使用荷兰拍的一些NFT项目
-------------

*   Azuki NFT
    
*   Art Blocks
    
*   Mutant Ape Yacht Club
    
*   Nanopass
    
*   Pixelmon
    

扩展阅读
----

[什么是荷兰式拍卖？为什么它在NFT领域很重要？](https://zhuanlan.zhihu.com/p/514505980?utmid=0) [LBP 与 荷兰拍](https://maahes.notion.site/LBP-31e042b4cd7d4dd2942a59dbcd993dca) [Gradual Dutch Auctions - Paradigm](https://maahes.notion.site/Gradual-Dutch-Auctions-Paradigm-b1d4a971422f49caa3dac8daf100b3d3) [YGG IDO 规则变更：将采取荷兰式拍卖！](https://yieldguild.medium.com/ygg-ido-%E8%A7%84%E5%88%99%E5%8F%98%E6%9B%B4-%E5%B0%86%E9%87%87%E5%8F%96%E8%8D%B7%E5%85%B0%E5%BC%8F%E6%8B%8D%E5%8D%96-ee96e49ec866) [NFT复盘 Azuki](https://mirror.xyz/gamfi.eth/JfXEXEpdxww8_STrYNw2Dk6xTQcyFAkN6g3eyrAfKAo) [荷兰式拍卖的缘起及NFT荷拍三大里程碑](https://www.youtube.com/watch?v=WIrKIODyjDg) [英式拍到荷兰拍](https://www.blocktempo.com/from-british-to-netherland-auction-see-how-traditional-auction-participate-in-nft-auction/)

---

*Originally published on [马赫斯.7](https://paragraph.com/@0xmaahes/nft)*
