solidity入门——拍卖案例一(明价拍卖)

前言

当下,开发以太坊智能合约使用最多的语言是solidity。这门语言最早由以太坊创世团队的Gavin Hood设计,后由以太坊技术团队开发维护。其特性结合了JavaScript、Java、Python等的特点。随着版本更迭,如今正走在更安全更严谨的道路上。

学习智能合约开发,可以从官方文档开始。戳这里:solidity官方文档。文档的第三章《Solidity by Example》提供了几个很不错的案例,这里我挑有意思的分析注解一下。


背景

第一个合约案例是《Simple Open Auction》,它的功能是模拟现实中最常见的拍卖形式。过程很简单:

购买者(Bidder)调用合约对拍卖品出价,出价会以Ether转账的形式暂存给合约。合约要求在一定时间段内完成竞价。竞价结束后合约统计最高出价者,退还除最高出价者之外的其他人暂存的钱款。并将最高价对应的钱款转给出售者(Beneficiary)。

代码

pragma solidity >=0.4.22 <0.6.0;

contract SimpleAuction {
    // 受益人,出售者
    address payable public beneficiary;
    // 竞价结束时间
    uint public auctionEndTime;

    // 最高价出价者
    address public highestBidder;
    // 最高价
    uint public highestBid;

    // 允许撤回之前的出价
    // 该map记录每个竞价者的最高出价
    mapping(address => uint) pendingReturns;

	// 竞价结束标识位
    bool ended;

    // 事件一:最高价发生变化时
    event HighestBidIncreased(address bidder, uint amount);
    // 事件二:竞价结束时
    event AuctionEnded(address winner, uint amount);

    // (0.5.2新格式)-->特殊注解<--
    // 出现时机:当用户被要求确认交易时就会弹出注释内容
    
    /// 构造函数
    //  竞价持续时间 `_biddingTime`
    /// 受益人账户地址 `_beneficiary`.
    constructor( uint _biddingTime,address payable _beneficiary) public {
        beneficiary = _beneficiary;
        auctionEndTime = now + _biddingTime;
    }

    /// 竞价函数直接由调用者发起,价格有msg.value确定
    /// 如果竞价失败,value中的钱可以在结束后撤回
    function bid() public payable {
        // 关键字payable用来标明合约函数可以接受Ether币.
        require(
            now <= auctionEndTime,
            "Auction already ended."
        );

        // 如果价格不超过最高价,钱会被退回.
        require(
            msg.value > highestBid,
            "There already is a higher bid."
        );

        if (highestBid != 0) {
            // 安全编写须知!!!
            // 这一时刻旧的最高价要被退回出价者
            // 直接调用highestBidder.send(highestBid)会有安全风险,因为send()接受特殊构参数后可以调用任意合约。
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// 撤回一个被超过的价格.
    function withdraw() public returns (bool) {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0) {

            // 安全编写须知!!!
            // 一定要先将map中的值清零,因为接收者可以在send()函数返回前利用接受合约重新调用withdraw()函数
            // 从而发生“多转”的bug,即重放攻击!!!
            pendingReturns[msg.sender] = 0;

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

    function auctionEnd() public {

        // 在于其他合约交互时(i.e. 调用合约函数或转账) 可以分成3个阶段:
        // 1. 检查条件;
        // 2. 执行动作; (potentially changing conditions)
        // 3. 与其他合约交互;
        // 安全设计!!!
        // 如果这三个阶段混到一块了,其他合约就可能调回当前合约从而修改状态变量。
        // 这会导致一些过程(i.e. 以太币转账)被执行多次。

		// 如果内部函数有和外部合约交互的过程,那么也该遵循以上准则。

        // 1. Conditions
        require(now >= auctionEndTime, "Auction not yet ended.");
        require(!ended, "auctionEnd has already been called.");

        // 2. Effects
        ended = true;
        emit AuctionEnded(highestBidder, highestBid);

        // 3. Interaction
        beneficiary.transfer(highestBid);
    }
}

总结

这个官方案例过程并不复杂,值得思考的点是注解中的安全编码建议。例如,里面提到的重放攻击就曾发生在THE DAO合约中,造成了非常严重的损失。现在的智能合约机制还不是特别完善,存在不少有隐患的安全问题。开发者是该多总结之前血淋淋的教训。

你可能感兴趣的:(智能合约开发)