[solidity语法学习二]基于Remix以太坊的BlindAuction模板代码分析

可紊 提交于 2020-08-18 16:48:06

前言

编译环境:Remix.
代码来源:SolidityDoc.
疑难解决来源:Ethereum gitter.
例子三参考:例图.
以太币单位换算:以太币单位.
本文主要由三个例子构成。分别是Simple Open Auction 以及延伸版本Blind Auction 、此外还有一个涉及到状态机类型的合同为Safe Remote Purchase。
该例子在基于(一)之上引入了相关语法,接下来我会尽力以自己的理解结合网上的资料进行分析解释,从而帮助我更好地理解相关语法的知识点。





本次例子中引入了msg.value变量以及payable变量,重点在于强调value这个价值。只有当地址中包含payable变量的时候,才能引入value这个功能。因此,我们正常输入函数需要在value中设置相应的以太币,如:
在这里插入图片描述

一、Simple Open Auction代码分析

a、初始化相应变量

address payable public beneficiary;//引入受益人地址[注1]
    uint public auctionEndTime;//定义拍卖停止时间
    
    address public highestBidder;//提出最高价格的竞标人地址
    uint public highestBid;//竞标最高价格
    
    mapping (address => uint) pendingReturns;//将每个地址隐射一个退还金额的变量
    
    bool ended;//判断竞标是否结束的变量
    
    event HighestBidIncreased(address bidder, uint amount);//最高竞价增加的log事件(涉及地址以及数量信息)
    event AuctionEnded(address winner, uint amount);//竞标成功的地址和数量信息的的log事件

[注1]:address payable
首先,我们得明确address和address payable的差别。
address指的是一个20字节的值,也就是以太坊账户地址。
address payable除了代表以太坊账户地址,还额外具备transfer和send函数的功能。
常见用法会在接下来函数具体应用中说明,并且会在该例子结束后具体对比说明。



b、构造函数

    constructor(
        uint _biddingTime,//定义一个时间间隔
        address payable _beneficiary)//定义受益人地址
    {
        beneficiary = _beneficiary;//赋值到全局变量
        auctionEndTime = block.timestamp + _biddingTime;//[注2]
    }

[注2]:这里定义一个整数型变量_biddingTime,实际上可以理解为定义一个时间间隔。比如赋值600 也就等价于600秒。
block.timestamp是获取当前时间,该值主要指的是获取当前时间。
PS:当然有些小伙伴会发现这个solidity7.0版本之前是使用now来代替,在solidity7.0语法升级以后,统一使用block.timestamp来获取当前时间,具体更新改动可以通过阅读Solidity v0.7.0 Breaking Changes 来进一步了解其他方面的变动,当然这里只是扩展一下。

c、竞标函数

function bid() public payable{
        require(
                block.timestamp <= auctionEndTime,
                "Auction already ended."
            );//判断是否处于竞标期间
        require(
                msg.value > highestBid,
                "There already is a higher bid"
            );//判断竞标金额是否大于最高竞标金额
        if (highestBid != 0){
            pendingReturns[highestBidder] += highestBid;
        }//[注3]
        highestBidder = msg.sender;//赋值竞标价格最高者地址
        highestBid = msg.value;//赋值竞标价格
        emit HighestBidIncreased(msg.sender, msg.value);
        //声明log日志用于显示竞标最高者地址以及价格
    }

[注3]由于当前竞标价格为最高价格,因此过往的价格都返还到原有地址的返还数组:pendingReturn[highestBidder] 里面。因此,可以调用d中函数把返还数组里面的以太币返还到指定账户。

d、返还竞标金额函数

 function withdraw() public returns (bool) {
        uint amount = pendingReturns[msg.sender];//定义一个整型变量amount存储返回数组的具体金额
        if(amount > 0){
            pendingReturns[msg.sender] = 0;//若amount>0,则清空返回数组的数据
            if(!msg.sender.send(amount))//[注4]{
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

[注4]通过判断amount是否为0,若不为0则把pendingReturns[msg.sender] = 0; 同时这里运用了msg.sender.send(amount) 函数。也就是前文提到的address payable的特性。
这里先介绍一下send函数的用法以及原理:
msg.sender.send(amount) 指的是将amount的金额发送到msg.sender的账户上。send除了传递金额以后还会发出一个bool型的返回值。
若成功执行则返回的是true,因此该if条件不满足可以直接return true。
若失败执行则if条件满足,此时将amount返回到pendingReturns[msg.sender] 账户里。



实际使用就是:XXX.send(具体变量) XXX必须是一个 address payable。

e、竞拍终止函数

function auctionEnd() public {
        require (block.timestamp >= auctionEndTime, "Auction not yet ender");//当前时间超过竞拍时间则满足条件
        require (!ended, "auctionEnd has already been called");
        //结束标志位判断,防止重复结算拍卖结束
        ended = true;//将ended标志位置为true
        emit AuctionEnded(highestBidder, highestBid);
        //通过log日志显示竞标成功的地址以及价格
        beneficiary.transfer(highestBid);
        //[注5]
    }

[注5]通过调用beneficiary.transfer(highestBid); ,可以把最高价格传递给受益人。
使用技巧:
XXX.transfer(具体变量) XXX必须是一个 address payable。

重点介绍一下send和transfer的区别:
send的使用优先级是比transfer低的。

send函数的具体执行流程:首先把指定金额发送到指定账户,如msg.sender.send(10)。就是把10以太币发送到msg.sender地址上,同时返回一个bool型变量判断是否成功。若发送成功返回true,而发送失败返回flase。即使发送失败,程序也会按照原来的逻辑继续进行下去。

transfer函数的具体执行流程:把金额发送到指定账户,若金额不足或者交易被拒绝(发送失败),则transfer函数会因为错误而恢复到执行前状态。并且transfer函数是不会返回一个bool型变量进行判断

以上加粗字体就是send函数和transfer函数的区别。

transfer优先级更高的原因主要是:假如调用send函数,当函数的gas费用使用完毕时会导致以太币在传输过程中存在丢失等现象。因此使用transfer函数可以更好地保证以太币在程序出现异常时能够原路返回到发送账户。

二、Simple Open Auction代码分析

盲拍代码是基于公开拍卖代码的基础上,引入哈希加密算法,对在竞拍期的竞拍信息进行隐藏。在展示期间,通过输入对应的值与竞拍期的值进行比较,若相同则正常工作。

此外在这里引用了以太币的换算概念,因此我在这里进行补充:
在这里插入图片描述
wei:Ethereum的最小单位
gwei:1gwei = 10^9 wei (10的9次方)
finney: 1finnery = 10^6 gwei = 10^15 wei (10的15次方)
ether:1ether = 10^3 finney = 10^9 gwei = 10^18 wei (10的18次方)




在这里引入单位换算是考虑到后续由于格式限制问题,导致无法直接就1Ether进行交易,由于编译器识别的值为wei。因此在这里提前给大家科普一下相关概念是有必要的。

a、初始化相应变量

//定义一个结构体变量
struct Bid{
        bytes32 blindedBid;//哈希加密后的字节串
        uint deposit;//存放的value值
    }
    
    address payable public beneficiary;//定义一个收益地址
    uint public biddingEnd;//竞标阶段持续时间
    uint public revealEnd;//揭示阶段持续时间
    bool public ended;//判断竞拍是否结束
    
    mapping(address => Bid[]) public bids;//映射地址为Bid结构体变量
    
    address public highestBidder;//最高出价者所在的地址
    uint public highestBid;//最高出价
    
    mapping(address => uint) pendingReturns;//映射地址到一个返回变量
    
    event AuctionEnded(address winner, uint highestBid);//事件竞拍结束
    
    modifier onlyBefore(uint _time) //判断当前时间是否在_time时间之前
    {
        require (block.timestamp < _time);
        _;
    }
    
    modifier onlyAfter(uint _time)//判断当前时间是否在_time时间之后
    {
        require (block.timestamp > _time);
        _;
    }

b、具体功能函数以及相应解析

智能合约构造函数:

constructor(
        uint _biddingTime,//竞拍时长
        uint _revealTime,//揭示时长
        address payable _beneficiary//受益人地址
    ){
        beneficiary = _beneficiary;
        biddingEnd = block.timestamp + _biddingTime;
        revealEnd = biddingEnd + _revealTime;
    }

竞拍结束时间 = 合约产生时的当前时间 + 竞拍时长
揭示结束时间 = 竞拍结束时间 + 揭示时长

※竞拍函数:
这个函数如果直接照搬例子的话会产生一个很大的问题。因此我想结合着揭示函数来一起讲:

例程源代码是:

//函数输入变量是一个bytes32字符串
function bid(bytes32 _blindedBid)
        public //公开的
        payable //可以接收和发送value
        onlyBefore(biddingEnd)//在竞拍阶段结束前
    {
    //在bids[msg.sender]数组内部,再添加一个结构体数组,构成一个二维数组
        bids[msg.sender].push(Bid({
            blindedBid: _blindedBid,//blindedBid是输入的字符串
            deposit: msg.value//deposit代表输入的value数
        }));
    }

我修改过后的代码为:

//输入uint型变量,bool型变量,bytes32变量。
    function bid(
        uint __values,//代表竞拍为多少wei(以太币最小单位)
        bool __fake,//bool型变量决定这次竞拍是真还是假
        bytes32 __secret//32位字节流
        ) 
    public//公开的
    payable//可以支付和收取value
    onlyBefore(biddingEnd)//只能在竞拍阶段结束前调用
    {

        bytes32  _blindedBid;//定义一个32位字节流
        _blindedBid = keccak256(abi.encodePacked(__values, __fake , __secret));//[注3]
        bids[msg.sender].push(Bid({
            blindedBid: _blindedBid,
            deposit: msg.value
        }));
    }

[注3]:
首先这里可以分成两个函数来看,一个是keccak256(),另外一个函数是abi.encodePacked()
首先我们来讲abi.encodePacked()。这个函数的功能实际上是一个拼接函数,把输入变量按照变量类型以及顺序进行拼接成一个bytes(字节流)。如果举例的话可以用下图的例子进行列举:
在这里插入图片描述
例如调用abi.encodePacked(int16 (-1), bytes1 (0x42), uint16 (0x03), string(“Hello, world!”)) 会输出下面这对字符串。相当于就是类型+具体数值的拼接。



当然该函数使用是有特点的:
1.当输入变量组合的总位数小于32位时,不会进行0或者符号位填充。同时输出的变量位数是动态的,因此理想情况下定义为bytes动态输出变量来保证能够正确接收具体值。
自己编写了一个测试例子为:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >0.6.99 <0.8.0;

contract test{
    uint16 public values;//定义一个uint16输入,等价于bytes2
    bool public fake;//定义一个bool型变量,等价于bytes1
    bytes4 public secret;//定义一个bytes4变量
    bytes32 public blindedBid;//一个bytes32测试keccak256()加密
    bytes public blindedBid2;//定义一个bytes测试abi.encodePacked()
    constructor(
        uint16 _values,
        bool _fake,
        bytes4 _secret
        ){

        values = _values;
        fake = _fake;
        secret = _secret;
        blindedBid = keccak256(abi.encodePacked(values, fake, secret));
        blindedBid2 = abi.encodePacked(values, fake, secret);  
            
        }
        

}

举例输入:
在这里插入图片描述
举例输出:
在这里插入图片描述
具体可以看出分别由"0x000b" = 11 , “00” = false, “12345678” = bytes4拼接而成,可以看出按输入顺序以及对应的数据类型顺序拼接在一起输出。



PS:本质上来讲,对于这个api.encodePacked函数是提供一个接口可以不考虑标准api表示格式,而是直接根据对应的数据类型,对应的顺序,从而输出对应的字节流。而实际上api.encode会考虑长度等问题(这个是一个较复杂的问题,如果以后用到会考虑专门写一篇文章来加深自己的理解)

PPS: abi.encodePacked是没有首尾概念的,因此下面这个是完全相等的:
abi.encodePacked(bytes1 “a” , bytes2 “bc”) == abi.encodePacked( bytes2 “ab”, bytes1 “c”) 。以上两次函数调用输出的值都是一样的。

因此,这里我们可以简单的理解为是一个对多个输入变量进行拼接的函数。其最终目的是为了和接下来的keccak256()联合起来使用。

keccak256()的具体原理我现在还不是很懂,但是可以理解为和比特币一样的哈希加密。即无论输入是任意长度的字节串,输出都会是bytes32的字节串。
只要输入的值不变,输出的bytes32的字节串也不变。任意两个相同的字节串,如果改变其中一小个变量,都会产生几乎完全不同的字节串输出。因此我们可以通过keccak256()函数进行加密解密,从而判定揭示阶段的输入变量是否与竞拍阶段的输入变量是否一致。

补充:
由于输入两个不同的变量产生相同的bytes32字节串的概率非常低,以至于可以忽略不计,所以可以理解为存在碰撞阻力(即找不到两个不同的输入产生相同输出的情况)

Q:为什么我要对原例子函数进行修改?
A:首先例子中的代码要求我们输入一个bytes32 的经过keccak256()加密的字节流本来就是一个不可能实现的事情。因为具体的加密原理都不知道,所以我们无法人工推出输出的bytes32字节流。因此我这里就改为输入三个代揭示的变量,通过remix以太坊虚拟机加密,并把输出结果保存在bytes32下的_blindedBid变量,同时添加到指定bids结构体数组。这样才能保证在揭示函数中有一个正确的_blindedBid变量进行对比判断是否正确

c、揭示函数

function reval(
        uint[] memory _values,//竞拍价值
        bool[] memory _fake,//是否为假竞拍
        bytes32[] memory _secret//安全码
    )
        public//公开的
        onlyAfter(biddingEnd)//竞拍阶段结束后
        onlyBefore(revealEnd)//揭示阶段结束前
    {
        uint length = bids[msg.sender].length;//[注4]
        require(_values.length == length);
        require(_fake.length == length);
        require(_secret.length == length);
        //上面都是判断揭示的变量是否与竞标时输入的变量长度是否一致
        uint refund;//定义refund变量来存放返回值
        //for循环依次比较,若变量一致则继续执行函数,否则continue,也就是继续执行for循环的i++,进行下一组变量比较
        for(uint i = 0 ; i < length ; i++){
            Bid storage bidToCheck = bids[msg.sender][i];//定义一个结构体指针指向bids[msg.sender][i],从而直接进行数据对比
            (uint value, bool fake, bytes32 secret) = (_values[i], _fake[i], _secret[i]);//对输入变量进行赋值
            if(bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake , secret))){
                continue;//判断竞标期和揭示期输入的价值、真假、密钥是否一致
            }
            refund += bidToCheck.deposit;//对竞标期输入的value存放在变量refund
            if(!fake && bidToCheck.deposit >= value){
                if(placeBid(msg.sender, value))
                    refund -= value;//若上述函数判断为真,会把竞标金额从refund中扣除。
            }//若bool型变量fake为false也就是有效竞拍,且竞标期发送的value大于实际中赋值的变量value则再次判断。同时placeBid是引用下面的函数,具体往下翻
            bidToCheck.blindedBid = bytes32(0);//对解密后的blindedBid变量进行初始化,即防止二次调用竞拍期产生的变量
        }
        msg.sender.transfer(refund);//将多余的refund退回到调用地址
    }

