
原文:Go-Ethereum 1.7.2 結(jié)合 Mist 0.9.2 實現(xiàn)眾籌合約的實例
作者:迦壹
(注:本文是在原文的基礎(chǔ)上,根據(jù)個人的理解,修改部分內(nèi)容并添加了一些注釋)
什么是ICO
ICO是以初始產(chǎn)生的數(shù)字加密貨幣作為投資回報的一種籌措資金的方式,它的概念源自證券界的Initial Public Offering(IPO,首次公開發(fā)行)。
相較于傳統(tǒng)意義上的IPO,ICO具有可以縮短投融資鏈、降低投融資門檻、流動性佳、全球性投資等優(yōu)勢。常見的ICO里,數(shù)字貨幣和區(qū)塊鏈項目向早期愛好者出售項目代幣。項目團隊通過ICO獲取技術(shù)開發(fā)和市場拓展資金;而項目愛好者通過ICO支持項目,同時也可在對應代幣進入交易市場后選擇交易退出。
當你有一個好的想法,需要大家的資金來資助你。你可以使用眾籌合約來發(fā)起請求捐款。眾籌合約的基本思路是,你設(shè)定一個眾籌目標,在達到目標的最后期限時,如果沒有完成眾籌,所有的捐款將被退回,因此減少了捐贈者的風險。由于代碼是開放的,可被審計的,也就不需要一個集中的、可信的平臺來擔保,每個捐款的人,只需要支付一定的gas。
眾籌的獎勵-代幣
一般來說,那些籌集資金的人在資金籌集和資金管理不善之后,根本就不能說這筆錢是如何使用的,這常常導致項目根本無法交付任何東西。這時我們可以使用智能合適中投票的方式來做決定,這樣對所有人都是公平的。
在下面的例子里,我們在眾籌中,主要解決兩個重要的問題:如何管理和保存用于獎勵的代幣;籌集獎金后如何使用。
傳統(tǒng)的眾籌或獎勵記錄通常有一個中央數(shù)據(jù)庫,來保存、跟蹤所有捐助者的過程:誰錯過了眾籌的最后期限了,誰在眾籌過程中捐贈了多少等。與之相反,在區(qū)塊鏈中我們將以分散的方式來做這件事,只需創(chuàng)建一個標記來記錄眾籌的每一條記錄、獎勵了多少代幣,后面每個捐贈者都可以得到一個他們可以交易、出售或保留的代幣。如果要給予實物獎勵,生產(chǎn)者只需要交換實物產(chǎn)品的代幣。捐贈者也可以將代幣做為紀念品保留,不管這個眾籌項目有沒有達到它的目標,都可以收藏。
眾籌合約示例
設(shè)置眾籌合約中使用的代幣
下面是一段簡單的代幣代碼,用于發(fā)行給捐贈者,注意我們沒有設(shè)置代幣的總量,而是一直在增發(fā),在實際使用過程中,可以根據(jù)需求自行做限制:
pragma solidity 0.4.20;
/**
* 一個簡單的代幣合約。
*/
contract token {
string public standard = 'yuyangray';
string public name; //代幣名稱
string public symbol; //代幣符號比如'$'
//代幣單位,展示的小數(shù)點后面多少個0,和以太幣一樣后面是是18個0
uint8 public decimals = 2;
uint256 public totalSupply; //代幣總量
/* 這里每個地址對應的是代幣的數(shù)量,而不是捐贈的以太幣的數(shù)量 */
mapping (address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value); //轉(zhuǎn)帳通知事件
/* 初始化合約,并且把初始的所有代幣都給這合約的創(chuàng)建者
* @param _owned 合約的管理者
* @param tokenName 代幣名稱
* @param tokenSymbol 代幣符號
*/
function token(address _owned, string tokenName, string tokenSymbol) public {
//合約的管理者獲得的代幣總量
balanceOf[_owned] = totalSupply;
name = tokenName;
symbol = tokenSymbol;
}
/**
* 轉(zhuǎn)帳,具體可以根據(jù)自己的需求來實現(xiàn)
* @param _to address 接受代幣的地址
* @param _value uint256 接受代幣的數(shù)量
*/
function transfer(address _to, uint256 _value) public {
//從發(fā)送者減掉發(fā)送額
balanceOf[msg.sender] -= _value;
//給接收者加上相同的量
balanceOf[_to] += _value;
//通知任何監(jiān)聽該交易的客戶端
Transfer(msg.sender, _to, _value);
}
/**
* 增加代幣,并將代幣發(fā)送給捐贈新用戶
* @param _to address 接受代幣的地址
* @param _amount uint256 接受代幣的數(shù)量
*/
function issue(address _to, uint256 _amount) public {
totalSupply = totalSupply + _amount;
balanceOf[_to] += _amount;
//通知任何監(jiān)聽該交易的客戶端
Transfer(this, _to, _amount);
}
}
上面的代碼,在Mist中是執(zhí)行的效果如下:

眾籌合約的基本設(shè)置
在眾籌合約中,下面幾個變量可以用于設(shè)置眾籌以太幣總量、眾籌截止時間、以太幣和代幣的兌換比例,如果不使用單位進行聲明換算,默認在以太坊中,所有的單位都是wei,1 ether=10^18 wei:
fundingGoal = fundingGoalInEthers * 1 ether; //眾籌以太幣總量
deadline = now + durationInMinutes * 1 minutes; //眾籌截止時間,單位是分鐘
price = 500 finney; //1個以太幣可以買 2 個代幣
在初始化眾籌合約構(gòu)造函數(shù)的時候,我們會將眾籌合約的帳戶地址,傳遞給代幣做為管理地址,這里使用的是關(guān)鍵字this表示當前合約的地址,也可以傳遞給某個人,初始創(chuàng)建時獎勵給這個人指定量的代幣。
function Crowdsale(
uint fundingGoalInEthers,
uint durationInMinutes,
string tokenName,
string tokenSymbol
) token(this, tokenName, tokenSymbol)
讓眾籌合約接收以太幣
在 solidity中,有一個未命名函數(shù)當合約收到以太幣的時候會默認執(zhí)行。我們加上 payable 關(guān)鍵字,就可以給這個合約打款(存入以太幣),然后我們可以根據(jù)之前的以太幣與代幣的兌換比例,獎勵給某人一定量的代幣,并進行統(tǒng)計。
/**
* 默認函數(shù)
*
* 默認函數(shù),可以向合約直接打款
*/
function () payable {
//判斷是否關(guān)閉眾籌
require(!crowdsaleClosed);
uint amount = msg.value;
//捐款人的金額累加
balance[msg.sender] += amount;
//捐款總額累加
amountRaised += amount;
//轉(zhuǎn)帳操作,轉(zhuǎn)多少代幣給捐款人
issue(msg.sender, amount / price * 10 ** uint256(decimals));
FundTransfer(msg.sender, amount, true);
}
上面的代碼中如果眾籌合約關(guān)閉(crowdsaleClosed = true),則不繼續(xù)執(zhí)行,無法交易,這樣做的原因是,如果眾籌合約無論是成功或是已經(jīng)結(jié)束,避免其他人賠錢打款,造成后期的不必要糾紛。

