Solidity 智能合约实例分析——盲拍

1 场景

在盲拍的应用场景中,我们定义如下几个关键要素:

  • 受益人,最终的钱款接收方
  • 竞拍者,任意持有合法账户的人都可以参与竞拍
  • 竞拍时间,只能在指定时间内完成出价
  • 明牌时间,参与者亮出出价
bid.jpg

2 逻辑

  1. 所有参与者持有一个区块链账户
  2. 发起人创建盲拍合约,创建时指定受益人,竞拍时间,揭晓时间
  3. 竞拍者在竞拍时间结束前,可以进行出价
  4. 竞拍者可以随时撤回自己的出价,并回收抵押资金
  5. 竞拍结束,价高者得

3 完整代码

源代码地址 https://solidity.readthedocs.io/en/v0.5.1/solidity-by-example.html

pragma solidity >0.4.23 <0.6.0;

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;
    mapping(address => uint) pendingReturns;

    event AuctionEnded(address winner, uint highestBid);

    modifier onlyBefore(uint _time) { require(now < _time); _; }
    modifier onlyAfter(uint _time) { require(now > _time); _; }

    constructor(
        uint _biddingTime,
        uint _revealTime,
        address payable _beneficiary
    ) public {
        beneficiary = _beneficiary;
        biddingEnd = now + _biddingTime;
        revealEnd = biddingEnd + _revealTime;
    }

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

    function reveal(
        uint[] memory _values,
        bool[] memory _fake,
        bytes32[] memory _secret
    )
        public
        onlyAfter(biddingEnd)
        onlyBefore(revealEnd)
    {
        uint length = bids[msg.sender].length;
        require(_values.length == length);
        require(_fake.length == length);
        require(_secret.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], _fake[i], _secret[i]);
            if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
                continue;
            }
            refund += bidToCheck.deposit;
            if (!fake && bidToCheck.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }

            bidToCheck.blindedBid = bytes32(0);
        }
        msg.sender.transfer(refund);
    }

    function placeBid(address bidder, uint value) internal
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            pendingReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }

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

4 解析

4.1 数据结构

一笔盲拍竞价数据由两个关键要素构成:加密出价,保证金。

struct Bid {
    bytes32 blindedBid; // 加密出价
    uint deposit;       // 保证金
}

这个合约中的出现了两类地址,受益人账户,比普通 address 多了一个 payable 修饰关键字,这表明这个账户能进行代币的相关操作。买家账户,只作为显示使用,不需要 payable

address payable public beneficiary;
address public highestBidder;

几个关于竞拍时间、流程控制的变量。

uint public biddingEnd;
uint public revealEnd;
bool public ended;
uint public highestBid;

将账户与竞价信息和抵押保证金相关联。

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

4.2 构造函数

solidity 的内置变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。在这里,以秒为单位,因此 _biddingTime, _revealTime 都是从当前开始经过XXX秒后。

constructor(
    uint _biddingTime,
    uint _revealTime,
    address payable _beneficiary
) public {
    beneficiary = _beneficiary;
    biddingEnd = now + _biddingTime;
    revealEnd = biddingEnd + _revealTime;
}

4.3 修改器

modifier 指示函数修改器。本示例中,这种修改器以嵌入的方式加到被作用函数上。运行时,_; 部分会用被作用函数的原有代码替代。

modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }

4.4 竞价函数

该函数使用了修改器 onlyBefore ,函数执行的实际代码为

require(now > _time);
... // bid函数代码

payable 关键字修饰该函数,表明涉及资金操作(msg.value)。其中,

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

是加密编码后的竞价信息,value 是出价,不得小于出价人持有的代币;fakefalse 时竞价才有效;secret 可以视为密钥。一个账户可以多次出价,通常,竞拍者会多次把 fake 设置为 true 并且给出一个无效 value 提交,用以隐藏真正出价。

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

4.5 明牌函数

该函数限制为在竞价结束后,明牌结束前执行。用到了修改器 onlyAfteronlyBefore。此处竞拍者需要传入自己所有的历史出价的三要素,并且以出价先后顺序排序。在明牌校验时,用到了多变量赋值语句:

(x,y,z) = (a,b,c)

明牌的具体逻辑如下。

auction_flow.jpg
function reveal(
    uint[] memory _values,
    bool[] memory _fake,
    bytes32[] memory _secret
)
    public onlyAfter(biddingEnd) onlyBefore(revealEnd)
{
    uint length = bids[msg.sender].length;
    require(_values.length == length);
    require(_fake.length == length);
    require(_secret.length == length);

    uint refund;    // 应退资金
    for (uint i = 0; i < length; i++) {
        // [][]不是二维数组,第一层是map解出value,第二层是访问一维数组
        Bid storage bidToCheck = 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;
        if (!fake && bidToCheck.deposit >= value) {
            // 尝试纳入有效竞价
            if (placeBid(msg.sender, value))
                refund -= value;
        }
        
        // 避免重复退款
        bidToCheck.blindedBid = bytes32(0);
    }
    msg.sender.transfer(refund);
}

4.6 出价函数

该函数用 internal 关键字修饰,表示只能合约内部调用。类似于 Java 中的 private 关键字。入参需要在调用者的代码中进行校验。

function placeBid(address bidder, uint value) internal
        returns (bool success)
{
    if (value <= highestBid) {
        return false;
    }
    if (highestBidder != address(0)) {
        pendingReturns[highestBidder] += highestBid;
    }
    highestBid = value;
    highestBidder = bidder;
    return true;
}

4.7 退款函数

用户可以调用该函数以退回资金。此处逻辑遵循 条件(condition)--结果(effect)--交互(interact) 的标准化过程,防止重复退款的事件发生。

function withdraw() public {
    uint amount = pendingReturns[msg.sender];
    // 条件
    if (amount > 0) {
        // 先设置结果
        pendingReturns[msg.sender] = 0;
        // 再执行交互流程
        msg.sender.transfer(amount);
    }
}

4.8 结束函数

这个函数出发了一个 event,它是以太坊虚拟机(EVM)日志基础设施提供的一个便利接口。当被发送事件(调用)时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并记录到区块链中。在DAPP的应用中,如果监听了某事件,当事件发生时,会进行回调。

event AuctionEnded(address winner, uint highestBid);

function auctionEnd()
    public onlyAfter(revealEnd)
{
    require(!ended);
    // 事件触发
    emit AuctionEnded(highestBidder, highestBid);
    ended = true;
    beneficiary.transfer(highestBid);
}

(完)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容