//接前面的placeBid函数调用。输入竞拍人地址和竞拍金额
function placeBid(address bidder, uint value) internal
        returns (bool success)
        {
            if(value <= highestBid){
                return false;
            }  //如果value低于最高竞价,则返回flase变量,即不会把这部分金额从refund变量中扣除
            if(highestBidder != address(0)){
                pendingReturns[highestBidder] += highestBid;
            }//如果之前存在一个竞价最高者,则吧最高价格退回到pendingReturns返回金额数组,方便后续用户通过withdraw函数调用
            highestBid = value;//更改最新的最高竞价
            highestBidder = bidder;//更改最新的最高竞价地址
            return true;
        }

[注4]:这里要区分一下一维数组和二维数组的length判断。比如:bids[].length就是判断bids[]数组中含有的变量个数。
如果是bids[msg.sender].length,可以理解基于bids[msg.sender]二维数组下的变量数目。对于我自己是一个小小的笔记。

以上两部分函数就是为了实现隐匿竞拍。无论是隐匿竞拍金额,通过在竞拍期提交报价并且使用keccak256()加密。随后在揭示期,各个竞拍用户依次揭示在竞拍期的报价。从而实现整个拍卖的过程。后续的函数都是前面一个例子中提到的。在这里就只列出来不做具体说明。

