如何编写一个拍卖的智能合约

710 阅读4分钟

拍卖的方式有几种,其中有两种概念你需要先了解下,一种是公开拍卖(open auction),一种叫盲拍(blind auction)。简单来讲就是,前一种拍卖大家都能互相看到对方的出价,而后一种则看不到。

先看一个简单的公开拍卖。

contract SimpleAuction {

    //拍卖的受益人
    address payable public beneficiary;
    //拍卖的结束时间
    uint public auctionEndTime;

    // 最高出价的人
    address public highestBidder;
    // 最高出价的价格
    uint public highestBid;

    // 这个map用来存放出价的人以及对应的出价,便于拍卖结束后退还
    mapping(address => uint) pendingReturns;

    //标识拍卖结束了,一旦结束就不能改了
    bool ended;
    

payable表示这个地址可以接收以太币。因为存放的是受益人的地址当然是可以接受以太币的。uctionEndTime是一个时间戳变量,表示拍卖的结束时间。

// 用来记录当前出价最高的事件
event HighestBidIncreased(address bidder, uint amount);
// 用来记录拍卖结束后
event AuctionEnded(address winner, uint amount);

这里是定义两个事件,用来记录状态的变更。

/// 拍卖已经结束
error AuctionAlreadyEnded();
    
/// 已经有更高的出价者了
error BidNotHighEnough(uint highestBid);
    
/// 拍卖还未结束
error AuctionNotYetEnded();
   
/// auctionEnd 方法已经被调用了
error AuctionEndAlreadyCalled();

这里有几个之前没讲过的知识点,首先是注释用/// 三斜杠,这样的注释可以用来生成文档(natSpec)。这里只是一个简单的示例,还可以定义的很复杂,比如:

/// @title A simulator for trees
/// @author Larry A. Gardner
/// @notice You can use this contract for only the most basic simulation
/// @dev All function calls are currently implemented without side effects
/// @custom:experimental This is an experimental contract.
contract Tree {
    /// @notice Calculate tree age in years, rounded up, for live trees
    /// @dev The Alexandr N. Tetearing algorithm could increase precision
    /// @param rings The number of rings from dendrochronological sample
    /// @return Age in years, rounded up for partial years
    function age(uint256 rings) external virtual pure returns (uint256) {
        return rings + 1;
    }

    /// @notice Returns the amount of leaves the tree has.
    /// @dev Returns only a fixed number.
    function leaves() external virtual pure returns(uint256) {
        return 2;
    }
}

另外就是error关键字,我们可以用error来定义一个错误,然后当某个条件满足时,我们再用revert关键字报告一个错误,同时错误背后的原因通过natSpec做了解释。后面的代码会看到调用。

// 通过构造函数初始化受益人和拍卖的结束时间
constructor(
        uint biddingTime,
        address payable beneficiaryAddress
) {
    beneficiary = beneficiaryAddress;
    auctionEndTime = block.timestamp + biddingTime;
}
    

block.timestamp 向合约提供当前区块的时间戳。

function bid() external payable {

        if (block.timestamp > auctionEndTime)
            revert AuctionAlreadyEnded();

        if (msg.value <= highestBid)
            revert BidNotHighEnough(highestBid);

        if (highestBid != 0) {

            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

拍卖的核心流程就是这个方法。同样payable表示这个方法可以接收以太币。external则表示这个方法是在外部被调用的,也就是被合约的用户通过接口调用。逻辑上也不复杂,pendingReturns会记录所有出价成功的人(注意不是拍卖成功)和他们的总出价。

同时,当前最高的出价人和出价会被当成日志记录在以太坊的区块链上,对这个日志感兴趣的人可以监听这个日志做一些事情。

  function withdraw() external returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {
            pendingReturns[msg.sender] = 0;

            if (!payable(msg.sender).send(amount)) {
                // No need to call throw here, just reset the amount owing
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

拍卖结束后,没有赢得最终拍卖的那些出价者需要有一个接口进行退款,就是这个withdraw方法。payable(msg.sender).send这句的意思是将以太币发送回调用者的地址。用payable修饰表示这个地址可以接收以太币。

    function auctionEnd() external {

        if (block.timestamp < auctionEndTime)
            revert AuctionNotYetEnded();
        if (ended)
            revert AuctionEndAlreadyCalled();

        ended = true;
        emit AuctionEnded(highestBidder, highestBid);
        beneficiary.transfer(highestBid);
    }

调用这个方法可以结束拍卖。前面先进行检查,看是否满足结束的条件。如果满足就更新状态并且记录日志。最后就是把拍卖的钱转给受益人。

我们把这段程序放在remix运行下,看看效果。

首先传入一个结束时间和受益人地址进行初始化。

在这里插入图片描述

我这里设置的受益人地址说:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

获取下变量的初始值看看,

在这里插入图片描述

看着都没啥问题。我们现在模拟一次出价,点击bid方法,然后value那里我们先默认用0,点击后发现报错:

[vm]from: 0x5B3...eddC4to: SimpleAuction.bid() 0xD7A...F771Bvalue: 0 weidata: 0x199...8aeeflogs: 0hash: 0x9b7...a19d1
transact to SimpleAuction.bid errored: VM error: revert.

revert
	The transaction has been reverted to the initial state.
Error provided by the contract:
BidNotHighEnough : 已经有更高的出价者了
Parameters:
{
 "highestBid": {
  "value": "0"
 }
}

有没有注意到,我们通过注释的写的error的描述,在错误出现的时候打印出来了。这样验证了我们前面的内容。

我么首先让用户a(地址是:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2)出价11 wei,然后让用户b (地址是:0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db)出价 20 wei,你会看到出价成功后对应的地址上的余额就会减少(同时也会扣除gas fee)。

点击拍卖结束后,受益人的地址上余额就会增加。同时我们可以选择对应的没有拍卖成功的出价人的地址,然后点击withdraw,他的出价会返回来。


参考: