基于truffle框架实现以太坊公开拍卖智能合约

环境依赖

以太坊的智能合约主要是 Solidity 实现,但是从零开始编写一个智能合约无疑是费时的(虽然本示例并不复杂),所以选择使用 Truffle 框架来帮助我们开发,所以我们首先需要安装 Node.js 、 npm 、 truffle1。智能合约需要运行在以太坊虚拟机上,我们不可能将未通过测试的智能合约直接部署在以太坊主链上————这会让你付出昂贵的代价,因此还需要一个测试链,而在通过Geth搭建私有以太坊网络中我们已经成功搭建了自己的测试网络,不过这次我们选择另一种更加便捷的方法,那就是 Ganache ,安装使用非常方便,只需官网下载一键安装即可。为了减少版本问题带来的未知Bug,可使本试验成功运行的主要软件版本为:

软件 版本
Node.js 14.16.0
npm 6.14.11
truffle 5.2.6
Ganache 2.5.4

项目搭建

Ganache创建测试链

  1. 运行Ganache后选择 NEW WOEKSPACE
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121724165.png
    NEW WOEKSPACE
  2. 为你的测试链取一个响亮的名字并保存
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121732526.png
    创建测试链
  3. 记下网络参数
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121821687.png
    记下参数

Truffle创建项目

  1. 创建文件夹 auction
  2. 进入文件夹执行truffle init初始化
  3. 创建智能合约truffle create contract Auction
  4. 在 truffle-config.js 配置文件中配置测试链
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121745041.png
    配置测试链
  5. 项目架构如下
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
AUCTION
├── build                       // 编译后的智能合约
│   └── contracts
│       └── Auction.json
├── contracts                   // 智能合约目录
│   └── Auction.sol
├── migrations                  // 迁移文件目录
│   └── 1_initial_migration.js
├── test                        // 测试文件目录
└── truffle-config.js           // 配置文件

编写合约

