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

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

在真正实现我们的艾西欧之前,先看下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测试网上。

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

推荐阅读更多精彩内容

  • 艾西欧在去年火遍了大江南北。各种艾西欧也是层出不穷,作为韭菜的我也随波逐流的参加了一些艾西欧,结果可想而知,现在已...
    RhainL阅读 1,273评论 1 2
  • 前面两篇文章,介绍了一个发布一个ERC20 Token,以及实现Token的流转问题。这次让我们来实现一个简单的艾...
    RhainL阅读 701评论 1 0
  • 故事是这样开始的!夜晚我独自一人坐在湖边想事情,皎洁的月亮照在平静的湖面,倒映出又圆又大的影子,就像银盘一样,我陷...
    我心安住阅读 115评论 0 0
  • 今天浪费了一天,很多事要做,却不知道怎么开始,然后成为了恶性循环
    PrajnaRen阅读 97评论 0 0
  • 作者:对儿 “什么是仪式感?” “就在此刻,你看到这个题目之后,突然想起一个人的时候。” 我自己其实是个很没有仪式...
    续事创意写作工作室阅读 276评论 0 1