檢測眾籌合約是否完成
檢測眾籌合約是否達到的代碼如下,如果已籌集的資金額,達到了眾籌的目標值,則結(jié)束眾籌,并通知客戶端做記錄
/**
* 檢測眾籌目標是否已經(jīng)達到
*/
function checkGoalReached() afterDeadline {
if (amountRaised >= fundingGoal){
//達成眾籌目標
fundingGoalReached = true;
GoalReached(beneficiary, amountRaised);
}
//關(guān)閉眾籌
crowdsaleClosed = true;
}
眾籌結(jié)束后的操作
同時我們還提供了另一個方法,當眾籌失敗的時候,捐贈者可以取回自己的捐助,如果眾籌成功,受益人可以獲得眾籌到的以太幣。
/**
* 收回資金
*
* 檢查是否達到了目標或時間限制,如果有,并且達到了資金目標,
* 將全部金額發(fā)送給受益人。如果沒有達到目標,每個貢獻者都可以退出
* 他們貢獻的金額
*/
function safeWithdrawal() afterDeadline {
//如果沒有達成眾籌目標
if (!fundingGoalReached) {
//獲取合約調(diào)用者已捐款余額
uint amount = balance[msg.sender];
if (amount > 0) {
//返回合約發(fā)起者所有余額
msg.sender.transfer(amount);
FundTransfer(msg.sender, amount, false);
balance[msg.sender] = 0;
}
}
//如果達成眾籌目標,并且合約調(diào)用者是受益人
if (fundingGoalReached && beneficiary == msg.sender) {
//將所有捐款從合約中給受益人
beneficiary.transfer(amountRaised);
FundTransfer(beneficiary, amount, false);
}
}
示例全代碼
pragma solidity 0.4.20;
/**
* 一個簡單的代幣合約。
*/
contract token {
string public standard = 'yuyangray';
string public name; //代幣名稱
string public symbol; //代幣符號比如'$'
//代幣單位,展示的小數(shù)點后面多少個0,和以太幣一樣后面是是18個0
uint8 public decimals = 2;
uint256 public totalSupply; //代幣總量
/* 這里每個地址對應的是代幣的數(shù)量,而不是捐贈的以太幣的數(shù)量 */
mapping (address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value); //轉(zhuǎn)帳通知事件
/* 初始化合約,并且把初始的所有代幣都給這合約的創(chuàng)建者
* @param _owned 合約的管理者
* @param tokenName 代幣名稱
* @param tokenSymbol 代幣符號
*/
function token(address _owned, string tokenName, string tokenSymbol) public {
//合約的管理者獲得的代幣總量
balanceOf[_owned] = totalSupply;
name = tokenName;
symbol = tokenSymbol;
}
/**
* 轉(zhuǎn)帳,具體可以根據(jù)自己的需求來實現(xiàn)
* @param _to address 接受代幣的地址
* @param _value uint256 接受代幣的數(shù)量
*/
function transfer(address _to, uint256 _value) public {
//從發(fā)送者減掉發(fā)送額
balanceOf[msg.sender] -= _value;
//給接收者加上相同的量
balanceOf[_to] += _value;
//通知任何監(jiān)聽該交易的客戶端
Transfer(msg.sender, _to, _value);
}
/**
* 增加代幣,并將代幣發(fā)送給捐贈新用戶
* @param _to address 接受代幣的地址
* @param _amount uint256 接受代幣的數(shù)量
*/
function issue(address _to, uint256 _amount) public {
totalSupply = totalSupply + _amount;
balanceOf[_to] += _amount;
//通知任何監(jiān)聽該交易的客戶端
Transfer(this, _to, _amount);
}
}
/**
* 眾籌合約
*/
contract Crowdsale is token {
address public beneficiary = msg.sender; //受益人地址,測試時為合約創(chuàng)建者
uint public fundingGoal; //眾籌目標,單位是ether
uint public amountRaised; //已籌集金額數(shù)量, 單位是ether
uint public deadline; //截止時間
uint public price; //代幣價格
bool public fundingGoalReached = false; //達成眾籌目標
bool public crowdsaleClosed = false; //眾籌關(guān)閉
mapping(address => uint256) public balance; //保存眾籌地址及對應的以太幣數(shù)量
// 受益人將眾籌金額轉(zhuǎn)走的通知
event GoalReached(address _beneficiary, uint _amountRaised);
// 用來記錄眾籌資金變動的通知,_isContribution表示是否是捐贈,因為有可能是捐贈者退出或發(fā)起者轉(zhuǎn)移眾籌資金
event FundTransfer(address _backer, uint _amount, bool _isContribution);
/**
* 初始化構(gòu)造函數(shù)
*
* @param fundingGoalInEthers 眾籌以太幣總量
* @param durationInMinutes 眾籌截止,單位是分鐘
* @param tokenName 代幣名稱
* @param tokenSymbol 代幣符號
*/
function Crowdsale(
uint fundingGoalInEthers,
uint durationInMinutes,
string tokenName,
string tokenSymbol
) token(this, tokenName, tokenSymbol) public {
fundingGoal = fundingGoalInEthers * 1 ether;
deadline = now + durationInMinutes * 1 minutes;
price = 500 finney; //1個以太幣可以買 2 個代幣
}
/**
* 默認函數(shù)
*
* 默認函數(shù),可以向合約直接打款
*/
function () payable public {
//判斷是否關(guān)閉眾籌
require(!crowdsaleClosed);
uint amount = msg.value;
//捐款人的金額累加
balance[msg.sender] += amount;
//捐款總額累加
amountRaised += amount;
//轉(zhuǎn)帳操作,轉(zhuǎn)多少代幣給捐款人
issue(msg.sender, amount / price * 10 ** uint256(decimals));
FundTransfer(msg.sender, amount, true);
}
/**
* 判斷是否已經(jīng)過了眾籌截止限期
*/
modifier afterDeadline() { if (now >= deadline) _; }
/**
* 檢測眾籌目標是否已經(jīng)達到
*/
function checkGoalReached() afterDeadline public {
if (amountRaised >= fundingGoal){
//達成眾籌目標
fundingGoalReached = true;
GoalReached(beneficiary, amountRaised);
}
//關(guān)閉眾籌
crowdsaleClosed = true;
}
/**
* 收回資金
*
* 檢查是否達到了目標或時間限制,如果有,并且達到了資金目標,
* 將全部金額發(fā)送給受益人。如果沒有達到目標,每個貢獻者都可以退出
* 他們貢獻的金額
* 注:這里代碼應該是限制了眾籌時間結(jié)束且眾籌目標沒有達成的情況下才允許退出。如果去掉限制條件afterDeadline,應該是可以允許眾籌時間還未到且眾籌目標沒有達成的情況下退出
*/
function safeWithdrawal() afterDeadline public {
//如果沒有達成眾籌目標
if (!fundingGoalReached) {
//獲取合約調(diào)用者已捐款余額
uint amount = balance[msg.sender];
if (amount > 0) {
//返回合約發(fā)起者所有余額
msg.sender.transfer(amount);
FundTransfer(msg.sender, amount, false);
balance[msg.sender] = 0;
}
}
//如果達成眾籌目標,并且合約調(diào)用者是受益人
if (fundingGoalReached && beneficiary == msg.sender) {
//將所有捐款從合約中給受益人
beneficiary.transfer(amountRaised);
FundTransfer(beneficiary, amount, false);
}
}
}
issue(msg.sender, amount / price * 10 ** uint256(decimals));
注:這里發(fā)現(xiàn)一個問題,原本以為換算方法為 amount(單位為以太幣) / price(單價 500 finney,單位為finney), 然后乘以10的decimals次方,以此來換算代幣,即為 70 / 500 * 100 = 14,這樣始終和作者最后在Mist上測試是不同,作者測試時輸入70以太幣會得到140代幣。
仔細查看作者注釋發(fā)現(xiàn)decimals只是表示小數(shù)點后有多少位。那么假設(shè)購買70以太幣,單價為500finney,decimals是2,代幣金額應該是 70 / 500 = 0.14,這顯然不對。
我現(xiàn)在理解的是傳入70ether,語法默認轉(zhuǎn)為70000finney,因為1ether = 1000finney,然后70000 / 500 = 140,加上decimals為2,最后代幣金額為140.00。語法真有這么智能的轉(zhuǎn)換?而且在運算中指定小數(shù)位怎么看也覺得很奇怪。暫時只能這樣理解了,如果有大神知道原理,請告知一下謝謝!
使用眾籌合約
創(chuàng)建四個帳號,分別是為 主帳號 、 張三 、 李四 、 王五,然后使用已經(jīng)通過 挖礦 得到以太幣的 主帳號 分別給 張三、 李四、 王五每人 100以太幣。