拍卖流程如下:在拍卖合约部署时(构造)会绑定拍卖时长和最终受益人,每个用户可以通过公开渠道进行竞拍,合约会储存所有的竞拍记录;当出价高于已有记录时会更新最高出价人和最高出价,同时也会返还原/退回最高出价人的出价;拍卖时间结束后会将拍卖收益发送给最终受益人并关闭所有合约修改接口。完整代码如下2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Auction {
    // 拍卖的参数:受益人、结束时间
    address payable public beneficiary;
    // 时间是unix的绝对时间戳(自1970-01-01以来的秒数)或以秒为单位的时间段。
    uint public auctionEnd;
    // 拍卖的当前状态:最高出价者、最高出价
    address public highestBidder;
    uint public highestBid;
    //保存历史竞价历史,用于取回的之前的出价
    mapping(address => uint256) pendingReturns;
    // 拍卖结束后设为 true,将禁止所有的变更
    bool ended;

    // 变更触发的事件:最高价更新、拍卖结束
    event HighestBidIncreased(address bidder, uint256 amount);
    event AuctionEnded(address winner, uint256 amount);

    // 构造函数:拍卖持续时间、受益人
    constructor(uint256 _biddingTime, address payable _beneficiary) public {
        beneficiary = _beneficiary;
        auctionEnd = now + _biddingTime;
    }

    /// 对拍卖进行出价,具体的出价随交易一起发送。如果没有在拍卖中胜出,则返还出价。
    function bid() public payable {
        // 出价参数已经包含在了交易中;关键字 payable 是处理以太币过程中必须的。

        // 如果拍卖已结束,竞拍失败
        require(now <= auctionEnd, "Auction already ended.");

        // 如果出价不够高,竞拍失败
        require(msg.value > highestBid, "There already is a higher bid.");

        if (highestBid != 0) {
            // 返还出价时,简单地直接调用 highestBidder.send(highestBid) 函数,是有安全风险的,因为它有可能执行一个非信任合约。
            // 更为安全的做法是让接收方自己通过withdraw函数提取金钱。
            pendingReturns[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit HighestBidIncreased(msg.sender, msg.value);
    }

    /// 取回出价(当该出价已被超越)
    function withdraw() public returns (bool) {
        uint256 amount = pendingReturns[msg.sender];
        if (amount > 0) {
            // 这里很重要,首先要设零值。因为,作为接收调用的一部分,接收者可以在  send  返回之前,重新调用该函数。
            pendingReturns[msg.sender] = 0;
            if (!msg.sender.send(amount)) {
                // 这里不需抛出异常,只需重置未付款
                pendingReturns[msg.sender] = amount;
                return false;
            }
        }
        return true;
    }

    /// 结束拍卖,并把最高的出价发送给受益人
    function endAuction() public {
        // 对于可与其他合约交互的函数(意味着它会调用其他函数或发送以太币),一个好的指导方针是将其结构分为三个阶段:
        // 1. 检查条件
        // 2. 执行动作 (可能会改变条件)
        // 3. 与其他合约交互
        // 如果这些阶段相混合,其他的合约可能会回调当前合约并修改状态,或者导致某些效果(比如支付以太币)多次生效。
        // 如果合约内调用的函数包含了与外部合约的交互,则它也会被认为是与外部合约有交互的。

        // 1. 条件:拍卖首次结束
        require(now >= auctionEnd, "Auction not yet ended.");
        require(!ended, "auctionEnd has already been called.");

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

        // 3. 交互
        beneficiary.transfer(highestBid);
    }
}

编译部署

  1. 创建部署文件migrations/1_initial_migration.js
    1
    2
    3
    4
    
    const Auction = artifacts.require("Auction");
    module.exports = function (deployer) {
    deployer.deploy(Auction, 20000, "0xd136bE3960877709AC32fc6f5A3c299BFc9827Ac");
    };
    
    其中20000是拍卖时长,也就是从部署的那一刻算起向后持续的时间,单位为秒,建议不要太小;0xd136bE3960877709AC32fc6f5A3c299BFc9827Ac是最终受益人的钱包地址,可在Ganache的默认十个账户中随便选一个,这里选择的是第0个账户。
  2. 编译合约
    1
    2
    3
    4
    5
    6
    7
    8
    
    C:\Users\Fantasy\Desktop\auction>truffle compile
    
    Compiling your contracts...
    ===========================
    > Compiling .\contracts\Auction.sol
    > Artifacts written to C:\Users\Fantasy\Desktop\auction\build\contracts
    > Compiled successfully using:
    - solc: 0.5.16+commit.9c3226ce.Emscripten.clang
    
  3. 部署合约
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    C:\Users\Fantasy\Desktop\auction>truffle migrate 
    ……
    1_initial_migration.js
    ======================
    Replacing 'Auction'
    -------------------
    > transaction hash:    0xc8395261413c662e00f3822b6647b0249fa6b789476b8efda680a7394d3491ac
    > Blocks: 0            Seconds: 0
    > contract address:    0x4a9119E5b00b1Fd0681077C78688500637eAE629
    > block number:        1
    > block timestamp:     1628764710
    > account:             0xd136bE3960877709AC32fc6f5A3c299BFc9827Ac
    > balance:             99.98959302
    > gas used:            520349 (0x7f09d)
    > gas price:           20 gwei
    > value sent:          0 ETH
    > total cost:          0.01040698 ETH
    > Saving artifacts
    -------------------------------------
    > Total cost:          0.01040698 ETH
    Summary
    =======
    > Total deployments:   1
    > Final cost:          0.01040698 ETH
    
    提示信息中可以看到合约的地址为0x4a9119E5b00b1Fd0681077C78688500637eAE629,部署成功后可在 Ganache 的 BLOCKS 栏看见一个新的区块,并且第0个账户的以太币减少,这是因为部署智能合约会消耗以太币,而部署过程默认使用第0个账户部署。

测试交互

  1. 输入指令进入 console
    1
    2
    
    C:\Users\Fantasy\Desktop\auction>truffle console
    truffle(development)>
    
    进入 console 界面后可按两次 tab 建获取提示信息,可以看见我们刚部署的智能合约已经生效:
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121847275.png
    console提示
  2. 获取刚部署的智能合约实例
    1
    2
    3
    4
    5
    6
    7
    8
    
    truffle(development)> let app = await Auction.deployed()
    undefined
    truffle(development)> app.
    ......
    app.AuctionEnded          app.HighestBidIncreased   app.abi                   app.address               app.allEvents
    app.auctionEnd            app.beneficiary           app.bid                   app.constructor           app.contract
    app.endAuction            app.getPastEvents         app.highestBid            app.highestBidder         app.methods
    app.send                  app.sendTransaction       app.transactionHash       app.withdraw
    
    这里注意一定要加上await关键字,否则会获取失败。其它相关的方法为3
    • 获取指定合约实例:let app = await Auction.at("0xd136bE3960877709AC32fc6f5A3c299BFc9827Ac")
    • 部署新的智能合约:let app2 = await Auction.new(2000, "0xd136bE3960877709AC32fc6f5A3c299BFc9827Ac")
  3. 第1个账户发起2个以太币的竞拍
    1
    
    truffle(development)> app.bid({from:accounts[1], value:web3.utils.toWei("2")})
    
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121859031.png
    发起竞拍1
  4. 第5个账户发起4个以太币的竞拍
    1
    
    truffle(development)> app.bid({from:accounts[5], value:web3.utils.toWei("4")})
    
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121901339.png
    发起竞拍2
    在竞拍后可以看见区块链上增加了两个新的块:
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121942767.png
    发起竞拍
    在竞拍后可以看见区块链上用户余额变化:
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121943263.png
    竞拍后余额变化
  5. 查看当前最高出价人和最高出价
    1
    2
    3
    4
    5
    6
    
    truffle(development)> app.highestBidder()
    '0x898a007a57EDd1a37202D4F3781acB589Af1EDA3'
    truffle(development)> let highestBid = await app.highestBid()
    undefined
    truffle(development)> highestBid.toString()
    '4000000000000000000'
    
    需要注意的是,app.highestBid()返回的是 BN(BigNumber)格式的数据无法直接查看,必须结合 await 关键字才能正常显示4
  6. 账户1非最高出价可以赎回出价
    1
    
    truffle(development)> app.withdraw({from:accounts[1]})
    
    此时可以看见链上增加了一个区块,并且账户1的2个以太币已退回:
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108121949736.png
    赎回出价
  7. 拍卖时间结束后申请发放最终收益
    1
    
    truffle(development)> app.endAuction()
    
    此时可以看见最终拍卖价格4个以太币已经发送到账户0上:
    https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/202108122007512.png
    发放最终收益

参考


  1. 朵耳. WIN10下node.js \ npm \ truffle的安装与配置. 简书. [2019-03-09] ↩︎

  2. learnblockchain.cn. 秘密竞价(盲拍)合约. learnblockchain.cn. [2020-07-23] ↩︎

  3. learnblockchain.cn. 与合约进行交互. learnblockchain.cn. [2019-05-21] ↩︎

  4. Ofir Baruch. Convert BN to number. Stack overflow. [2020/07/23] ↩︎