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

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

在真正實現(xiàn)我們的艾西歐之前陨仅,先看下open-zeppelin已經(jīng)提供的工具合約壁袄。我們使用MintableToken 來實現(xiàn)我們的Token(可以在zeppelin-solidity/contracts/token/目錄中查看)司倚。MintableToken實現(xiàn)了ERC20標(biāo)準(zhǔn)豆混,它允許我們自由的控制token的發(fā)行量,可以看下MintableToken的關(guān)鍵代碼:

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();
  }

可以看到這個方法主要是調(diào)用了token的mint方法來給轉(zhuǎn)ETH的地方發(fā)放Token拆讯。當(dāng)然這個合約還有其他的一些邏輯比如購買的時間要在開始時間和結(jié)束時間之內(nèi),轉(zhuǎn)的ETH數(shù)量要大于0等等养叛。

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

CappedCrowdsale 允許我們設(shè)置募集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必須達(dá)到一定的數(shù)額(也就是上一篇文章說的軟頂)啸箫,沒達(dá)到則可以給投資者退款。

// 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的名稱召川,標(biāo)識,和小數(shù)位胸遇。

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

從上面的分析我們知道 open-zeppelin 提供的合約模板已經(jīng)提供了軟頂荧呐、硬頂?shù)膶崿F(xiàn)。現(xiàn)在我們還缺預(yù)售以及預(yù)售打折狐榔,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; // 默認(rèn)是預(yù)售
  

  // Token 分配
  // =============================
  uint256 public maxTokens = 100000000000000000000; // 總共 100 個Token
  uint256 public tokensForEcosystem = 20000000000000000000; // 20個用于生態(tài)建設(shè)
  uint256 public tokensForTeam = 10000000000000000000; // 10個用于團(tuán)隊獎勵
  uint256 public tokensForBounty = 10000000000000000000; // 10個用于激勵池
  uint256 public totalTokensForSale = 60000000000000000000; // 60 個用來眾籌
  uint256 public totalTokensForSaleDuringPreICO = 20000000000000000000; // 60個中的20個用來預(yù)售
  // ==============================

  // 預(yù)售總額
  uint256 public totalWeiRaisedDuringPreICO;


  // ETH 轉(zhuǎn)出事件
  event EthTransferred(string text);
  // ETH 退款事件
  event EthRefunded(string text);


  // 構(gòu)造函數(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超過了預(yù)售的總量薄腻,退回ETH
        EthRefunded("PreICO Limit Hit");
        return;
      }

      buyTokens(msg.sender);

      if (stage == CrowdsaleStage.PreICO) {
          totalWeiRaisedDuringPreICO = totalWeiRaisedDuringPreICO.add(msg.value); // 統(tǒng)計預(yù)售階段籌集的ETH
      }
  }

  // 轉(zhuǎn)移籌集的資金
  function forwardFunds() internal {
          // 預(yù)售階段的資金轉(zhuǎn)移到 設(shè)置的錢包中
      if (stage == CrowdsaleStage.PreICO) {
          wallet.transfer(msg.value);
          EthTransferred("forwarding funds to wallet");
      } else if (stage == CrowdsaleStage.ICO) {
          // 資金轉(zhuǎn)移到退款金庫中
          EthTransferred("forwarding funds to refundable vault");
          super.forwardFunds();
      }
  }
 

  // 結(jié)束眾籌: 在結(jié)束之前如果還有剩余token沒有被購買轉(zhuǎn)移到生態(tài)建設(shè)賬戶中收捣,同時給團(tuán)隊和激勵池的賬戶發(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)建設(shè)咐蚯,10個用于團(tuán)隊激勵,60個用來眾籌弄贿,60個中的20個用來PreIco階段售賣春锋。

在PreIco階段差凹,募集到的ETH會直接轉(zhuǎn)到指定的錢包中,Ico階段募集的ETH會轉(zhuǎn)到退款金庫中,如果募集的資金達(dá)到要求則把ETH轉(zhuǎn)到指定錢包,不然就給投資者退款顾腊。

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

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