d、通用例子

 function withdraw() public {
        uint amount = pendingReturns[msg.sender];
        if (amount > 0){
            pendingReturns[msg.sender] = 0;
            msg.sender.transfer(amount);
        }
    }
    
    function auctionEnd() public onlyAfter(revealEnd)
    {
        require(!ended);
        emit AuctionEnded(highestBidder, highestBid);
        ended = true;
        beneficiary.transfer(highestBid);
    }

以上代码的实现原理可以参考前一个公开拍卖的例子。

e、细节补充:

首先最大的细节就是在这个代码用所输入的uint value变量,其单位的最小值为wei。因此为了能够保证更直观地获取明显的以太币变化,因此这里推荐使用finney作为最小单位,通过前方的单位转换表可以得知输入value可以用参考的:1000000000000000 (这里代表1finney)
Q:为什么不直接使用Ether变量呢?
A:因为发现用uint变量的时候没法输入一个10^18变量,否则会报错,如:
在这里插入图片描述
报错:请输入一个有效变量,因此我初步理解是由于结合数组的情况就不允许这样子输入。综上所述,只能退而求其次选择finney作为单位。



其次就是constructor函数的时间可以理解为以秒为基本单位。此外对于keccak256()和abi.encodePacked() 暂时只是从应用的角度上理解。希望能够在接下来的学习中加深对该函数的理解。

