实现一个真正可用的艾西欧(中)

上一篇已经把准备工作做好了,现在让我们直接进入代码。

在真正实现我们的艾西欧之前,先看下open-zeppelin已经提供的工具合约。我们使用MintableToken 来实现我们的Token(可以在zeppelin-solidity/contracts/token/目录中查看)。MintableToken实现了ERC20标准,它允许我们自由的控制token的发行量,可以看下MintableToken的关键代码:

function mint(address _to, uint256 _amount) onlyOwner canMint public returns (bool) {
    totalSupply = totalSupply.add(_amount);
    balances[_to] = balances[_to].add(_amount);
    Mint(_to, _amount);
    Transfer(address(0), _to, _amount);
    return true;
  }

合约的控制者可以通过mint方法给 以太坊地址发Token,同时增加token的发行量。

除了发布Token,还需要艾西欧的合约,open-zeppelin 也提供了工具类合约Crowdsale,这个合约主要是实现了用户购买token的方法。

function buyTokens(address beneficiary) public payable {
    require(beneficiary != address(0));
    require(validPurchase());

    uint256 weiAmount = msg.value;

    // calculate token amount to be created
    uint256 tokens = weiAmount.mul(rate);

    // update state
    weiRaised = weiRaised.add(weiAmount);

    token.mint(beneficiary, tokens);
    TokenPurchase(msg.sender, beneficiary, weiAmount, tokens);

    forwardFunds();
  }

可以看到这个方法主要是调用了token的mint方法来给转ETH的地方发放Token。当然这个合约还有其他的一些逻辑比如购买的时间要在开始时间和结束时间之内,转的ETH数量要大于0等等。

除了可以购买Token外,我们还需要限定Token最高不能超过一定数额的ETH,同时如果没有募集到足够的ETH的时候需要把募集的ETH退还给投资者,这两个需要要怎么实现呢? open-zeppelin 已经为我们实现好了,对应的合约是CappedCrowdsale和RefundableCrowdsale。

CappedCrowdsale 允许我们设置募集ETH的最大值,也就是上一篇文章中提到的硬顶。CappedCrowdsale 重写了Crowdsale 合约中的validPurchase方法,要求所募集的资金在最大值范围内。

function validPurchase() internal view returns (bool) {
    bool withinCap = weiRaised.add(msg.value) <= cap;
    return super.validPurchase() && withinCap;
  }

RefundableCrowdsale 要求我们的募集到的ETH必须达到一定的数额(也就是上一篇文章说的软顶),没达到则可以给投资者退款。

// if crowdsale is unsuccessful, investors can claim refunds here
  function claimRefund() public {
    require(isFinalized);
    require(!goalReached());

    vault.refund(msg.sender);
  }

如果艾西欧没有成功,投资者是可以重新获取他们的投入资金的。

Token 实现

首先让我们实现我们自己的Token,随便取个名字就叫WebCoin吧。

pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/token/MintableToken.sol';

contract WebCoin is MintableToken {
    string public name = "Web Token";
    string public symbol = "WT";
    uint8 public decimals = 18;
}

WebCoin 继承了 MintableToken。 在WebCoin中指定Token的名称,标识,和小数位。

艾西欧合约实现

从上面的分析我们知道 open-zeppelin 提供的合约模板已经提供了软顶、硬顶的实现。现在我们还缺预售以及预售打折,Token分配等一些问题。
直接上代码

pragma solidity ^0.4.18;

import './WebCoin.sol';
import 'zeppelin-solidity/contracts/crowdsale/CappedCrowdsale.sol';
import 'zeppelin-solidity/contracts/crowdsale/RefundableCrowdsale.sol';