測試

合約已經(jīng)寫完了,但是我們得保證合約可以正常執(zhí)行锨络,畢竟是跟錢相關(guān)的東西,沒有完備的測試掠归,心里會很虛的鸥鹉。在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('測試設(shè)置 PreICO 階段', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
          await instance.setCrowdsaleStage(0);
          const stage = await instance.stage.call();
          assert.equal(stage.toNumber(), 0, '設(shè)置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 會直接轉(zhuǎn)入指定地址', 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 轉(zhuǎn)出失敗');
            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('設(shè)置Ico階段', function(done){
        WebCrowdsale.deployed().then(async function(instance) {
          await instance.setCrowdsaleStage(1);
          const stage = await instance.stage.call();
          assert.equal(stage.toNumber(), 1, '設(shè)置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募集的資金會轉(zhuǎn)入退款金庫', 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 未轉(zhuǎn)入退款金庫');
            done();
       });
    });

    it('Ico結(jié)束退款金庫的余額需要轉(zhuǎn)入指定地址', 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(), '退款金庫轉(zhuǎn)出余額失敗');
            done();
       });
    });
});

上面測試用例測試艾西歐的幾個階段唉铜,當(dāng)然還可以編寫更多的測試用例來保證智能合約可以正常執(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); // 結(jié)束時間
  deployer.deploy(WebCrowdsale, 
    startTime, 
    endTime,
    5, 
    "0x5AEDA56215b167893e80B4fE645BA6d5Bab767DE", // 使用Ganache UI的最后一個賬戶地址(第十個)替換這個賬戶地址炎滞。這會是我們得到募集資金的賬戶地址
    2000000000000000000, // 2 ETH
    500000000000000000000 // 500 ETH
  );
};

truffle會執(zhí)行這個js把設(shè)置好參數(shù)的WebCrowdsale智能合約發(fā)布到鏈上敢艰。

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

require(_startTime >= now);

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

truffle本地配置文件

在執(zhí)行測試之前责掏,我們需要先在truffle.js 中配置本地運行的以太坊客戶端host和port等信息。truffle.js 設(shè)置如下:

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)上。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末氢架,一起剝皮案震驚了整個濱河市傻咖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌岖研,老刑警劉巖卿操,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異孙援,居然都是意外死亡害淤,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門拓售,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窥摄,“玉大人,你說我怎么就攤上這事础淤≌阜牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵值骇,是天一觀的道長莹菱。 經(jīng)常有香客問我移国,道長吱瘩,這世上最難降的妖魔是什么贷岸? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任诚欠,我火速辦了婚禮胁艰,結(jié)果婚禮上涩澡,老公的妹妹穿的比我還像新娘。我一直安慰自己票摇,他們只是感情好拘鞋,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著矢门,像睡著了一般盆色。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上祟剔,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天隔躲,我揣著相機(jī)與錄音,去河邊找鬼物延。 笑死宣旱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的叛薯。 我是一名探鬼主播浑吟,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼耗溜!你這毒婦竟也來了组力?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤抖拴,失蹤者是張志新(化名)和其女友劉穎忿项,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體城舞,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡轩触,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了家夺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脱柱。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖拉馋,靈堂內(nèi)的尸體忽然破棺而出榨为,到底是詐尸還是另有隱情,我是刑警寧澤煌茴,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布随闺,位于F島的核電站,受9級特大地震影響蔓腐,放射性物質(zhì)發(fā)生泄漏矩乐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望散罕。 院中可真熱鬧分歇,春花似錦、人聲如沸欧漱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽误甚。三九已至缚甩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窑邦,已是汗流浹背蹄胰。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留奕翔,地道東北人裕寨。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像派继,于是被迫代替她去往敵國和親宾袜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

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