三、Safe Remote Purchase代码分析

本例子在巩固上述例子学习语法之余,引入了在以太坊智能合约下实现状态机的功能。具体的状态如下图:
在这里插入图片描述

a、初始化相应变量

uint public value;//定义商品的价值
    address payable public seller;//定义一个payable类型的卖方
    address payable public buyer;//定义一个payable类型的买方
    
    enum State { Created, Locked, Release, Inactive }
    //定义一个枚举类型包含四个状态变量
    State public state;//定义枚举变量
    
    modifier condition(bool _condition){
        require(_condition);
        _;
    }//[注1]+判断是否bool型变量condition是否为true
    
    modifier onlyBuyer(){
        require(
            msg.sender == buyer,
            "Only buyer can call this."
        );
        _;
    }//[注1]+判断调用地址是否为买方地址
    
    modifier onlySeller(){
        require(
            msg.sender == seller,
            "Only seller can call this."
            );
            _;
    }//[注1]+判断调用地址是否为卖方地址
    
    modifier inState(State _state) {
        require(
            state == _state,
            "Invalid state."
            );
            _;
    }//[注1]+判断是否在对应的交易阶段调用对应的函数
    
    //以下是四个阶段通知的声明,方便后续可以通过log日志获取详细信息
    event Aborted();
    event PurchaseConfirmed();
    event ItemReceived();
    event SellerRefunded();

注1:
modifier函数根据例子可以得知有以下功能:
基本使用格式类似:

modifier XXX(变量类型 变量){
	require(判断条件, "错误时返回消息");
	_;//其中_表示原函数
}

综上所述:可以通过modifier函数修改,从而对整个函数的调用进行限制。比如第二篇例文通过判断block.timestamp 以及设定的拍卖时间来决定函数能否调用;而这里的话通过对交易所处的阶段进行判断,从而在对应的交易阶段只允许调用相应的函数。

b、具体功能函数

撤回函数:

 function abort()
    public
    onlySeller//调用地址为卖方地址才可运行该函数
    inState(State.Created)//当状态处于合约产生状态时可调用
    {
        emit Aborted();//调用log日志显示
        state = State.Inactive;//状态转为不工作状态
        seller.transfer(address(this).balance);//[注2]
    }

注2:address(this).blance 相当于查询合约地址存储的以太币数量。再通过transfer函数可以把金额进行返还。

买家确认购买函数:

function confirmPurchase()
    public
    inState(State.Created)//处于合约产生阶段可调用
    condition(msg.value == ( 2 * value))//购买人的输入value必须持有2*value
    payable
    {
        emit PurchaseConfirmed();
        buyer = msg.sender;//对买方地址进行赋值
        state = State.Locked;//状态切换为锁定订单
    }

买家确认收货函数:

function confirmReceived()
    public
    onlyBuyer//限制买家进行调用
    inState(State.Locked)//处于合约锁定阶段可以调用
    {
        emit ItemReceived();
        state = State.Release;//转换为合约发布阶段
        buyer.transfer(value);//买方退回value
    }

卖方收回资金函数:

function refundSeller()
    public
    onlySeller//只有卖方能够调用
    inState(State.Release)//状态是合约锁定解除可调用
    {
        emit SellerRefunded();
        state = State.Inactive;//状态切换为合约不工作
        seller.transfer(3 * value);//返回3*value给卖家
    }

总的话可以引入这么一副图来对整个程序工作流程进行解释:
在这里插入图片描述

四、最后汇个总

首先这次三个例子花了自己很长时间去理解具体的知识点以及运用。在实际学习过程中也只能通过阅读英文资料来大致理解,但是还是绕了很多弯路。不过可以得出的结论就是,Solidity的doc里面的例子还是很实用的。以上例子都涉及到以太币的使用以及设立状态机的概念等等。

希望自己下一篇文章能够提个速度,并且到时候回顾这段时间自己写的文章。做完巩固之后争取做出一个小作品哈哈哈。

当然,如果大家有什么疑问可以随时在评论区指出来。我会尽力和你一起分析疑问并追求共同进步。可能这篇文章看的会有点乱,毕竟在写这篇文章的时候心境不断地在发生变化。自学果然还是好难,希望未来能够接触到各种大佬共同学习,相互指教,最后共同进步!!!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!