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

Posted lucasma.eth

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何编写一个拍卖的智能合约-续相关的知识,希望对你有一定的参考价值。

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

上一篇文章我们实现了一个简单的open auction,本篇我们来讨论下如何实现一个blind auction

盲拍有个核心问题就是如何保证数据的安全性,而区块链的加密特性正是解决该问题的关键。

我们实现的思路是这样的,在拍卖期间,竞拍者并不会真正的发送自己的竞价,而是发送一个本次竞价的哈希值版本。因为哈希值本身基本不会重复,所以就可以唯一代表一次竞拍。等待拍卖结束时,在reveal阶段才会公开他们的竞拍。

盲拍另一个需要解决的问题是怎样保证约束力。就是如何防止竞拍人在赢得拍卖后不发送他们的货币,也就是防止他们乱喊价。在公开拍卖的场景是不存在这个问题的,因为公开拍卖是真实的以太币转移,在区块链上是公开的,不可篡改也没法抵赖。

下面这个示例给出了一种解决方案,就是每个人可以多次竞价,同时发送价格和哈希值,哈希值的输入包括一个fake字段,如果fake是false表示这次有效的喊价(当然不一定是最高喊价),fake是true表示本次喊价无效。通过这种方法,即使每次交易都在链上公开了,别人也不知道你哪次竞价有效。

来看下示例代码。

contract BlindAuction 
    struct Bid 
        bytes32 blindedBid; //出价对应的哈希
        uint deposit; //保证金?
    

    address payable public beneficiary; //受益人
    
    uint public biddingEnd;
    uint public revealEnd;
    
    bool public ended; //是否结束

    mapping(address => Bid[]) public bids;

    address public highestBidder;
    uint public highestBid;

    // 拍卖结束后根据这个map的数据退换其他竞拍者的出价
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);
    

接下来定义几个事件,

///调用某个方法太早了,也就是还没到可以调用的时间
error TooEarly(uint time);

///调用某个方法太迟了
error TooLate(uint time);

/// 拍卖结束的方法已经被调用了
error AuctionEndAlreadyCalled();

继续看,

 modifier onlyBefore(uint time) 
        if (block.timestamp >= time) revert TooLate(time);
        _;
    
    modifier onlyAfter(uint time) 
        if (block.timestamp <= time) revert TooEarly(time);
        _;
    

modifier是个关键字,我们可以用这个关键字自定义修饰符,修饰符就是类似external,payable这种可以加到变量或者方法前面的关键字。修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。

比如这里的onlyBefore表示传入的时间不能早于当前区块链的时间。下面会看到具体的应用例子。

constructor(
        uint biddingTime,
        uint revealTime,
        address payable beneficiaryAddress
    ) 
        beneficiary = beneficiaryAddress;
        biddingEnd = block.timestamp + biddingTime;
        revealEnd = biddingEnd + revealTime;
    

这个是构造函数,比较好理解。revealTime指的是最终披露竞价结果的时间。

function bid(bytes32 blindedBid)
        external
        payable
        onlyBefore(biddingEnd)
    
        bids[msg.sender].push(Bid(
            blindedBid: blindedBid,
            deposit: msg.value
        ));
    

竞价的核心方法,入参是一个哈希,就是我们前面讲的,盲拍是不公开真正的出价,而是根据出价计算一个哈希结果代替出价。计算方法是:

keccak256(abi.encodePacked(value, fake, secret))

注意这里的fake字段,前面有解释。

方法的逻辑很简单,把出价放入map就可以了。

function reveal(
        uint[] calldata values,
        bool[] calldata fakes,
        bytes32[] calldata secrets
    )
        external
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    
        uint length = bids[msg.sender].length;
        require(values.length == length);
        require(fakes.length == length);
        require(secrets.length == length);

        uint refund;
        for (uint i = 0; i < length; i++) 
            Bid storage bidToCheck = bids[msg.sender][i];
            (uint value, bool fake, bytes32 secret) =
                    (values[i], fakes[i], secrets[i]);
            if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) 
                // Bid was not actually revealed.
                // Do not refund deposit.
                continue;
            
            refund += bidToCheck.deposit;
            if (!fake && bidToCheck.deposit >= value) 
                if (placeBid(msg.sender, value))
                    refund -= value;
            
            // Make it impossible for the sender to re-claim
            // the same deposit.
            bidToCheck.blindedBid = bytes32(0);
        
        payable(msg.sender).transfer(refund);
    

reveal方法是实现盲拍的核心,它是最终披露竞拍结果的方法,这个方法首先有约束时间不能早于竞拍结束的时间,又同时不能晚于披露的时间。这里有个新的东西叫calldata,它表示一个只读的数据入参数,这个好处是我们不用担心这个数据在外部会被修改,在函数内部就可以直接便利数据而不用先复制到内存里。

方法的开始是一段参数检查,调用者传过来的披露数据是三组数组,每组数组的长度必须要和自己在盲拍阶段的出价次数一样(每个人可以出价多次)。也就是说你要揭露的竞价要和之前盲拍阶段喊价的次数一致。

然后是执行一段循环,循环的逻辑其实就是我前面讲的,就是每个人可以多次竞价,需要判断哪次的出价是有效的,如果是有效的再去看看是否是最高出价(placeBid),不是有效的出价要退还给出价的人。

这里用到了一个内部方法,如下:

function placeBid(address bidder, uint value) internal
            returns (bool success)
    
        if (value <= highestBid) 
            return false;
        
        if (highestBidder != address(0)) 
            // Refund the previously highest bidder.
            pendingReturns[highestBidder] += highestBid;
        
        highestBid = value;
        highestBidder = bidder;
        return true;
    

这个方法用来判断一个人的出价是否可以作为有效的一次竞拍,如果有效就更新当前的出价信息到highestBid和highestBidder。同时为了能在竞拍结束后退款,也会更新pendingReturns。

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

            payable(msg.sender).transfer(amount);
        
    

退款的方法,这个其实上一篇公开拍卖也讲过,拍卖结束后要把没有赢得竞拍的钱退还回去。

    function auctionEnd()
        external
        onlyAfter(revealEnd)
    
        if (ended) revert AuctionEndAlreadyCalled();
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    

这个也很简单,拍卖结束了,给一些状态置位,把钱拍卖的收益转给受益人。


参考:

  • https://docs.soliditylang.org/en/v0.8.10/solidity-by-example.html

以上是关于如何编写一个拍卖的智能合约-续的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

北大肖臻老师《区块链技术与应用》系列课程学习笔记[25]以太坊-智能合约-5

智能合约solidity:转账,打款,退款,销毁等

第124篇 NFT市场智能合约