荷兰拍卖

本章讲解在 Solidity 中,什么是荷兰拍卖,以及荷兰拍卖合约的算法和实现。

视频Bilibili  |  Youtube
官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

拍卖是一种通过竞价来出售商品或服务的交易方式,最终会以最高的出价成交。

1.  拍卖分类

拍卖可以分为多种类别,常见以下三种方式:

1)英国拍卖

英国拍卖,英文为 English Auction。这是一种最为常见的拍卖形式,参与者通过竞价不断提高价格,拍卖会以最高价出售商品或服务。

2)荷兰拍卖

荷兰拍卖,英文为 Dutch Auction。它与英国拍卖相反,是从一个较高的价格开始,然后逐渐降低,第一个愿意接受的出价即成交。

3)封闭式拍卖

封闭式拍卖,英文为 Sealed-Bid Auction。参与者在不知道其他人出价的情况下,私下提交出价,最高价者获胜。

2. 荷兰拍卖算法

荷兰拍卖,英文名称 Dutch Auction,它是一种特殊的拍卖形式。 亦称“减价拍卖”,它是指拍卖标的的竞价由高到低依次递减,直到第一个竞买人应价时击槌成交的一种拍卖,特殊情况下会到达底价。

荷兰拍卖非常适合于在区块链的业务场景,很多 NFT 通过荷兰拍卖发售,其中包括 AzukiWorld of Women,其中 Azuki 通过荷兰拍卖筹集了超过 8000 枚 ETH

项目方非常喜欢这种拍卖形式,主要有三个原因:

  • 价格由最高慢慢下降,能让项目方获得最大的收入。
  • 能够做到自动成交,无人值守。
  • 拍卖持续较长时间,可以避免 gas 大战。

我们把荷兰拍卖结合区块链技术,并根据实际情况,编写成一个智能合约,算法可以分为以下步骤:

1)起拍

拍卖者首先确定一个较高的起拍价和一个最低价,参与者的投标不能低于最低价,也不会高于起拍价。

2)降价

按照预定的价格衰减周期,每隔一段时间,价格就降低一定的幅度。

如果无人投标,那么会直至降低到最低价。

3)结束

一旦有人投标,拍卖立即结束。

4)成交

投标结束后,与唯一的投标者成交,他也是本次拍卖出价最高的竞标者。

3. 荷兰拍卖合约

荷兰拍卖合约包括 4 个函数:开始拍卖函数 startAuction、竞价函数 bid、查看当前最低投标额的函数 getPrice,以及提取合约资金的函数 claim

其中,开始拍卖函数 startAuction 和提取合约资金的函数 claim ,只有合约拥有者有权调用, 而竞价函数 bid  和查看当前最低投标额的函数 getPrice ,可以由任何人调用。

在拍卖开始后,投标者可以通过函数 getPrice 查看当前的当前最低投标额,然后使用竞拍函数 bid ,填入合适的投标金额进行投标。

在函数 bid 中,一旦有人投标,拍卖就会结束。拍卖者可以通过函数 claim 取走合约里投标的资金。

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

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

contract DutchAuction is Ownable {
    uint constant START_PRICE = 10 ether; // 起拍价
    uint constant END_PRICE = 1 ether; // 最低价
    uint constant DURATION = 90 seconds; // 拍卖持续时间
    uint constant INTERNAL = 10 seconds; // 降价间隔时间。每隔一段时间,价格降低一次
    uint constant DEC_PER_STEP = (START_PRICE - END_PRICE) / DURATION * INTERNAL; // 每次的降价幅度
    
    uint public startTime; // 拍卖开始时间
    uint public highestBid; // 当前最高出价
    address public winner; // 竞标胜出者
   
    // 拍卖开始事件
    event AuctionStarted(uint startPrice, uint startTime);
    // 拍卖结束事件
    event AuctionEnded(address winner, uint winningBid);

    // 构造函数,设置合约拥有者
    constructor() Ownable(msg.sender) {
    }

    // 开始拍卖,仅合约拥有者可调用
    function startAuction() public onlyOwner {
        // 记录拍卖开始时间为当前时间戳
        startTime = block.timestamp;
        // 将胜出者地址清零
        winner = address(0);
        // 将当前最高出价清零
        highestBid = 0;
        // 触发拍卖开始事件
        emit AuctionStarted(START_PRICE, startTime); 
    }

    // 竞拍出价
    function bid() public payable {
        // 确保拍卖已经开始
        require(startTime > 0, "auction not yet started"); 
        // 确保拍卖还未结束
        require(winner == address(0), "auction is over");

        // 计算当前拍卖价格
        uint currentPrice = START_PRICE - ((block.timestamp - startTime)/INTERNAL * DEC_PER_STEP);
        if (currentPrice < END_PRICE) {
            currentPrice = END_PRICE;
        }
        // 出价必须高于当前拍卖价格
        require(msg.value >= currentPrice, 
            "bid must be not less than the current price");

        // 更新胜出者为当前出价者
        winner = msg.sender; 
        // 更新最高出价为当前出价
        highestBid = msg.value;
        // 触发竞拍事件
        emit AuctionEnded(msg.sender, msg.value);

        //这里可以加入对竞拍成功者的任意操作
        //..... 
    }

    // 提取合约中的资金。仅合约拥有者可调用
    function claim() public onlyOwner {
        // 提取合约余额资金给合约拥有者
        payable(owner()).transfer(address(this).balance);
    }

    // 查看当前最低投标额
    function getPrice() public view returns(uint) {
        // 确保拍卖已经开始
        require(startTime > 0, "auction not yet started"); 
        // 确保拍卖还未结束
        require(winner == address(0), "auction is over");

        // 计算当前拍卖价格
        uint currentPrice = START_PRICE - ((block.timestamp - startTime)/INTERNAL * DEC_PER_STEP);
        if (currentPrice < END_PRICE) {
            currentPrice = END_PRICE;
        }
        // 返回当期投标需要的金额
        return currentPrice;
    }
}

4. 部署和测试

我们可以把上面编写的荷兰拍卖合约,复制到 Remix 里进行编译,然后部署到区块链上。

点击 startAuction 开始竞标,我们可以通过 getPrice 方法,查看当前所需的最低投标额。

按照我们设定参数,最低投标额从 10 ETH 开始,每 10 秒就会降价 1 ETH,直至将至最低价 1 ETH。

我们可以选择时机,在上方的 Value 处填写出价,点击 bid 进行投标。