contract WebCrowdsale is CappedCrowdsale, RefundableCrowdsale {

  // ico 阶段
  enum CrowdsaleStage { PreICO, ICO }
  CrowdsaleStage public stage = CrowdsaleStage.PreICO; // 默认是预售
  

  // Token 分配
  // =============================
  uint256 public maxTokens = 100000000000000000000; // 总共 100 个Token
  uint256 public tokensForEcosystem = 20000000000000000000; // 20个用于生态建设
  uint256 public tokensForTeam = 10000000000000000000; // 10个用于团队奖励
  uint256 public tokensForBounty = 10000000000000000000; // 10个用于激励池
  uint256 public totalTokensForSale = 60000000000000000000; // 60 个用来众筹
  uint256 public totalTokensForSaleDuringPreICO = 20000000000000000000; // 60个中的20个用来预售
  // ==============================

  // 预售总额
  uint256 public totalWeiRaisedDuringPreICO;


  // ETH 转出事件
  event EthTransferred(string text);
  // ETH 退款事件
  event EthRefunded(string text);


  // 构造函数
  function WebCrowdsale(uint256 _startTime, uint256 _endTime, uint256 _rate, address _wallet, uint256 _goal, uint256 _cap) CappedCrowdsale(_cap) FinalizableCrowdsale() RefundableCrowdsale(_goal) Crowdsale(_startTime, _endTime, _rate, _wallet) public {
      require(_goal <= _cap);
  }
  // =============

  // 发布Token
  function createTokenContract() internal returns (MintableToken) {
    return new WebCoin(); // 发布众筹合约的时候会自动发布token
  }
  
  // 众筹 阶段管理
  // =========================================================

  // 改变众筹阶段,有preIco 和 ico阶段
  function setCrowdsaleStage(uint value) public onlyOwner {

      CrowdsaleStage _stage;

      if (uint(CrowdsaleStage.PreICO) == value) {
        _stage = CrowdsaleStage.PreICO;
      } else if (uint(CrowdsaleStage.ICO) == value) {
        _stage = CrowdsaleStage.ICO;
      }

      stage = _stage;

      if (stage == CrowdsaleStage.PreICO) {
        setCurrentRate(5);
      } else if (stage == CrowdsaleStage.ICO) {
        setCurrentRate(2);
      }
  }

  // 改变兑换比例
  function setCurrentRate(uint256 _rate) private {
      rate = _rate;
  }


  // 购买token
  function () external payable {
      uint256 tokensThatWillBeMintedAfterPurchase = msg.value.mul(rate);
      if ((stage == CrowdsaleStage.PreICO) && (token.totalSupply() + tokensThatWillBeMintedAfterPurchase > totalTokensForSaleDuringPreICO)) {
        msg.sender.transfer(msg.value); // 购买的token超过了预售的总量,退回ETH
        EthRefunded("PreICO Limit Hit");
        return;
      }

      buyTokens(msg.sender);

      if (stage == CrowdsaleStage.PreICO) {
          totalWeiRaisedDuringPreICO = totalWeiRaisedDuringPreICO.add(msg.value); // 统计预售阶段筹集的ETH
      }
  }

  // 转移筹集的资金
  function forwardFunds() internal {
          // 预售阶段的资金转移到 设置的钱包中
      if (stage == CrowdsaleStage.PreICO) {
          wallet.transfer(msg.value);
          EthTransferred("forwarding funds to wallet");
      } else if (stage == CrowdsaleStage.ICO) {
          // 资金转移到退款金库中
          EthTransferred("forwarding funds to refundable vault");
          super.forwardFunds();
      }
  }
 

  // 结束众筹: 在结束之前如果还有剩余token没有被购买转移到生态建设账户中,同时给团队和激励池的账户发token
  function finish(address _teamFund, address _ecosystemFund, address _bountyFund) public onlyOwner {

      require(!isFinalized);
      uint256 alreadyMinted = token.totalSupply();
      require(alreadyMinted < maxTokens);

      uint256 unsoldTokens = totalTokensForSale - alreadyMinted;
      if (unsoldTokens > 0) {
        tokensForEcosystem = tokensForEcosystem + unsoldTokens;
      }

      token.mint(_teamFund,tokensForTeam);
      token.mint(_ecosystemFund,tokensForEcosystem);
      token.mint(_bountyFund,tokensForBounty);
      finalize();
  }
  // ===============================

  // 如果要上线移除这个方法
  // 用于测试 finish 方法
  function hasEnded() public view returns (bool) {
    return true;
  }
}

从代码中我们看到,艾西欧分为了PreICO和ICO两个阶段。其中PreICO阶段1ETH可以兑换5个WebCoin,ICO阶段1ETH可以兑换2个WebCoin。我们最多发行100个WebCoin。其中20个用于生态建设,10个用于团队激励,60个用来众筹,60个中的20个用来PreIco阶段售卖。

在PreIco阶段,募集到的ETH会直接转到指定的钱包中,Ico阶段募集的ETH会转到退款金库中,如果募集的资金达到要求则把ETH转到指定钱包,不然就给投资者退款。

最后需要调用finish()来结束本次艾西欧,finish方法会给用于团队,生态建设和激励的地址发token,同时如果还有没有卖完的token则会发放到生态建设的地址中。

其他的代码逻辑注释里面写的很清楚了,就不在多作介绍了。

测试

合约已经写完了,但是我们得保证合约可以正常执行,毕竟是跟钱相关的东西,没有完备的测试,心里会很虚的。在test/ 目录创建我们的测试用例TestCrowdsale.js。

var WebCrowdsale = artifacts.require("WebCrowdsale");
var WebCoin = artifacts.require("WebCoin");

