以太坊開發(fā)(十二)眾籌合約示例及講解

原文: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ā)散思維做其他很多智能合約,比如實名/匿名投票、淘寶交易合約等。

擴展閱讀

以太坊官方眾籌說明

ico眾籌合約代碼分析

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

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

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