打開 Mist,連接上 geth 以后,點擊 合約 -> 部署合約 ,選擇 張三 來創(chuàng)建這個合約,將代碼貼入,然后右側(cè)選擇 Crowdsale,Funding goal in ethers 眾籌以太幣的總數(shù)我們設(shè)置為 100,Duration in minutes 眾籌截止時間,我們設(shè)置為 20 分鐘,Token name 代幣的名稱設(shè)置為 陌上花開,Token Symbol 代幣符號設(shè)置為 $。

合約創(chuàng)建成功以后,點擊右上方的 合約,然后點擊 陌上花開 眾籌合約,進行詳情頁,點擊右側(cè)的 存入以太幣,我們分別使用 李四 購買 70 ether 的代幣、王五 購買 50 ether 的代幣.

購買成功以后,可以看到,在 錢包 頁面 李四、王五 和帳戶上都多了兩個圖標,說明他們購買的代幣已經(jīng)到帳,并且相關(guān)的余額也發(fā)生了變化。而 張三 的比特幣是 99,98 ether,因為只要執(zhí)行合約就需要花費一些 gas。

之前我們設(shè)置的代幣小數(shù)后面 2個0,而1個以太幣可以購買2個代幣,看下圖 李四、王五 兩個人購買的數(shù)量上是對的。

在 眾籌合約截止時間以后,我們調(diào)用 checkGoalReached 方法,來判斷眾籌目標是否達成。如果嚴謹一些應該只允許管理員來操作這個方法,因為是測試,寫的隨意一些,任何人都可以調(diào)用。調(diào)用成功后,在眾籌合約的詳情頁,F(xiàn)unding goal reached 和 Crowdsale closed 的值都為Yes。

如果眾籌失敗或者是在眾籌過程中,捐贈人反悔了,想要收回捐贈的以太幣,可以調(diào)用 safeWithdrawal 方法,取回之前捐贈的以太幣。如果眾籌成功后,是無法取回的。
注:這里代碼應該是限制了眾籌時間結(jié)束且眾籌目標沒有達成的情況下才允許退出。如果去掉限制條件afterDeadline,應該是可以允許眾籌時間還未到且眾籌目標沒有達成的情況下退出
張三 的比特幣是99,98 ether,在眾籌結(jié)束后。調(diào)用 safeWithdrawal 方法,會將眾籌合約中的所有比特幣,轉(zhuǎn)到 張三 的帳戶地址下。

一個簡單的眾籌合約做完了。通過這個例子,可以發(fā)散思維做其他很多智能合約,比如實名/匿名投票、淘寶交易合約等。