美文网首页区块链入门以太坊区块链研习社
第八课 如何调试以太坊官网的智能合约众筹案例

第八课 如何调试以太坊官网的智能合约众筹案例

作者: 笔名辉哥 | 来源:发表于2018-04-20 15:59 被阅读737次

    1. 文章摘要

    【本文目标】
    发布并执行通ETH官网的众筹合约代码。
    【前置条件】
    参考《第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易》完成了ColorBay的发行。
    【技术收获】
    1). 调试成功以太坊官网的智能合约众筹代码
    2). REMIX和myetherwallet配合的智能合约代码调试
    【实操课程列表】
    第一课 如何在WINDOWS环境下搭建以太坊开发环境
    第二课 如何实现以太坊最简智能合约“Hello World”的运行
    第四课 以太坊开发框架Truffle从入门到实战
    第六课 技术小白如何开发一个DAPP区块链应用(以宠物商店为例)
    第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易
    第八课 如何调试以太坊官网的智能合约众筹案例
    【说明】未列出的课程为知识普及的非实操类课程,所有区块链文章参考“区块链入口”专栏。

    2.众筹和代币(TOKEN)的投资逻辑

    ICO(Initial Crypto-Token Offering,首次代币众筹)被认为是区块链生态内生的一种新型投融资方式,概念起源于IPO,只不过募集的货币变为比特币、以太坊等通用数字货币,从而支持项目的开发成本。
    目前对于ICO没有统一的定义, 一般而言,ICO指区块链初创项目在区块链平台上发行项目独有的加密代币,投资者通过使用指定的数字货币(如比特币、以太币)购买代币的方式为项目进行众筹融资的行为。代币依项目不同代表了对项目未来的使用权、投票权等。随着项目成果获得认可,使用人数增加,代币作为交易媒介或权益的价值获得不断提升。
    2013年7月募集了5000个比特币的Mastercoin(现名为 Omni)是首个有记录的ICO,而以太坊在2014年7月超过1500万美元的ICO则开启了ICO快速发展的进程。2015 年,The DAO实现高达1.5亿美元融资,但后因受黑客攻击而失败。2016年以来,ICO众筹速度快、募集金额不断升高,常出现哄抢一空的情况。

    众筹列表

    ICO的流程及关键元素
    对于ICO的流程没有统一的概述,一般认为ICO的流程总体可以分成准备期、窗口期、测试期和项目运行四个阶段。这四个阶段的主要内容如下:

    ICO众筹流程图

    在ICO中有众多参与者与关键要素,可能包括 ICO 项目发起者、ICO 众筹平台、代币、代币钱包(部分直接就是平台或项目运行平台中的功能)等。
    ICO 风险评估方法
    针对 ICO 投资的高风险状况,知名的区块链网站 Smith+Crown 在其ICO 手册中给出了几点投资参考,其首要剔提出的投资建议就是关注项目团队和项目执行力。而《财经》杂志也在6月5日的文章中也给出了鉴别风险的参考建议:

    九大问题.jpg

    作为技术派,本文不再探讨技术使用背后的是是非非,只聚焦在众筹代码技术的实现和调试。

    3,官网智能合约众筹代码分析

    以太坊官网有一段关于ICO众筹的代码和运行介绍,但是其提供的测试环境跟很多人的测试环境不同,对测试步骤也介绍不全,很多人无法正常运行该智能合约,对其中的功能也不剩了解。
    本文对这段代码增加了中文注释,并且对部分不适合的代码做了微调修改,并在下一章节提供了详细的调试步骤说明,供技术小白傻瓜式入门学习。

    pragma solidity ^0.4.16;
    
    interface token {
        function transfer(address receiver, uint amount);
    }
    
    contract Crowdsale {
        address public beneficiary;  // 募资成功后的收款方
        uint public fundingGoal;   // 募资额度
        uint public amountRaised;   // 参与数量
        uint public deadline;      // 募资截止期
    
        uint public price;    //  token 与以太坊的汇率 , token卖多少钱
        token public tokenReward;   // 要卖的token
    
        mapping(address => uint256) public balanceOf;
    
        bool public fundingGoalReached = false;  // 众筹是否达到目标
        bool public crowdsaleClosed = false;   //  众筹是否结束
    
        /**
        * 事件可以用来跟踪信息
        **/
        event GoalReached(address recipient, uint totalAmountRaised);
        event FundTransfer(address backer, uint amount, bool isContribution);
        event LogAmount(uint amount);
    
        /**
         * 构造函数, 设置相关属性
         */
        function Crowdsale(
            address ifSuccessfulSendTo,
            uint fundingGoalInEthers,
            uint durationInMinutes,
            uint weiCostOfEachToken,
            address addressOfTokenUsedAsReward) {
                beneficiary = ifSuccessfulSendTo;
                fundingGoal = fundingGoalInEthers * 1 ether;
                deadline = now + durationInMinutes * 1 minutes;
                /*一个TOKEN等同于1个以太坊ETH太贵了,修改官网代码,变为一个TOKEN等同于1个wei*/
                /*price = etherCostOfEachToken * 1 ether;*/
                price = weiCostOfEachToken * 1 wei;
                tokenReward = token(addressOfTokenUsedAsReward);   // 传入已发布的 token 合约的地址来创建实例
        }
    
        /**
         * 无函数名的Fallback函数,
         * 在向合约转账时,这个函数会被调用
         */
        function () payable {
            require(!crowdsaleClosed);
            uint amount = msg.value;
            balanceOf[msg.sender] += amount;
            amountRaised += amount;
            LogAmount(amount);/*打款3个ETH,判断此处是3还是3*10^18*/
            /*官网这个代码有问题,导致打回的币的数量会非常小,此处*1000倍,表示
              1个ETH等于1000个TOKEN/
            /*tokenReward.transfer(msg.sender, amount / price);*/
            tokenReward.transfer(msg.sender, 1000 * (amount / price));
            /*msg.sender对应的是当前运行的外部账号的地址*/
            FundTransfer(msg.sender, amount, true);
        }
    
        /**
        *  定义函数修改器modifier(作用和Python的装饰器很相似)
        * 用于在函数执行前检查某种前置条件(判断通过之后才会继续执行该方法)
        * _ 表示继续执行之后的代码
        **/
        modifier afterDeadline() { if (now >= deadline) _; }
    
        /**
         * 判断众筹是否完成融资目标, 这个方法使用了afterDeadline函数修改器
         * 此段代码不会在deadline后自动运行,而是需要在deadline时间到后人工点击执行
         * 如果在deadline时间前人工点击,会中断,也不会执行函数体代码;
         */
        function checkGoalReached() afterDeadline {
            if (amountRaised >= fundingGoal) {
                fundingGoalReached = true;
                GoalReached(beneficiary, amountRaised);
            }
            crowdsaleClosed = true;
        }
    
    
        /**
         * 完成融资目标时,融资款发送到收款方
         * 未完成融资目标时,执行退款
         * 此段代码不会在deadline后自动运行,而是在deadline时间到后人工点击执行
         * 如果在deadline时间前人工点击,会中断,也不会执行函数体代码;
         */
        function safeWithdrawal() afterDeadline {
            /*众筹截止时间后,如果众筹目标没有达到,则执行退款到当前外部账号*/
            /*官网的这段代码的健壮性不够,要使合约的执行逻辑合理,则需要需要保持当前账号为众筹打ETH的账号*/
            if (!fundingGoalReached) {
                uint amount = balanceOf[msg.sender];
                balanceOf[msg.sender] = 0;
                if (amount > 0) {
                    if (msg.sender.send(amount)) {
                        FundTransfer(msg.sender, amount, false);
                    } else {
                        balanceOf[msg.sender] = amount;
                    }
                }
            }
            /*如果众筹目标达到了,并且受益账号等同于当前账号,则把众筹到的ETH打给当前账号*/
            if (fundingGoalReached && beneficiary == msg.sender) {
                if (beneficiary.send(amountRaised)) {
                    FundTransfer(beneficiary, amountRaised, false);/**/
                } else {
                    //If we fail to send the funds to beneficiary, unlock funders balance
                    fundingGoalReached = false;
                }
            }
        }
    }
    

    函数说明
    1,Crowdsale: 众筹合约的构造函数
    ifSuccessfulSendTo: 募资成功后的收款方(本案例固定为合约创建者)
    fundingGoalInEthers: 募资额度, 为了方便我们仅募3个ether
    durationInMinutes: 募资时间,为了测试,案例时间设置为10分钟
    weiCostOfEachToken:每个代币的价格, 案例在函数内部放大1000倍,设置为1,实际表示1个ETH需要发放1000个代币;
    addressOfTokenUsedAsReward: 代币合约地址,案例发布的彩贝币(CB)的地址为"0x5eeec41dc08d7caece17c4a349635934637036f1";

    2,function () payablepayable: 回调函数
    没有函数名的payalbe函数为回调函数,意思是往智能合约地址打ETH的时候,则会自动调用该函数执行。
    该函数的作用是收到ETH时,给众筹账号返回1000*n个ETH的彩贝CB代币。这儿代码健壮性不够,不管众筹是否成功,众筹账号都收到了CB代币。

    3. checkGoalReached:检查众筹目标是否达到
    该调用函数修改器modifier的函数afterDeadline,只是表示截止时间前执行这个代码,实际不会checkGoalReached执行函数体的代码,只会执行afterDeadline的代码后就返回。
    该函数的功能是设置fundingGoalReached为true表示众筹目标达到,设置crowdsaleClosed为true表示众筹可关闭。
    该函数在截止时间到后要人工执行的,不会自动调用。

    4. safeWithdrawal: 众筹结束执行代码
    该调用函数修改器modifier的函数afterDeadline,只是表示截止时间前执行这个代码,实际不会checkGoalReached执行函数体的代码,只会执行afterDeadline的代码后就返回。
    如果众筹目标没有达到,则在当前执行账号为众筹账号的情况下,把募集的ETH打回给众筹发送的账号。
    如果众筹目标账号达到,则把募集的ETH打给智能合约创建的账号。

    4,智能合约众筹代码调试

    智能合约执行的代码的坑较多,本文通过一步步的演示,给大家说明在REMIX+MetaMASK的环境下,如何完成该众筹合约代码的成功执行。

    目标和总体步骤

    目标:在10分钟内众筹3个ETH,返回3个ColorBay代币
    前提条件:参考《第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易》的实现,代币已经创建成功。
    具体步骤:
    [1] 在ACCOUNT 8上创建众筹智能合约,内容为ACCOUNT8在10分钟内众筹3个ETH,代币为ColorBay。
    [2] ACCOUNT 8打3个ColorBay TOKEN给众筹智能合约
    [3] ACCOUNT 1打3个ETH给众筹智能合约,同事收到3000个ColorBay
    [4] 10分钟时间到后,人工执行checkGoalReached翻转众筹智能合约状态
    [5] 10分钟时间到后,人工执行safeWithdrawal把众筹ETH打给收益账户ACCOUNT8

    [1] 在ACCOUNT 8上创建众筹智能合约

    直接调用REMIX官网编辑器地址即可调用Remix SOLIDITY 编辑器,如果该链接不能打开的话,你可以使用国内的小编专用Remix SOLIDITY编辑器 ,把上面的智能合约代码COPY后完成编译。

    编译成功
    设置MetaMASK的外部账户为ACCOUNT8(只要是里面有一定的ETH和代币的账户就行),
    image.png

    在ACCOUNT 8上创建众筹智能合约,内容为ACCOUNT8在10分钟内众筹3个ETH,代币为ColorBay。每个代币的价格1wei,代币合约地址为CB彩贝代币智能合约地址"0x5eeec41dc08d7caece17c4a349635934637036f1".不知道这个地址来由的参考第七课的“MetaMask加载TOKEN”章节描述。
    “Create”按钮的输入框代码为

    "0x3D7DfB80E71096F2c4Ee63C42C4D849F2CBBE363",1, 10, 1,"0x5eeec41dc08d7caece17c4a349635934637036f1"
    

    【说明】在remix中输入地址一定要加英文""表示。

    具体的配置和操作步骤参考下图: 创建众筹智能合约 智能合约创建成功,点击Remix输出框的对应函数的"Detail"按钮,可以看到一些信息。 智能合约创建成功

    我们可以获得该众筹智能合约的地址为

    0x58103623f9ebd9b6a0518160c257e3884ddf0d08

    [2] ACCOUNT 8打3个和3000个ColorBay TOKEN给众筹智能合约

    转账3个Color Bay步骤
    进入Network Ropsten(infura.io)的转账环境(由于你懂的原因误伤,国内有些互联网环境下无法打开时,请使用手机移动热点的方式访问即可打开)

    转账CB设置步骤
    确认交易
    弹出后,点击支付交易费用
    之后,点击网页下方的“Verify Transaction”,可以看到交易信息的区块进度,最终交易成功。
    此次交易块的地址为https://ropsten.etherscan.io/tx/0xc9013c7b57fca80c894d508a62bbdf019592466e1ff13a0139f1d3123c32a904
    交易信息查看
    转账3000个Color Bay步骤
    按照我们的规划,转让3个Color Bay(CB)是不够的,我们要相同步骤再转账一次3000个CB的。
    此次交易块的地址为
    https://ropsten.etherscan.io/tx/0xaaf04f1a63032bf2be77dc36d219f7bc4ce638a61a8b0b0c40d3c077bdd6b3a3
    下面截图介绍一下交易信息内容描述:
    交易概述页
    跟踪事件页,下面EventLogs对应的是事件函数" event Transfer(address indexed from, address indexed to, uint256 value);"
    【说明】事件,用来通知客户端交易发生,不会定义函数体,仅仅记录输入参数。
    跟踪事件页

    [3]ACCOUNT 1打3个ETH给众筹智能合约,同时收到3000个ColorBay

    确保测试账号ACCOUNT 1有3个ETH测试币,没有的话采用BUY按钮免费买几个。

    MetaMASK切换到ACCOUNT 1
    浏览器切换到https://www.myetherwallet.com/#send-transaction网站,转账3个ETH给众筹智能合约地址
    转账3个ETH
    转账确认
    提交交易费用

    本次交易对应的交易信息网址为https://ropsten.etherscan.io/tx/0x485087d6bdbb92b292349694dd99c3ec698d4e9ddb0d573dda84395a59257ef7#eventlog。可见,已经把3000个代币打回给ACCOUNT1账号了。参考下图解释下事件描述:

    打3个ETH,收到3000个CB
    此时查看ACCOUNT1的账号信息,可以发现增加了3000个CB地址:
    https://ropsten.etherscan.io/token/0x5eeec41dc08d7caece17c4a349635934637036f1?a=0xd1f7922e8b78cbeb182250753ade8379d1e09949
    截图说明:
    代币流转信息
    【说明】官网的这个代码不够智能,即使没有众筹成功,代币也已经发给众筹者了,这个逻辑不够严谨。
    ACCOUNT1减少了3个ETH,查看地址:
    https://ropsten.etherscan.io/address/0xd1f7922e8b78cbeb182250753ade8379d1e09949
    ETH的变化
    [4] 10分钟时间到后,Meta账户切换为Account 8,人工执行checkGoalReached翻转众筹智能合约状态

    [4] 10分钟时间到后,Meta账户切换为Account 8,人工执行checkGoalReached翻转众筹智能合约状态

    此处的操作作者曾经踩了个大坑。没有认真阅读代码,把时间设置为100分钟,然后在100分钟内点击checkGoalReached函数,发现没有任何变化。


    10分钟时间到达后,检查众筹状态 众筹状态结果

    [5] 10分钟时间到后,人工执行safeWithdrawal把众筹ETH打给收益账户ACCOUNT8

    众筹结束打币
    众筹结束事件查看图 众筹的ETH已到账

    整个交易流程的代币转移可以查看代币合约的链接信息,看看事件可以看到所有的交易记录和账号:
    https://ropsten.etherscan.io/address/0x5eeec41dc08d7caece17c4a349635934637036f1#events

    5,一个具有商用价值的众筹智能合约代码

    个人觉得官网的这个智能合约不够好。理想中符合逻辑的智能合约应该是时间到后自动去检查众筹金额,到达目标时则自动执行合约,未到达目标时则代币和ETH均返回原始的账号。
    画了业务流程图,但是还没有精力实现这段代码。待有兴趣的码友实现后交流。


    商用的众筹智能合约流程

    欧阳哥哥实现了该智能合约代码,并给出了详细的测试步骤,有兴趣的同学可自己分析。《【以太坊开发】众筹智能合约改进》

    pragma solidity ^0.4.23;
    
    library SafeMath {
      function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
          return 0;
        }
        uint256 c = a * b;
        assert(c / a == b);
        return c;
      }
    
      function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
      }
    
      function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
      }
    
      function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
      }
    }
    
    contract Ownable {
      address public owner;
    
    
      event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
    
      /**
       * @dev The Ownable constructor sets the original `owner` of the contract to the sender
       * account.
       */
      function Ownable() public {
        owner = msg.sender;
      }
    
    
      /**
       * @dev Throws if called by any account other than the owner.
       */
      modifier onlyOwner() {
        require(msg.sender == owner);
        _;
      }
    
    
      /**
       * @dev Allows the current owner to transfer control of the contract to a newOwner.
       * @param newOwner The address to transfer ownership to.
       */
      function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0));
        OwnershipTransferred(owner, newOwner);
        owner = newOwner;
      }
    
    }
    
    contract Pausable is Ownable {
      event Pause();
      event Unpause();
    
      bool public paused = false;
    
    
      /**
       * @dev Modifier to make a function callable only when the contract is not paused.
       */
      modifier whenNotPaused() {
        require(!paused);
        _;
      }
    
      /**
       * @dev Modifier to make a function callable only when the contract is paused.
       */
      modifier whenPaused() {
        require(paused);
        _;
      }
    
      /**
       * @dev called by the owner to pause, triggers stopped state
       */
      function pause() onlyOwner whenNotPaused public {
        paused = true;
        Pause();
      }
    
      /**
       * @dev called by the owner to unpause, returns to normal state
       */
      function unpause() onlyOwner whenPaused public {
        paused = false;
        Unpause();
      }
    }
    
    contract ERC20Basic {
      uint256 public totalSupply;
      function balanceOf(address who) public view returns (uint256);
      function transfer(address to, uint256 value) public returns (bool);
      event Transfer(address indexed from, address indexed to, uint256 value);
    }
    
    contract ERC20 is ERC20Basic {
      function allowance(address owner, address spender) public view returns (uint256);
      function transferFrom(address from, address to, uint256 value) public returns (bool);
      function approve(address spender, uint256 value) public returns (bool);
      event Approval(address indexed owner, address indexed spender, uint256 value);
    }
    
    contract BasicToken is ERC20Basic {
      using SafeMath for uint256;
    
      mapping(address => uint256) balances;
    
      /**
      * @dev transfer token for a specified address
      * @param _to The address to transfer to.
      * @param _value The amount to be transferred.
      */
      function transfer(address _to, uint256 _value) public returns (bool) {
        require(_to != address(0));
        require(_value <= balances[msg.sender]);
    
        // SafeMath.sub will throw if there is not enough balance.
        balances[msg.sender] = balances[msg.sender].sub(_value);
        balances[_to] = balances[_to].add(_value);
        Transfer(msg.sender, _to, _value);
        return true;
      }
    
      /**
      * @dev Gets the balance of the specified address.
      * @param _owner The address to query the the balance of.
      * @return An uint256 representing the amount owned by the passed address.
      */
      function balanceOf(address _owner) public view returns (uint256 balance) {
        return balances[_owner];
      }
    
    }
    
    contract StandardToken is ERC20, BasicToken {
    
      mapping (address => mapping (address => uint256)) internal allowed;
    
    
      /**
       * @dev Transfer tokens from one address to another
       * @param _from address The address which you want to send tokens from
       * @param _to address The address which you want to transfer to
       * @param _value uint256 the amount of tokens to be transferred
       */
      function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
        require(_to != address(0));
        require(_value <= balances[_from]);
        require(_value <= allowed[_from][msg.sender]);
    
        balances[_from] = balances[_from].sub(_value);
        balances[_to] = balances[_to].add(_value);
        allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
        Transfer(_from, _to, _value);
        return true;
      }
    
      /**
       * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
       *
       * Beware that changing an allowance with this method brings the risk that someone may use both the old
       * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
       * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
       * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
       * @param _spender The address which will spend the funds.
       * @param _value The amount of tokens to be spent.
       */
      function approve(address _spender, uint256 _value) public returns (bool) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);
        return true;
      }
    
      /**
       * @dev Function to check the amount of tokens that an owner allowed to a spender.
       * @param _owner address The address which owns the funds.
       * @param _spender address The address which will spend the funds.
       * @return A uint256 specifying the amount of tokens still available for the spender.
       */
      function allowance(address _owner, address _spender) public view returns (uint256) {
        return allowed[_owner][_spender];
      }
    
      /**
       * @dev Increase the amount of tokens that an owner allowed to a spender.
       *
       * approve should be called when allowed[_spender] == 0. To increment
       * allowed value is better to use this function to avoid 2 calls (and wait until
       * the first transaction is mined)
       * From MonolithDAO Token.sol
       * @param _spender The address which will spend the funds.
       * @param _addedValue The amount of tokens to increase the allowance by.
       */
      function increaseApproval(address _spender, uint _addedValue) public returns (bool) {
        allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue);
        Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
        return true;
      }
    
      /**
       * @dev Decrease the amount of tokens that an owner allowed to a spender.
       *
       * approve should be called when allowed[_spender] == 0. To decrement
       * allowed value is better to use this function to avoid 2 calls (and wait until
       * the first transaction is mined)
       * From MonolithDAO Token.sol
       * @param _spender The address which will spend the funds.
       * @param _subtractedValue The amount of tokens to decrease the allowance by.
       */
      function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) {
        uint oldValue = allowed[msg.sender][_spender];
        if (_subtractedValue > oldValue) {
          allowed[msg.sender][_spender] = 0;
        } else {
          allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
        }
        Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
        return true;
      }
    
    }
    
    contract PausableToken is StandardToken, Pausable {
    
      function transfer(address _to, uint256 _value) public whenNotPaused returns (bool) {
        return super.transfer(_to, _value);
      }
    
      function transferFrom(address _from, address _to, uint256 _value) public whenNotPaused returns (bool) {
        return super.transferFrom(_from, _to, _value);
      }
    
      function approve(address _spender, uint256 _value) public whenNotPaused returns (bool) {
        return super.approve(_spender, _value);
      }
    
      function increaseApproval(address _spender, uint _addedValue) public whenNotPaused returns (bool success) {
        return super.increaseApproval(_spender, _addedValue);
      }
    
      function decreaseApproval(address _spender, uint _subtractedValue) public whenNotPaused returns (bool success) {
        return super.decreaseApproval(_spender, _subtractedValue);
      }
    }
    
    contract ColorBayTestToken is PausableToken {
        string public name;
        string public symbol;
        uint256 public decimals = 18;
    
        function ColorBayTestToken(uint256 initialSupply, string tokenName, string tokenSymbol) public {
            totalSupply = initialSupply * 10 ** uint256(decimals);
            balances[msg.sender] = totalSupply;
            name = tokenName;
            symbol = tokenSymbol;
        }
    }
    
    
    
    //-----------------------------------------------------------------------------
    
    
    
    
    interface token {
        function transfer(address receiver, uint amount);
    }
    
    contract Crowdsale is Ownable {
        using SafeMath for uint256;
        address public beneficiary;
        uint public fundingGoal;
        uint public amountRaised;
        uint public deadline;
        uint public price;
        token public tokenReward;
        mapping(address => uint256) public balanceOf;
        bool public fundingGoalReached = false;
        bool public crowdsaleClosed = false;
        
        event GoalReached(address recipient, uint totalAmountRaised);
        event FundTransfer(address backer, uint amount, bool isContribution);
        
        
        address[] public funder;
        
        modifier afterDeadline() { if (now >= deadline) _; }
        
        function Crowdsale(
            address ifSuccessfulSendTo,
            uint fundingGoalInEthers,
            uint durationInMinutes,
            uint finneyCostOfEachToken,
            address addressOfTokenUsedAsReward) public {
                beneficiary = ifSuccessfulSendTo;
                fundingGoal = fundingGoalInEthers.mul(1 ether);
                deadline = now + durationInMinutes.mul(1 minutes);
                price = finneyCostOfEachToken.mul(1 finney);
                tokenReward = token(addressOfTokenUsedAsReward);
        }
        
        event LogPay(address sender, uint value, uint blance, uint amount, bool isClosed);
        function () public payable {
            require(!crowdsaleClosed);
            funder.push(msg.sender);
            balanceOf[msg.sender] = balanceOf[msg.sender].add(msg.value);
            amountRaised = amountRaised.add(msg.value);
            if(amountRaised >= fundingGoal) {
                crowdsaleClosed = true;
                fundingGoalReached = true;
            }
            emit LogPay(msg.sender, msg.value, balanceOf[msg.sender], amountRaised, crowdsaleClosed);
        }
        
        function getThisBalance() public constant returns (uint) {
            return this.balance;
        }
        
        function getNow() public constant returns (uint, uint) {
            return (now, deadline);
        }
        
        function setDeadline(uint minute) public onlyOwner {
            deadline = minute.mul(1 minutes).add(now);
        }
        
        function safeWithdrawal() public onlyOwner afterDeadline {
            if(amountRaised >= fundingGoal) {
                crowdsaleClosed = true;
                fundingGoalReached = true;
                emit GoalReached(beneficiary, amountRaised);
            } else {
                crowdsaleClosed = false;
                fundingGoalReached = false;
            }
            uint i;
            if(fundingGoalReached) {
                if(amountRaised > fundingGoal && funder.length>0) {
                    address returnFunder = funder[funder.length.sub(1)];
                    uint overFund = amountRaised.sub(fundingGoal);
                    if(returnFunder.send(overFund)) {
                        balanceOf[returnFunder] = balanceOf[returnFunder].sub(overFund);
                        amountRaised = fundingGoal;
                    }
                }
                for(i = 0; i < funder.length; i++) {
                    tokenReward.transfer(funder[i], balanceOf[funder[i]].mul(1 ether).div(price));
                    balanceOf[funder[i]] = 0;
                }
                if (beneficiary.send(amountRaised)) {
                    emit FundTransfer(beneficiary, amountRaised, false);
                } else {
                    fundingGoalReached = false;
                }
                
            } else {
                for(i = 0; i < funder.length; i++) {
                    if (balanceOf[funder[i]] > 0 && funder[i].send(balanceOf[funder[i]])) {
                        amountRaised = 0;
                        balanceOf[funder[i]] = 0;
                        emit FundTransfer(funder[i], balanceOf[funder[i]], false);
                    }
                }
            }
        }
        
    }
    
    

    6,唯链的众筹智能合约

    小编因为参与过唯链VeChain的众筹,了解唯链的众筹智能合约代码,这个是纯商用的代码价值。
    唯链众筹智能合约地址点击查看
    他们的代码和交易信息如下:

    pragma solidity ^0.4.11;
    
    contract Owned {
    
        address public owner;
    
        function Owned() {
            owner = msg.sender;
        }
    
        modifier onlyOwner() {
            require(msg.sender == owner);
            _;
        }
    
        function setOwner(address _newOwner) onlyOwner {
            owner = _newOwner;
        }
    }
    
    /**
     * @title SafeMath
     * @dev Math operations with safety checks that throw on error
     */
    library SafeMath {
      function mul(uint256 a, uint256 b) internal constant returns (uint256) {
        uint256 c = a * b;
        assert(a == 0 || c / a == b);
        return c;
      }
    
      function div(uint256 a, uint256 b) internal constant returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
      }
    
      function sub(uint256 a, uint256 b) internal constant returns (uint256) {
        assert(b <= a);
        return a - b;
      }
    
      function add(uint256 a, uint256 b) internal constant returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
      }
    
      function toUINT112(uint256 a) internal constant returns(uint112) {
        assert(uint112(a) == a);
        return uint112(a);
      }
    
      function toUINT120(uint256 a) internal constant returns(uint120) {
        assert(uint120(a) == a);
        return uint120(a);
      }
    
      function toUINT128(uint256 a) internal constant returns(uint128) {
        assert(uint128(a) == a);
        return uint128(a);
      }
    }
    
    
    // Abstract contract for the full ERC 20 Token standard
    // https://github.com/ethereum/EIPs/issues/20
    
    contract Token {
        /* This is a slight change to the ERC20 base standard.
        function totalSupply() constant returns (uint256 supply);
        is replaced with:
        uint256 public totalSupply;
        This automatically creates a getter function for the totalSupply.
        This is moved to the base contract since public getter functions are not
        currently recognised as an implementation of the matching abstract
        function by the compiler.
        */
        /// total amount of tokens
        //uint256 public totalSupply;
        function totalSupply() constant returns (uint256 supply);
    
        /// @param _owner The address from which the balance will be retrieved
        /// @return The balance
        function balanceOf(address _owner) constant returns (uint256 balance);
    
        /// @notice send `_value` token to `_to` from `msg.sender`
        /// @param _to The address of the recipient
        /// @param _value The amount of token to be transferred
        /// @return Whether the transfer was successful or not
        function transfer(address _to, uint256 _value) returns (bool success);
    
        /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from`
        /// @param _from The address of the sender
        /// @param _to The address of the recipient
        /// @param _value The amount of token to be transferred
        /// @return Whether the transfer was successful or not
        function transferFrom(address _from, address _to, uint256 _value) returns (bool success);
    
        /// @notice `msg.sender` approves `_addr` to spend `_value` tokens
        /// @param _spender The address of the account able to transfer the tokens
        /// @param _value The amount of wei to be approved for transfer
        /// @return Whether the approval was successful or not
        function approve(address _spender, uint256 _value) returns (bool success);
    
        /// @param _owner The address of the account owning tokens
        /// @param _spender The address of the account able to transfer the tokens
        /// @return Amount of remaining tokens allowed to spent
        function allowance(address _owner, address _spender) constant returns (uint256 remaining);
    
        event Transfer(address indexed _from, address indexed _to, uint256 _value);
        event Approval(address indexed _owner, address indexed _spender, uint256 _value);
    }
    
    
    /// VEN token, ERC20 compliant
    contract VEN is Token, Owned {
        using SafeMath for uint256;
    
        string public constant name    = "VeChain Token";  //The Token's name
        uint8 public constant decimals = 18;               //Number of decimals of the smallest unit
        string public constant symbol  = "VEN";            //An identifier    
    
        // packed to 256bit to save gas usage.
        struct Supplies {
            // uint128's max value is about 3e38.
            // it's enough to present amount of tokens
            uint128 total;
            uint128 rawTokens;
        }
    
        Supplies supplies;
    
        // Packed to 256bit to save gas usage.    
        struct Account {
            // uint112's max value is about 5e33.
            // it's enough to present amount of tokens
            uint112 balance;
    
            // raw token can be transformed into balance with bonus        
            uint112 rawTokens;
    
            // safe to store timestamp
            uint32 lastMintedTimestamp;
        }
    
        // Balances for each account
        mapping(address => Account) accounts;
    
        // Owner of account approves the transfer of an amount to another account
        mapping(address => mapping(address => uint256)) allowed;
    
        // bonus that can be shared by raw tokens
        uint256 bonusOffered;
    
        // Constructor
        function VEN() {
        }
    
        function totalSupply() constant returns (uint256 supply){
            return supplies.total;
        }
    
        // Send back ether sent to me
        function () {
            revert();
        }
    
        // If sealed, transfer is enabled and mint is disabled
        function isSealed() constant returns (bool) {
            return owner == 0;
        }
    
        function lastMintedTimestamp(address _owner) constant returns(uint32) {
            return accounts[_owner].lastMintedTimestamp;
        }
    
        // Claim bonus by raw tokens
        function claimBonus(address _owner) internal{      
            require(isSealed());
            if (accounts[_owner].rawTokens != 0) {
                uint256 realBalance = balanceOf(_owner);
                uint256 bonus = realBalance
                    .sub(accounts[_owner].balance)
                    .sub(accounts[_owner].rawTokens);
    
                accounts[_owner].balance = realBalance.toUINT112();
                accounts[_owner].rawTokens = 0;
                if(bonus > 0){
                    Transfer(this, _owner, bonus);
                }
            }
        }
    
        // What is the balance of a particular account?
        function balanceOf(address _owner) constant returns (uint256 balance) {
            if (accounts[_owner].rawTokens == 0)
                return accounts[_owner].balance;
    
            if (bonusOffered > 0) {
                uint256 bonus = bonusOffered
                     .mul(accounts[_owner].rawTokens)
                     .div(supplies.rawTokens);
    
                return bonus.add(accounts[_owner].balance)
                        .add(accounts[_owner].rawTokens);
            }
            
            return uint256(accounts[_owner].balance)
                .add(accounts[_owner].rawTokens);
        }
    
        // Transfer the balance from owner's account to another account
        function transfer(address _to, uint256 _amount) returns (bool success) {
            require(isSealed());
    
            // implicitly claim bonus for both sender and receiver
            claimBonus(msg.sender);
            claimBonus(_to);
    
            // according to VEN's total supply, never overflow here
            if (accounts[msg.sender].balance >= _amount
                && _amount > 0) {            
                accounts[msg.sender].balance -= uint112(_amount);
                accounts[_to].balance = _amount.add(accounts[_to].balance).toUINT112();
                Transfer(msg.sender, _to, _amount);
                return true;
            } else {
                return false;
            }
        }
    
        // Send _value amount of tokens from address _from to address _to
        // The transferFrom method is used for a withdraw workflow, allowing contracts to send
        // tokens on your behalf, for example to "deposit" to a contract address and/or to charge
        // fees in sub-currencies; the command should fail unless the _from account has
        // deliberately authorized the sender of the message via some mechanism; we propose
        // these standardized APIs for approval:
        function transferFrom(
            address _from,
            address _to,
            uint256 _amount
        ) returns (bool success) {
            require(isSealed());
    
            // implicitly claim bonus for both sender and receiver
            claimBonus(_from);
            claimBonus(_to);
    
            // according to VEN's total supply, never overflow here
            if (accounts[_from].balance >= _amount
                && allowed[_from][msg.sender] >= _amount
                && _amount > 0) {
                accounts[_from].balance -= uint112(_amount);
                allowed[_from][msg.sender] -= _amount;
                accounts[_to].balance = _amount.add(accounts[_to].balance).toUINT112();
                Transfer(_from, _to, _amount);
                return true;
            } else {
                return false;
            }
        }
    
        // Allow _spender to withdraw from your account, multiple times, up to the _value amount.
        // If this function is called again it overwrites the current allowance with _value.
        function approve(address _spender, uint256 _amount) returns (bool success) {
            allowed[msg.sender][_spender] = _amount;
            Approval(msg.sender, _spender, _amount);
            return true;
        }
    
        /* Approves and then calls the receiving contract */
        function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
            allowed[msg.sender][_spender] = _value;
            Approval(msg.sender, _spender, _value);
    
            //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this.
            //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData)
            //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead.
            //if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { revert(); }
            ApprovalReceiver(_spender).receiveApproval(msg.sender, _value, this, _extraData);
            return true;
        }
    
        function allowance(address _owner, address _spender) constant returns (uint256 remaining) {
            return allowed[_owner][_spender];
        }
    
        // Mint tokens and assign to some one
        function mint(address _owner, uint256 _amount, bool _isRaw, uint32 timestamp) onlyOwner{
            if (_isRaw) {
                accounts[_owner].rawTokens = _amount.add(accounts[_owner].rawTokens).toUINT112();
                supplies.rawTokens = _amount.add(supplies.rawTokens).toUINT128();
            } else {
                accounts[_owner].balance = _amount.add(accounts[_owner].balance).toUINT112();
            }
    
            accounts[_owner].lastMintedTimestamp = timestamp;
    
            supplies.total = _amount.add(supplies.total).toUINT128();
            Transfer(0, _owner, _amount);
        }
        
        // Offer bonus to raw tokens holder
        function offerBonus(uint256 _bonus) onlyOwner { 
            bonusOffered = bonusOffered.add(_bonus);
            supplies.total = _bonus.add(supplies.total).toUINT128();
            Transfer(0, this, _bonus);
        }
    
        // Set owner to zero address, to disable mint, and enable token transfer
        function seal() onlyOwner {
            setOwner(0);
        }
    }
    
    contract ApprovalReceiver {
        function receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData);
    }
    
    
    // Contract to sell and distribute VEN tokens
    contract VENSale is Owned{
    
        /// chart of stage transition 
        ///
        /// deploy   initialize      startTime                            endTime                 finalize
        ///                              | <-earlyStageLasts-> |             | <- closedStageLasts -> |
        ///  O-----------O---------------O---------------------O-------------O------------------------O------------>
        ///     Created     Initialized           Early             Normal             Closed            Finalized
        enum Stage {
            NotCreated,
            Created,
            Initialized,
            Early,
            Normal,
            Closed,
            Finalized
        }
    
        using SafeMath for uint256;
        
        uint256 public constant totalSupply         = (10 ** 9) * (10 ** 18); // 1 billion VEN, decimals set to 18
    
        uint256 constant privateSupply              = totalSupply * 9 / 100;  // 9% for private ICO
        uint256 constant commercialPlan             = totalSupply * 23 / 100; // 23% for commercial plan
        uint256 constant reservedForTeam            = totalSupply * 5 / 100;  // 5% for team
        uint256 constant reservedForOperations      = totalSupply * 22 / 100; // 22 for operations
    
        // 59%
        uint256 public constant nonPublicSupply     = privateSupply + commercialPlan + reservedForTeam + reservedForOperations;
        // 41%
        uint256 public constant publicSupply = totalSupply - nonPublicSupply;
    
    
        uint256 public constant officialLimit = 64371825 * (10 ** 18);
        uint256 public constant channelsLimit = publicSupply - officialLimit;
    
        // packed to 256bit
        struct SoldOut {
            uint16 placeholder; // placeholder to make struct pre-alloced
    
            // amount of tokens officially sold out.
            // max value of 120bit is about 1e36, it's enough for token amount
            uint120 official; 
    
            uint120 channels; // amount of tokens sold out via channels
        }
    
        SoldOut soldOut;
        
        uint256 constant venPerEth = 3500;  // normal exchange rate
        uint256 constant venPerEthEarlyStage = venPerEth + venPerEth * 15 / 100;  // early stage has 15% reward
    
        uint constant minBuyInterval = 30 minutes; // each account can buy once in 30 minutes
        uint constant maxBuyEthAmount = 30 ether;
       
        VEN ven; // VEN token contract follows ERC20 standard
    
        address ethVault; // the account to keep received ether
        address venVault; // the account to keep non-public offered VEN tokens
    
        uint public constant startTime = 1503057600; // time to start sale
        uint public constant endTime = 1504180800;   // tiem to close sale
        uint public constant earlyStageLasts = 3 days; // early bird stage lasts in seconds
    
        bool initialized;
        bool finalized;
    
        function VENSale() {
            soldOut.placeholder = 1;
        }    
    
        /// @notice calculte exchange rate according to current stage
        /// @return exchange rate. zero if not in sale.
        function exchangeRate() constant returns (uint256){
            if (stage() == Stage.Early) {
                return venPerEthEarlyStage;
            }
            if (stage() == Stage.Normal) {
                return venPerEth;
            }
            return 0;
        }
    
        /// @notice for test purpose
        function blockTime() constant returns (uint32) {
            return uint32(block.timestamp);
        }
    
        /// @notice estimate stage
        /// @return current stage
        function stage() constant returns (Stage) { 
            if (finalized) {
                return Stage.Finalized;
            }
    
            if (!initialized) {
                // deployed but not initialized
                return Stage.Created;
            }
    
            if (blockTime() < startTime) {
                // not started yet
                return Stage.Initialized;
            }
    
            if (uint256(soldOut.official).add(soldOut.channels) >= publicSupply) {
                // all sold out
                return Stage.Closed;
            }
    
            if (blockTime() < endTime) {
                // in sale            
                if (blockTime() < startTime.add(earlyStageLasts)) {
                    // early bird stage
                    return Stage.Early;
                }
                // normal stage
                return Stage.Normal;
            }
    
            // closed
            return Stage.Closed;
        }
    
        function isContract(address _addr) constant internal returns(bool) {
            uint size;
            if (_addr == 0) return false;
            assembly {
                size := extcodesize(_addr)
            }
            return size > 0;
        }
    
        /// @notice entry to buy tokens
        function () payable {        
            buy();
        }
    
        /// @notice entry to buy tokens
        function buy() payable {
            // reject contract buyer to avoid breaking interval limit
            require(!isContract(msg.sender));
            require(msg.value >= 0.01 ether);
    
            uint256 rate = exchangeRate();
            // here don't need to check stage. rate is only valid when in sale
            require(rate > 0);
            // each account is allowed once in minBuyInterval
            require(blockTime() >= ven.lastMintedTimestamp(msg.sender) + minBuyInterval);
    
            uint256 requested;
            // and limited to maxBuyEthAmount
            if (msg.value > maxBuyEthAmount) {
                requested = maxBuyEthAmount.mul(rate);
            } else {
                requested = msg.value.mul(rate);
            }
    
            uint256 remained = officialLimit.sub(soldOut.official);
            if (requested > remained) {
                //exceed remained
                requested = remained;
            }
    
            uint256 ethCost = requested.div(rate);
            if (requested > 0) {
                ven.mint(msg.sender, requested, true, blockTime());
                // transfer ETH to vault
                ethVault.transfer(ethCost);
    
                soldOut.official = requested.add(soldOut.official).toUINT120();
                onSold(msg.sender, requested, ethCost);        
            }
    
            uint256 toReturn = msg.value.sub(ethCost);
            if(toReturn > 0) {
                // return over payed ETH
                msg.sender.transfer(toReturn);
            }        
        }
    
        /// @notice returns tokens sold officially
        function officialSold() constant returns (uint256) {
            return soldOut.official;
        }
    
        /// @notice returns tokens sold via channels
        function channelsSold() constant returns (uint256) {
            return soldOut.channels;
        } 
    
        /// @notice manually offer tokens to channel
        function offerToChannel(address _channelAccount, uint256 _venAmount) onlyOwner {
            Stage stg = stage();
            // since the settlement may be delayed, so it's allowed in closed stage
            require(stg == Stage.Early || stg == Stage.Normal || stg == Stage.Closed);
    
            soldOut.channels = _venAmount.add(soldOut.channels).toUINT120();
    
            //should not exceed limit
            require(soldOut.channels <= channelsLimit);
    
            ven.mint(
                _channelAccount,
                _venAmount,
                true,  // unsold tokens can be claimed by channels portion
                blockTime()
                );
    
            onSold(_channelAccount, _venAmount, 0);
        }
    
        /// @notice initialize to prepare for sale
        /// @param _ven The address VEN token contract following ERC20 standard
        /// @param _ethVault The place to store received ETH
        /// @param _venVault The place to store non-publicly supplied VEN tokens
        function initialize(
            VEN _ven,
            address _ethVault,
            address _venVault) onlyOwner {
            require(stage() == Stage.Created);
    
            // ownership of token contract should already be this
            require(_ven.owner() == address(this));
    
            require(address(_ethVault) != 0);
            require(address(_venVault) != 0);      
    
            ven = _ven;
            
            ethVault = _ethVault;
            venVault = _venVault;    
            
            ven.mint(
                venVault,
                reservedForTeam.add(reservedForOperations),
                false, // team and operations reserved portion can't share unsold tokens
                blockTime()
            );
    
            ven.mint(
                venVault,
                privateSupply.add(commercialPlan),
                true, // private ICO and commercial plan can share unsold tokens
                blockTime()
            );
    
            initialized = true;
            onInitialized();
        }
    
        /// @notice finalize
        function finalize() onlyOwner {
            // only after closed stage
            require(stage() == Stage.Closed);       
    
            uint256 unsold = publicSupply.sub(soldOut.official).sub(soldOut.channels);
    
            if (unsold > 0) {
                // unsold VEN as bonus
                ven.offerBonus(unsold);        
            }
            ven.seal();
    
            finalized = true;
            onFinalized();
        }
    
        event onInitialized();
        event onFinalized();
    
        event onSold(address indexed buyer, uint256 venAmount, uint256 ethCost);
    }
    

    参考

    相关文章

      网友评论

      • 3e25d510f9e0:我用mist钱包测试的,前面都没问题,就是最后safeWithdrawal() 运行了几次,都没动静。是版本问题吗
      • 123321en:你好, 看了例子受益良多, 非常感谢。
        请问怎么获取 合约内的 ColorBay TOKEN 数量。用this.balance 只能获取eth的数量 , 🙏
        笔名辉哥:@123321en 你函数用错了,address.balance只能获取以太坊的余额。
        接口增加ERC20代币的描述:
        interface token {
        function balanceOf(address who) public view returns (uint256);
        }
        众筹合约增加你的函数描述如下:
        function getBalance() public view returns (uint)
        {
        // return address(tokenReward).balance;

        return tokenReward.balanceOf(address(tokenReward));
        }
        经确认是可以获取余额的。

        欢迎加入知识星球 https://t.zsxq.com/ZFaaYVF 后进入技术讨论专项微信群,会更及时答复交流。
        123321en:@笔名辉哥 🙏我按照上面的例子,加了一个function

        function getBalance() public view returns (uint)
        {
        return address(tokenReward).balance;
        }

        我转了1000 REC20 token 给 当前合约,但是getBalance返回的结果是 0 ,请问是我写错了么?🙏
        笔名辉哥:@123321en 转换为colorbay的地址即可
      • bb765cea6cac:还有,欧阳哥哥改善的代码中的safeWithdrawal()函数里
        for(i = 0; i < funder.length; i++) {
        tokenReward.transfer(funder[i], balanceOf[funder[i]].mul(1 ether).div(price));
        balanceOf[funder[i]] = 0;
        }
        这段不太理解
        笔名辉哥:@酒酿元子 我们群里讨论
      • bb765cea6cac:你好,请问官方众筹合约中payable匿名函数里
        balanceOf[msg.sender] += amount;
        这句是什么意思?谢谢!
        bb765cea6cac:@笔名辉哥
        是指众筹参与方打款后收到的回馈代币增加吗?这个不是由下面一句tokenReward.transfer(msg.sender, 1000 * (amount / price));
        实现的吗?
        而且amount不是参与方的打款(ether/wei)数量么
        笔名辉哥:@酒酿元子 回馈收到的代币数量要加上来
      • 逍遥子_:/*一个TOKEN等同于1个以太坊ETH太贵了,修改官网代码,变为一个TOKEN等同于1个wei*/
        /*price = etherCostOfEachToken * 1 ether;*/
        price = weiCostOfEachToken * 1 wei;
        ----------------------------------------------------------------
        /*官网这个代码有问题,导致打回的币的数量会非常小,此处*1000倍,表示 1个ETH等于1000个TOKEN/ /*tokenReward.transfer(msg.sender, amount / price);*/ tokenReward.transfer(msg.sender, 1000 * (amount / price));
        --------分割线-----------
        你好,这地方好像不太对啊,我正在查,希望题主也能看看
      • 逍遥子_:目前好像所谓的智能合约自动执行,还是需要去主动触发它,做不到完全自动

      本文标题:第八课 如何调试以太坊官网的智能合约众筹案例

      本文链接:https://www.haomeiwen.com/subject/hrqzkftx.html