contract('WebCrowdsale', function(accounts) {
    it('测试发布是否成功,同时token地址正常', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
            const token = await instance.token.call();
            assert(token, 'Token 地址异常');
            done();
       });
    });

    it('测试设置 PreICO 阶段', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
          await instance.setCrowdsaleStage(0);
          const stage = await instance.stage.call();
          assert.equal(stage.toNumber(), 0, '设置preIco阶段失败');
          done();
       });
    });

    it('1ETH可以兑换5个Token', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
            const data = await instance.sendTransaction({ from: accounts[7], value: web3.toWei(1, "ether")});
            const tokenAddress = await instance.token.call();
            const webCoin = WebCoin.at(tokenAddress);
            const tokenAmount = await webCoin.balanceOf(accounts[7]);
            assert.equal(tokenAmount.toNumber(), 5000000000000000000, '兑换失败');
            done();
       });
    });

    it('PreIco阶段募集的ETH 会直接转入指定地址', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
            let balanceOfBeneficiary = await web3.eth.getBalance(accounts[9]);
            balanceOfBeneficiary = Number(balanceOfBeneficiary.toString(10));

            await instance.sendTransaction({ from: accounts[1], value: web3.toWei(2, "ether")});

            let newBalanceOfBeneficiary = await web3.eth.getBalance(accounts[9]);
            newBalanceOfBeneficiary = Number(newBalanceOfBeneficiary.toString(10));

            assert.equal(newBalanceOfBeneficiary, balanceOfBeneficiary + 2000000000000000000, 'ETH 转出失败');
            done();
       });
    });

    it('PreIco募集的资金是否正常', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
            var amount = await instance.totalWeiRaisedDuringPreICO.call();
            assert.equal(amount.toNumber(), web3.toWei(3, "ether"), 'PreIco募集的资金计算异常');
            done();
       });
    });

    it('设置Ico阶段', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
          await instance.setCrowdsaleStage(1);
          const stage = await instance.stage.call();
          assert.equal(stage.toNumber(), 1, '设置Ico阶段异常');
          done();
       });
    });

    it('测试1ETH可以兑换2Token', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
            const data = await instance.sendTransaction({ from: accounts[2], value: web3.toWei(1.5, "ether")});
            const tokenAddress = await instance.token.call();
            const webCoin = WebCoin.at(tokenAddress);
            const tokenAmount = await webCoin.balanceOf(accounts[2]);
            assert.equal(tokenAmount.toNumber(), 3000000000000000000, '兑换失败');
            done();
       });
    });

    it('Ico募集的资金会转入退款金库', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
            var vaultAddress = await instance.vault.call();

            let balance = await web3.eth.getBalance(vaultAddress);

            assert.equal(balance.toNumber(), 1500000000000000000, 'ETH 未转入退款金库');
            done();
       });
    });

    it('Ico结束退款金库的余额需要转入指定地址', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
            let balanceOfBeneficiary = await web3.eth.getBalance(accounts[9]);
            balanceOfBeneficiary = balanceOfBeneficiary.toNumber();

            var vaultAddress = await instance.vault.call();
            let vaultBalance = await web3.eth.getBalance(vaultAddress);

            await instance.finish(accounts[0], accounts[1], accounts[2]);

            let newBalanceOfBeneficiary = await web3.eth.getBalance(accounts[9]);
            newBalanceOfBeneficiary = newBalanceOfBeneficiary.toNumber();

            assert.equal(newBalanceOfBeneficiary, balanceOfBeneficiary + vaultBalance.toNumber(), '退款金库转出余额失败');
            done();
       });
    });
});

上面测试用例测试艾西欧的几个阶段,当然还可以编写更多的测试用例来保证智能合约可以正常执行。

发布合约代码

在执行测试之前,我们必须编写合约的发布代码。在migrations目录创建2_WebCrowdsale.js 文件。

var WebCrowdsale = artifacts.require("./WebCrowdsale.sol");

module.exports = function(deployer) {
  const startTime = Math.round((new Date(Date.now() - 86400000).getTime())/1000); // 开始时间
  const endTime = Math.round((new Date().getTime() + (86400000 * 20))/1000); // 结束时间
  deployer.deploy(WebCrowdsale, 
    startTime, 
    endTime,
    5, 
    "0x5AEDA56215b167893e80B4fE645BA6d5Bab767DE", // 使用Ganache UI的最后一个账户地址(第十个)替换这个账户地址。这会是我们得到募集资金的账户地址
    2000000000000000000, // 2 ETH
    500000000000000000000 // 500 ETH
  );
};

truffle会执行这个js把设置好参数的WebCrowdsale智能合约发布到链上。

为了我们可以在本地测试,先找到zeppelin-solidity/contracts/crowdsale/Crowdsale.sol 文件第44行注释一下代码

require(_startTime >= now);

可以在正式上线的时候取消注释,现在为了可以直接在本地测试,先注释掉,不然合约的测试用例会失败,因为合约设置的 startTime < now

truffle本地配置文件

在执行测试之前,我们需要先在truffle.js 中配置本地运行的以太坊客户端host和port等信息。truffle.js 设置如下:

module.exports = {
  networks: {
    development: {
      host: "localhost",
      port: 7545,
      gas: 6500000,
      network_id: "5777"
    }
  },
  solc: {
     optimizer: {
       enabled: true,
       runs: 200
     }
  }
};

在上一篇文章中我们安装的Ganache客户端,运行后监控的端口就是7545,host 就是localhost。测试用例也会在Ganache上面执行。

测试

在命令行执行 truffle test。 truffle会自动把合约编译,发布到Ganache上,最后在执行测试用例。

如果一切正常,我们可以在命令行中看到测试用例执行成功。

本篇就先写到这了,下一篇会继续写如何把合约发布到Ropsten测试网上。

你可能感兴趣的:(实现一个真正可用的艾西欧(中))