解读Solidity的例子——简单的公开拍卖

536 阅读6分钟

这篇文章继续我们上次开始的系列。Solidity智能合约的例子,它实现了一个简化的真实世界的过程。

在这里,我们将走过一个简单的公开拍卖的例子。

为了可读性和开发的目的,我们将首先列出整个智能合约的例子,不加注释。

然后,我们将逐一剖析它,分析它,解释它。

按照这个路径,我们将获得智能合约的实践经验,以及编码、理解和调试智能合约的良好实践。

智能合约--简单的公开拍卖

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract SimpleAuction {
    address payable public beneficiary;
    uint public auctionEndTime;

    address public highestBidder;
    uint public highestBid;

    mapping(address => uint) pendingReturns;

    bool ended;

    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

    error AuctionAlreadyEnded();
    error BidNotHighEnough(uint highestBid);
    error AuctionNotYetEnded(uint timeToAuctionEnd);
    error AuctionEndAlreadyCalled();

    constructor(
        uint biddingTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        auctionEndTime = block.timestamp + biddingTime;
    }

    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);
    }

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

            if (!payable(msg.sender).send(amount)) {
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    function auctionEnd() external {

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

        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        beneficiary.transfer(highestBid);
    }
}

代码分解和分析

// SPDX-License-Identifier: GPL-3.0

只用Solidity编译器0.8.4及以后的版本进行编译,但在0.9版本之前。

pragma solidity ^0.8.4;

