上一篇已經(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)上。