實現(xiàn)一個真正可用的艾西歐(中)

上一篇已經(jīng)把準備工作做好了,現(xiàn)在讓我們直接進入代碼。

在真正實現(xiàn)我們的艾西歐之前,先看下open-zeppelin已經(jīng)提供的工具合約。我們使用MintableToken 來實現(xiàn)我們的Token(可以在zeppelin-solidity/contracts/token/目錄中查看)。MintableToken實現(xiàn)了ERC20標準,它允許我們自由的控制token的發(fā)行量,可以看下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方法給 以太坊地址發(fā)Token,同時增加token的發(fā)行量。

除了發(fā)布Token,還需要艾西歐的合約,open-zeppelin 也提供了工具類合約Crowdsale,這個合約主要是實現(xiàn)了用戶購買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的地方發(fā)放Token。當然這個合約還有其他的一些邏輯比如購買的時間要在開始時間和結束時間之內(nèi),轉的ETH數(shù)量要大于0等等。

除了可以購買Token外,我們還需要限定Token最高不能超過一定數(shù)額的ETH,同時如果沒有募集到足夠的ETH的時候需要把募集的ETH退還給投資者,這兩個需要要怎么實現(xiàn)呢? open-zeppelin 已經(jīng)為我們實現(xiàn)好了,對應的合約是CappedCrowdsale和RefundableCrowdsale。

CappedCrowdsale 允許我們設置募集ETH的最大值,也就是上一篇文章中提到的硬頂。CappedCrowdsale 重寫了Crowdsale 合約中的validPurchase方法,要求所募集的資金在最大值范圍內(nèi)。

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

RefundableCrowdsale 要求我們的募集到的ETH必須達到一定的數(shù)額(也就是上一篇文章說的軟頂),沒達到則可以給投資者退款。

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

    vault.refund(msg.sender);
  }

如果艾西歐沒有成功,投資者是可以重新獲取他們的投入資金的。

Token 實現(xiàn)

首先讓我們實現(xiàn)我們自己的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的名稱,標識,和小數(shù)位。

艾西歐合約實現(xiàn)

從上面的分析我們知道 open-zeppelin 提供的合約模板已經(jīng)提供了軟頂、硬頂?shù)膶崿F(xiàn)?,F(xiàn)在我們還缺預售以及預售打折,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個用于生態(tài)建設
  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);


  // 構造函數(shù)
  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);
  }
  // =============

  // 發(fā)布Token
  function createTokenContract() internal returns (MintableToken) {
    return new WebCoin(); // 發(fā)布眾籌合約的時候會自動發(fā)布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); // 統(tǒng)計預售階段籌集的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沒有被購買轉移到生態(tài)建設賬戶中,同時給團隊和激勵池的賬戶發(fā)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。我們最多發(fā)行100個WebCoin。其中20個用于生態(tài)建設,10個用于團隊激勵,60個用來眾籌,60個中的20個用來PreIco階段售賣。

在PreIco階段,募集到的ETH會直接轉到指定的錢包中,Ico階段募集的ETH會轉到退款金庫中,如果募集的資金達到要求則把ETH轉到指定錢包,不然就給投資者退款。

最后需要調用finish()來結束本次艾西歐,finish方法會給用于團隊,生態(tài)建設和激勵的地址發(fā)token,同時如果還有沒有賣完的token則會發(fā)放到生態(tài)建設的地址中。

其他的代碼邏輯注釋里面寫的很清楚了,就不在多作介紹了。

測試

合約已經(jīng)寫完了,但是我們得保證合約可以正常執(zhí)行,畢竟是跟錢相關的東西,沒有完備的測試,心里會很虛的。在test/ 目錄創(chuàng)建我們的測試用例TestCrowdsale.js。

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

contract('WebCrowdsale', function(accounts) {
    it('測試發(fā)布是否成功,同時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();
       });
    });
});

上面測試用例測試艾西歐的幾個階段,當然還可以編寫更多的測試用例來保證智能合約可以正常執(zhí)行。

發(fā)布合約代碼

在執(zhí)行測試之前,我們必須編寫合約的發(fā)布代碼。在migrations目錄創(chuàng)建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會執(zhí)行這個js把設置好參數(shù)的WebCrowdsale智能合約發(fā)布到鏈上。

為了我們可以在本地測試,先找到zeppelin-solidity/contracts/crowdsale/Crowdsale.sol 文件第44行注釋一下代碼

require(_startTime >= now);

可以在正式上線的時候取消注釋,現(xiàn)在為了可以直接在本地測試,先注釋掉,不然合約的測試用例會失敗,因為合約設置的 startTime < now。

truffle本地配置文件

在執(zhí)行測試之前,我們需要先在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客戶端,運行后監(jiān)控的端口就是7545,host 就是localhost。測試用例也會在Ganache上面執(zhí)行。

測試

在命令行執(zhí)行 truffle test。 truffle會自動把合約編譯,發(fā)布到Ganache上,最后在執(zhí)行測試用例。

如果一切正常,我們可以在命令行中看到測試用例執(zhí)行成功。

本篇就先寫到這了,下一篇會繼續(xù)寫如何把合約發(fā)布到Ropsten測試網(wǎng)上。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 艾西歐在去年火遍了大江南北。各種艾西歐也是層出不窮,作為韭菜的我也隨波逐流的參加了一些艾西歐,結果可想而知,現(xiàn)在已...
    RhainL閱讀 1,668評論 1 2
  • 前面兩篇文章,介紹了一個發(fā)布一個ERC20 Token,以及實現(xiàn)Token的流轉問題。這次讓我們來實現(xiàn)一個簡單的艾...
    RhainL閱讀 765評論 1 0
  • 故事是這樣開始的!夜晚我獨自一人坐在湖邊想事情,皎潔的月亮照在平靜的湖面,倒映出又圓又大的影子,就像銀盤一樣,我陷...
    我心安住閱讀 138評論 0 0
  • 今天浪費了一天,很多事要做,卻不知道怎么開始,然后成為了惡性循環(huán)
    PrajnaRen閱讀 123評論 0 0
  • 作者:對兒 “什么是儀式感?” “就在此刻,你看到這個題目之后,突然想起一個人的時候?!?我自己其實是個很沒有儀式...

友情鏈接更多精彩內(nèi)容