contract SimpleAuction {

拍卖的参数是变量beneficiaryauctionEndTime ,我们将在创建合同时,即在合同构造函数中,用合同创建参数来初始化这些参数。

时间变量的数据类型是无符号整数uint ,这样我们就可以表示绝对的Unix时间戳(自1970-01-01以来的秒数)或以秒为单位的时间段(从我们选择的参考时刻开始的秒数)。

    address payable public beneficiary;
    uint public auctionEndTime;

拍卖的当前状态反映在两个变量中,highestBidderhighestBid

    address public highestBidder;
    uint public highestBid;

之前的出价可以撤回,这就是为什么我们有映射数据结构来记录pendingReturns

    mapping(address => uint) pendingReturns;

拍卖结束的指示标志变量。默认情况下,该标志被初始化为false ;一旦它切换到true ,我们将防止改变它。

    bool ended;

当变化发生时,我们希望我们的智能合约能发出相应的变化事件。

    event HighestBidIncreased(address bidder, uint amount);
    event AuctionEnded(address winner, uint amount);

我们要定义四个错误来描述相关的失败。除了这些错误,我们还将引入 "三斜线 "评论,通常被称为natspec 评论。它们使用户能够在显示错误或要求用户确认交易时看到评论。

    /// The auction has already ended.
    error AuctionAlreadyEnded();

    /// There is already a higher or equal bid.
    error BidNotHighEnough(uint highestBid);

    /// The auction has not ended yet, the remaining seconds are displayed.
    error AuctionNotYetEnded(uint timeToAuctionEnd);

    /// The function auctionEnd has already been called.
    error AuctionEndAlreadyCalled();

用合同创建参数biddingTimebeneficiaryAddress 来初始化合同。

    /// Create a simple auction with `biddingTime`
    /// seconds bidding time on behalf of the
    /// beneficiary address `beneficiaryAddress`.
    constructor(
        uint biddingTime,
        address payable beneficiaryAddress
    ) {
        beneficiary = beneficiaryAddress;
        auctionEndTime = block.timestamp + biddingTime;
    }

竞标者通过向代表受益人的智能合约发送货币支付)进行竞标,因此bid() ,函数被定义为 [payable](https://blog.finxter.com/what-is-payable-in-solidity/).

    /// Bid on the auction with the value sent
    /// together with this transaction.
    /// The value will only be refunded if the
    /// auction is not won.
    function bid() external payable {

如果竞价期结束,该函数调用会回滚。

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

如果出价不超过最高出价,该函数将交易回滚给投标人。

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

之前的最高出价者被淘汰了,他的出价被加到他之前的出价中,以备退款。

💡 直接退款被认为是一种安全风险,因为有可能执行一个不受信任的合同。

相反,投标人(收件人)将通过使用下面的withdraw()函数自己撤回他们的投标。

        if (highestBid != 0) {
            pendingReturns[highestBidder] += highestBid;
        }

新的最高出价人和他的出价被记录下来;事件HighestBidIncreased ,携带这个信息对被释放出来。

        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

投标者调用withdraw() 函数来检索他们的投标金额。

    /// Withdraw a bid that was overbid.
    function withdraw() external returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {

send() 函数返回之前,有可能再次调用withdraw() 函数。这就是为什么我们需要通过将一个发件人的待定返回设置为0来禁止同一发件人的多次连续提款的原因。

      pendingReturns[msg.sender] = 0;

msg.sender 的变量类型不是地址payable ,因此我们需要通过使用函数 payable() 作为包装函数来明确转换它。

如果send() 函数以错误结束,我们只需重置待付金额并返回false

            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;
    }

auctionEnd() 函数结束拍卖,并将最高出价发送给受益人。

Solidity 官方文档建议将交互函数分为三个功能部分。

  • 检查条件。
  • 执行动作,以及
  • 与其他合同进行交互。

否则,通过合并这些部分而不是将它们分开,不止一个调用合同可以尝试修改被调用合同的状态并改变被调用合同的状态。

    /// End the auction and send the highest bid
    /// to the beneficiary.
    function auctionEnd() external {

检查条件...

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

...执行行动...

        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

...并与其他合约进行交互。

        beneficiary.transfer(highestBid);
    }
}

我们的智能合约的例子是一个简单的,但却是一个强大的例子,使我们能够向受益人出价一定数量的货币。

当合同通过其构造函数实例化时,它设置了拍卖结束时间和其受益人,即受益人地址。

该合同有三个简单的功能,通过专门的函数实现:出价、撤回出价和结束拍卖。

一个新的出价只有在其金额严格大于当前最高出价时才会被接受。一个新的出价被接受意味着当前的最高出价被添加到投标人的余额中,以便以后提取。新的最高出价人成为当前最高出价人,新的最高出价成为当前最高出价。

提取投标将所有以前出价的总和返回给每个投标人(mapping pendingReturns)。

合同测试场景

公开拍卖时间(以秒为单位)。240

受益人。0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

测试/演示步骤。

  1. Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 bids 10 Wei;
  2. Account 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db bids 25 Wei;
  3. Account 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB bids 25 Wei (rejected);
  4. Account 0x617F2E2fD72FD9D5503197092aC168c91465E7f2 bids 35 Wei;
  5. Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 bids 40 Wei + initiates premature auction end;
  6. Account 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 withdraws his bids;
  7. Account 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db withdraws his bids;
  8. Account 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB withdraws his bids;
  9. Account 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB initiates timely auction end;
  10. Account 0x617F2E2fD72FD9D5503197092aC168c91465E7f2 withdraws his bids;

附录--合同论据

本节是运行合约的额外信息。我们应该想到,我们的例子账户可能会随着Remix的每次刷新/重新加载而改变。

我们的合约创建参数是公开拍卖持续时间(秒)和受益人地址(在部署例子时复制这一行)。

300, 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

💡 信息:我们可以使用任何数量的时间,但我选择了300秒,以便及时模拟结束拍卖的尝试被拒绝和成功结束拍卖的情况。

总结

我们继续我们的智能合约例子系列,这篇文章实现了一个简单的公开拍卖。

首先,为了可读性,我们布置了干净的源代码(没有任何注释)。不建议省略注释,但我们喜欢生活在边缘地带--并试图变得有趣!这是不可能的。😀

其次,我们剖析了代码,对其进行了分析,并对每个可能的非琐碎部分进行了解释。只是因为我们是了不起的、安全的玩家,从不冒险,一切按部就班地进行🙂