詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

稀土君發表於2018-06-15

編者按:本文系 登鏈科技CTO 熊麗兵 講師,在由掘金技術社群主辦,以太坊社群基金會、以太坊愛好者與 ConsenSys 協辦的《開發者的以太坊入門指南 | Jeth 第一期 - 北京場》 活動上的分享整理。Jeth 圍繞以太坊技術開發主題的系列線下活動。每期 Jeth 會邀請以太坊開發領域的優秀技術團隊和工程師線上下分享技術乾貨。旨在為開發者提供線下技術交流互動機會,幫助開發者成長。

熊麗兵老師本次活動分享視訊回放(B站)

分享整理傳送門

智慧合約全棧介紹 - Howard | Jeth 第一期

以太坊智慧合約 + DApp 從入門到上線:來自前端工程師的實戰指南 - 王仕軍 | Jeth 第一期

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

熊麗兵老師目前在登鏈科技擔任 CTO,是全網訪問量最大的區塊鏈技術部落格《深入淺出區塊鏈》博主,對底層公鏈技術,區塊鏈技術落地都有深入研究。熊老師曾先後加入創新工場及獵豹移動,全面負責數款千萬級使用者開發及管理工作,2014年作為技術合夥人參與建立酷吧時代科技,2016年起重心投入區塊鏈技術領域。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

很高興參加掘金技術社群這次舉辦的《開發者的以太坊入門指南》活動,今天我帶來的分享主題是通過代幣和眾籌來介紹智慧合約的開發。我先做一下自我介紹,我叫熊麗兵,應該有一些人看過我的部落格《深入淺出區塊鏈》,我現在在登鏈科技擔任 CTO。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

我今天分享內容分為圖上的四個部分,我是最後一個做分享的講師,相信在場的觀眾堅持到現在有點疲憊,但是我的內容非常實用,有很多人拿我的程式碼已經籌了不少錢,或許我的程式碼對你也有幫助,希望大家能認真聽。

Token 代幣是什麼

  • 幣 → 錢
  • 代幣 → 可以替代錢

代幣,幣其實就是錢,代幣可以代替錢——這是我給代幣下的一個定義。從這個定義出發,不管是比特幣還是以太幣都算代幣。我們今天要講的這個代幣是基於以太坊的智慧合約開發出來的。代幣不單單是可以代替錢,還可以代替很多的東西,可以代替積分,也可以代替一本書或一首歌,以上的都可以用以太坊智慧合約來代替。

智慧合約

智慧合約就是以太坊上的程式,是程式碼和資料(狀態)的集合。 智慧合約跟人工智慧的“智慧”是沒有關係的,智慧合約並不智慧。智慧合約最早是尼克薩博提出來的一個概念,是指把法律條文程式化,這個理念和以太坊上的程式非常類似,因為法律條文的執行不應該受到任何人的干涉與干擾;以太坊的程式也是一樣的,只要你寫好程式碼以後沒有任何人可以干擾以太坊程式的執行。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

智慧合約有很多程式語言,最常見、被官方推薦的是 Solidity 語言,這個語言的字尾是 .sol 。上圖是一個簡單的 Hello World,其中 contract 對應著合約,我們可以把它理解為一個“類”,如果把 contract 變成 class,它就是定義一個“類”了,這跟我們寫別的語言定一個“類”有點相似。因此這個合約的名字就叫做 Hello World,作用是返回一個字串,這是一個最簡單的智慧合約。寫合約的時候不像其他語言的有main方法,Solidity 中是沒有的,每一個函式都是單獨要用的。

如何實現代幣

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

我們再來看看如何來實現一個代幣,實現一個代幣最根本的是要維護一個帳本。這裡有一個簡單的帳本,尾號122帳戶裡面有餘額100,尾號123賬戶有120,如果我們要執行從尾號122的賬戶轉賬10塊錢到尾號123的帳戶的時候,是不是從這個100裡面減掉10,從120裡面加上10。如果要實現這樣一個帳本應該怎麼做?大家想一想,我們是不是可以把帳戶這部分(賬號)當成 key,把餘額當成 value,這就是 ”鍵值對“,就是一個Map,或者叫字典。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

Solidity裡面有一個資料結構叫作 mapping ,我們可以用這個關鍵字 mapping 這樣的結構來儲存帳本資訊,這裡 Key 是一個地址或一個賬號。 value 表示餘額。 另外,我們要發幣的話要設定發行量。以及我們需要有一個轉賬函式,從一個地址轉到另外一個地址。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

我們看一下如何實現最簡單的代幣,我們來看一下這個代幣的程式碼,它只有15行。它定義了一個叫作My Token的合約,它用了mapping 鍵值對的資料結構,這個型別的鍵是address(地址型別),值是 uint 無符號型整數的。變數的名字叫作 balanceOf,balance 在這裡指代餘額。那我們這個合約裡面有兩個方法,一個叫做建構函式,另外一個是轉帳的方法。建構函式來初始化發行量,最初的時候所有的發行的貨幣都在 owner 手裡,我們通過msg.sender獲得當前建立合約的人是誰。剛開始發行的時候,所有的代幣都在 owner 自己手裡,這個就像央行發行貨幣的時候,剛開始貨幣都在央行手裡是一樣的。

然後另外一個方法就是transfer,有兩個引數:接受者的地址,以及傳送的金額。看看具體的實現,第一個用來判斷交易條件,即判斷他有沒有足夠的錢完成交易。第二步是做一個溢位的判斷,待會兒後面會有一個分析,就是目標這個帳戶加上這個餘額要大於他原來的帳戶。假如說目標帳戶的餘額接近這個儲存的上限,他加上一個值他可能會發生溢位,我們這裡要判斷是否會出現這種情況。

第三步和第四步就是做簡單的減法和加法,在原帳戶裡面減去或加上一個金額,綜上我們通過這十幾行的程式碼就實現了代幣。

ERC-20 標準

  • 什麼是 ERC-20
  • 標準包含哪些內容 名稱、發行量、統一函式名、事件名

我們接下來看一下 ERC-20,我們剛剛已經實現了這個代幣,為什麼還要有 ERC-20?如果錢包要支援代幣的轉帳也好,獲取代幣的名字也罷,都需要有統一的名字。ERC-20 其實是一個規範,大家可以點開上方 GitHub 連結中檢視規範的具體內容。 ERC-20 包含了名稱、發行量、統一的轉帳名稱、授權的函式名以及事件名。

pragma solidity 0.4.20;

contract ERC20Interface {
  string public name;
  string public symbol;
  uint8 public  decimals;
  uint public totalSupply;

  function transfer(address _to, uint256 _value) returns (bool success);
  function transferFrom(address _from, address _to, uint256 _value) returns (bool success);
  function approve(address _spender, uint256 _value) returns (bool success);
  function allowance(address _owner, address _spender) view returns (uint256 remaining);

  event Transfer(address indexed _from, address indexed _to, uint256 _value);
  event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}
複製程式碼

這個是ERC20的一個介面檔案,我們來具體看一下它有哪些內容:

name 是我們需要指定名字,比如說我們要發生一個掘金Token,它的名字叫做掘金Token。

symbol是 代幣的符號,如常見的 BTC、ETH 等。

decimal 是代幣最少交易的單位,它表示小數點的位數,如果最少可以交易0.1個代幣的話,小數點位數就是1;假如最少交易一個代幣,就沒有小數點,那這個值就是0。

totalSupply 是指總髮行量。

下面幾個方法是用來進行轉帳的: transfer 指代轉賬的目標地址,它會提供一個返回值是否轉賬成功。

transferFrom 由被委託人呼叫,由被委託人轉移授權人的貨幣,

approve 是把許可權委託給別人,括號裡寫的是被委託人的地址和被委託了多大的金額

allowance 可以返回被授權委託的金額,委託人可以查詢剩下的金額。

TransferApproval 可以監聽並記錄事件資訊,當事件發生時你可以得到通知。

實現ERC-20介面

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

接下來演示具體的 ERC-20 程式碼,因為ERC-20 Token的程式碼有點長,我們切換到remix看一下程式碼:

contract ERC20 is ERC20Interface {

    mapping (address => uint256) public balanceOf;
    mapping (address => mapping (address => uint256)) internal allowed;

    constructor() public {
        totalSupply = 1000;
        name = "JueJin Token";
        symbol = "JJT";
        decimals = 0;
        balanceOf[msg.sender] = totalSupply;
    }

  function balanceOf(address _owner) view returns (uint256 balance) {
      return balanceOf[_owner];
  }

  function transfer(address _to, uint _value) public returns (bool success) {
      require(_to != address(0));
      require(_value <= balanceOf[msg.sender]);
      require(balanceOf[_to] + _value >= balanceOf[_to]);

      balanceOf[msg.sender] -= _value;
      balanceOf[_to] += _value;
      emit Transfer(msg.sender, _to, _value);
      return true;
    }

    function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
      require(_to != address(0));
      require(_value <= balanceOf[_from]);
      require(_value <= allowed[_from][msg.sender]);
      require(balanceOf[_to] + _value >= balanceOf[_to]);

      balanceOf[_from] -= _value;
      balanceOf[_to] += _value;

      allowed[_from][msg.sender] -= _value;
      emit Transfer(_from, _to, _value);
      return true;
    }

  function approve(address _spender, uint256 _value) returns (bool success) {
      allowed[msg.sender][_spender] = _value;
      emit Approval(msg.sender, _spender, _value);
      return true;
  }

  function allowance(address _owner, address _spender) view returns (uint256 remaining) {
      return allowed[_owner][_spender];
  }

}

複製程式碼

這個就是標準的ERC-20的程式碼,它首先 import 了一個介面檔案,我們通過 is 這個方式實現這樣一個介面,就像在 Java 裡面 extends 一樣。在這個合約裡面, balanceOf 用來定義每一個地址所對應的餘額。allowed 中我們剛剛講到標準裡面有兩個方法,一個是 approve 授權,另一個是代理轉帳,進行這個過程的時候就需要判斷有沒有授權,我們用 allowed 去做這個事情,它的 key 是地址,儲存owner,value也是一個mapping,記錄被授權人及額度。

    constructor() public {
        totalSupply = 1000;
        name = "JueJin Token";
        symbol = "JJT";
        decimals = 0;
        balanceOf[msg.sender] = totalSupply;
    }

  function balanceOf(address _owner) view returns (uint256 balance) {
      return balanceOf[_owner];
  }

複製程式碼

建構函式很簡單,對我們剛剛指明的資訊,比如說名字、總髮行量、符號等,對狀態的變數做一些初始化。那比如說我這個代幣的名字就叫做掘金Token,balanceOf這個函式很簡單,它就是返回某一個帳號他有多少餘額。

function transfer(address _to, uint _value) public returns (bool success) {
      require(_to != address(0));
      require(_value <= balanceOf[msg.sender]);
      require(balanceOf[_to] + _value >= balanceOf[_to]);

      balanceOf[msg.sender] -= _value;
      balanceOf[_to] += _value;
      emit Transfer(msg.sender, _to, _value);
      return true;
    }
    
    function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
      require(_to != address(0));
      require(_value <= balanceOf[_from]);
      require(_value <= allowed[_from][msg.sender]);
      require(balanceOf[_to] + _value >= balanceOf[_to]);

      balanceOf[_from] -= _value;
      balanceOf[_to] += _value;

      allowed[_from][msg.sender] -= _value;
      emit Transfer(_from, _to, _value);
      return true;
    }
複製程式碼

transfer 和我們剛才說的方法差不多,只多了一步,他發出了這樣一個事件,這個也是ERC20他標準裡面需要實現的,我們在實現這樣一個轉帳的時候必須要把這個事件記錄下來。那 transferFrom 在轉出的時候不再是由轉出的人而是由 From 這個地址發出的,但是 transferFrom 這個方法我們需要做一個檢查,就是必須要有足夠授權的額度,當我們執行了之後需要減掉一定的授權額度,其他的地方都一樣。

  function approve(address _spender, uint256 _value) returns (bool success) {
      allowed[msg.sender][_spender] = _value;
      emit Approval(msg.sender, _spender, _value);
      return true;
  }
複製程式碼

我們再來看一下 approve 這段程式碼,approve 這個方法我可以授權給其他人來操作我帳號下的代幣,這個引數是被授權人的地址和授權的額度。這個函式的實現其實就是對於我們剛剛定義的一個狀態變數allowed進行一個賦值,同樣這個方法也需要去發出這樣的一個事件,即我授權給誰了。

 function allowance(address _owner, address _spender) view returns (uint256 remaining) {
      return allowed[_owner][_spender];
  }
複製程式碼

最後我們看看 allowance,它就是返回授權額度。

以上就是ERC-20代幣的標準實現,有50多行的程式碼,實現之後就是可以簡單的部署一下。

眾籌

我們接著講眾籌,這個是我給眾籌的一個定義,就是(約定時間內)向公眾募資(約定數量),ICO 意思是首次代幣發行,首次代幣發行的時候其實是向公眾募資以太幣,因此也是一個眾籌行為,最出名的專案就是 EOS ,他們在將近一年的時間裡募集了721萬個 ETH。

實現眾籌

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

那我們再來看一下如何來實現一個眾籌。

  1. 首先要設定眾籌的時間,你不能無限期的眾籌;然後設定一個目標的金額,還有兌換的價格,因為我們剛剛講ICO他其實是用 ETH 買我們自己的代幣,需要設定一個兌換的價格;此外就是受益人,當我們眾籌完成之後誰能夠來提取募集到的 ETH 。

  2. 實現一個以太和代幣的一個兌換,當我們的合約收到別人打過來的 ETH 之後,我們要給他轉對應的代幣給他,注意這樣的一個過程是被動觸發的。

  3. 實現提款/退款,眾籌目標完成後,受益人是可以提款的,把所有的 ETH 給提走;若眾籌沒有完成,應該允許退款,當然這一步不是所有人能夠做到的。

然後接著我們還是一樣,就是我們來看一下 ICO 的程式碼來了解如何實現眾籌。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

pragma solidity ^0.4.16;

interface token {
    function transfer(address receiver, uint amount) external ;
}

contract Ico {
    address public beneficiary;
    uint public fundingGoal;
    uint public amountRaised;
    
    uint public deadline;
    uint public price;
    token public tokenReward;
    
    mapping(address => uint256) public balanceOf;
    bool crowdsaleClosed = false;

    event GoalReached(address recipient, uint totalAmountRaised);
    event FundTransfer(address backer, uint amount, bool isContribution);


    constructor (
        uint fundingGoalInEthers,
        uint durationInMinutes,
        uint etherCostOfEachToken,
        address addressOfTokenUsedAsReward
    ) public {
        beneficiary = msg.sender;
        fundingGoal = fundingGoalInEthers * 1 ether;
        deadline = now + durationInMinutes * 1 minutes;
        price = etherCostOfEachToken * 1 ether;
        tokenReward = token(addressOfTokenUsedAsReward);
    }


    function () public payable {
        require(!crowdsaleClosed);
        
        uint amount = msg.value;  // wei
        balanceOf[msg.sender] += amount;
  
        amountRaised += amount;
        if (amount == 0) {
            tokenReward.transfer(msg.sender, amount / price);
        }
        
        

        emit FundTransfer(msg.sender, amount, true);
    }

    modifier afterDeadline() {
        if (now >= deadline) {
            _;
        }
    }


    function checkGoalReached() public afterDeadline {
        if (amountRaised >= fundingGoal) {
            emit GoalReached(beneficiary, amountRaised);
        }
        crowdsaleClosed = true;
    }


    function safeWithdrawal() public afterDeadline {
        
        if (amountRaised < fundingGoal) {
            uint amount = balanceOf[msg.sender];
            balanceOf[msg.sender] = 0;
            if (amount > 0) {
                msg.sender.transfer(amount);
                emit FundTransfer(msg.sender, amount, false);
            }
        }

        if (fundingGoal <= amountRaised && beneficiary == msg.sender) {
            beneficiary.transfer(amountRaised);
            emit FundTransfer(beneficiary, amountRaised, false);
        }
    }
}
複製程式碼

首先第一步是要設定相關的引數,這些引數是在構造的時候去做設計的,我們看看有哪些引數。

  • beneficiary 是受益人;
  • fundingGoal 是眾籌的目標;
  • amountRaised 表示當前眾籌的總額;
  • Deadline 是眾籌的截止日期;
  • price 是兌換價格
  • tokenReward 是所關聯的代幣,實際上我們需要用代幣的一個地址去給他關聯起來,待會兒我們看建構函式的時候就可以看到;
  • mapping(address → uint256) 是用來記錄每一個參與的眾籌人投入了多少 ETH。
  • bool crowdsaleClosed 判斷我們的眾籌是否已經關閉了;
  • event GoalReached 記錄眾籌完成的事件;
  • event FundTransfer 記錄轉換的事件;
constructor (
        uint fundingGoalInEthers,
        uint durationInMinutes, 
        uint etherCostOfEachToken,
        address addressOfTokenUsedAsReward
    ) public {
        beneficiary = msg.sender;
        fundingGoal = fundingGoalInEthers * 1 ether;
        deadline = now + durationInMinutes * 1 minutes;
        price = etherCostOfEachToken * 1 ether;
        tokenReward = token(addressOfTokenUsedAsReward);
    }
複製程式碼

建構函式就是我們需要在建立合約的時候知道的幾個引數。

  • uint fundingGoalInEthers 目標總額;
  • unit durationInMinutes 眾籌持續時間單位是分鐘,這個大家可以隨意去調的。
 function () public payable {
        require(!crowdsaleClosed);
        
        uint amount = msg.value;  // wei
        balanceOf[msg.sender] += amount;
  
        amountRaised += amount;
        if (amount == 0) {
            tokenReward.transfer(msg.sender, amount / price);
        }
複製程式碼

這個函式很奇怪,它沒有函式的名字,這個函式會在別人往這個合約打入 ETH 的時候他會被動觸發的。我們通過使用者打入的 ETH 的金額,去換算我們應該給他兌換多少代幣。 那首先我們用msg.value 來獲得打給使用者的以太幣的數量,記錄每一個人從過去到現在一共打了多少 ETH 。

此外我們還要有一個變數amountRaised不停地把募集到的金額記錄下來,給使用者打入對應的代幣。 msg.sender是打入以太幣的地址,tranfer這個方法正是我們剛剛寫ERC20實現的方法,我們要用這個方法去給使用者傳送我們的代幣。

function safeWithdrawal() public afterDeadline {
        
        if (amountRaised < fundingGoal) {
            uint amount = balanceOf[msg.sender];
            balanceOf[msg.sender] = 0;
            if (amount > 0) {
                msg.sender.transfer(amount);
                emit FundTransfer(msg.sender, amount, false);
            }
        } 
        
複製程式碼

無論是使用者退款還是受益人取ETH,我們都需要提款,但必須要在眾籌結束之後才可以提款,這就是afterDeadLine的用處。afterDeadLine就是一個函式修改器,有點像Python的裝飾器,它在函式執行的時候可先進行一些判斷,只有符合條件的情況下才會去執行這樣的一個函式。那這裡必須符合的條件,就是當前的時間必須是在DeadLine之後。 回頭來看safeWithdrawal的實現,我們先要判斷當前募集到的總額是否小於目標的金額,如果是小於目標的金額表示眾籌失敗,失敗的話所有參與眾籌的人都可以把錢提走。這裡面我們首先拿到使用者之前打入的以太幣數量,然後通過 API 提供的 transfer 方法,給某一個地址轉入對應的以太,通過這個方法可以把使用者之前發過來的以太打回去。

if (fundingGoal <= amountRaised && beneficiary == msg.sender) {
            beneficiary.transfer(amountRaised);
            emit FundTransfer(beneficiary, amountRaised, false);
        }
複製程式碼

這裡還有另外一個分支就是如果要募集到資金的總額他設定的目標,這樣受益人就可以把所有的以太提走。當然這裡面還有一個條件,呼叫這個方法的人必須是當前的受益者,這個應該很好理解,不是說所有人都可以提款,然後我們同樣是呼叫transfer 的方法把以太轉到受益人的地址名下,這樣就完成了一個ICO。總體上程式碼也不多,只有七八十行。

擴充套件功能

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期
我們接著來看一下 ERC-20 的擴充套件功能。

  • 空投大家也接觸過,空投是可以不需要打入任何的以太就可以獲得對應的Token,所以被很多的專案拿來做營銷和推廣專案。
        amountRaised += amount;
        if (amount == 0) {
            tokenReward.transfer(msg.sender, 10);
        } else {
            tokenReward.transfer(msg.sender, amount / price);
        }
複製程式碼

假如說我們要給每個賬戶空投10個幣,程式碼就可以這麼寫,學會了就可以發幣了。

  • 挖礦本質就是增發。增發很簡單,增發是我們剛剛講到的總供應量totalSupply,通過函式修改總供應量不就是增發了嗎,也就是平常說的挖礦了。
  • 鎖定,有一些專案他們怕別人砸盤,所以對代幣的轉移有一些限制,就是我不讓你轉,並有分時間段做一些限制,比方說參與眾籌之後的三個月內不能轉走。鎖定的本質就是在代幣轉帳的時候加入了一些控制條件。

常見漏洞分析

美鏈

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

美鏈前段時間炒得比較火,正是因為它這個溢位漏洞,我們在這個連結中可以瞭解發生漏洞時交易的情況。在這筆交易的Token Transfer 裡面我們可以看到有巨大數量的幣轉移到了兩個不同的地址。實際上美鏈共發行70億個代幣,轉移的幣遠大於發行量,這筆交易造成憑空增發了很多代幣,這個漏洞出來了之後所有的交易所都已經關閉了美鏈的交易,當時美鏈應該是60多億市值,然後因為這個漏洞基本上直接歸零。

Function: batchTransfer(address[] _receivers, uint256 _value)

MethodID: 0x83f12fec

[0] :0000000000000000000000000000000000000000000000000000000000000040
[1] :8000000000000000000000000000000000000000000000000000000000000000
[2] :0000000000000000000000000000000000000000000000000000000000000002
[3] :000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
[4] :0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7
複製程式碼

上方就是頁面 Input Data 欄中當時函式呼叫的情況。這裡用的是批量轉帳的方法,這邊傳入一個地址,給所有這些地址做轉移對的金額。這個攻擊的交易把 value 設計得非常巧妙:他是8後面接了63個0,因為 uint 最大儲存上限是256位,換算成16進位制剛好是64個字元。如果我們對這個8後面接了63個0的數乘了2,我們知道一個數乘一個2相當於向左移一位(16進位制8是二進位制1000),但是他只存了256位,溢位了之後就變成0。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

我們剛剛看到傳入的這個是8後面接了63個0,那這樣的話unit256 amount = unit256(cnt) * _value中的value 乘以地址的個數的時候乘以了2之後,剛好amount就是0,這就導致這行程式碼後面的所有檢測都會通過,他有一個判斷就是他原地址的餘額需要大於amount,那麼這裡溢位後amount是 0。

接下來我們來看下半部分程式碼中的條件,balance[msg.sender] = balances[msg.sender].sub(amount)轉出了這個人他減去了金額減去了0,但是剩下的這兩個傳入地址需要加上8後面加63個0這樣一個代幣的金額,這樣的話就對這兩個地址,就是相當於平空增加了這麼多代幣,這個就是他溢位的漏洞。這個其實溢位的漏洞是這個合約裡面比較常見的一個漏洞,其實解決方法很簡單,就是這裡應該去引入SafeMath去做加法,我們應該所有的算數運算方法都要用SafeMath避免去溢位這樣的一個漏洞。

EDU漏洞

我剛剛講這張合約的時候他有一部是可以授權給其他人轉帳,就是別人可以代表我去轉帳,那 EDU 漏洞會在這種情況下觸發,即在沒有經過授權的情況下別人就可以把錢轉走。

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

我想這個智慧合約的作者沒有理解transferFrom的意思,他忘了去用 allowed[_from][msg.sender] >= _value) 判斷轉帳的時候是否有足夠許可權。其實即使他沒有加這一句,如果他要是引入了我剛剛講的SafeMath也可以同樣避免這個問題。 每一次執行減法的時候,每個 mappping 都有為0的預設值。如果他要是引入了 SafeMath 的話,0減去一個值也會發生溢位。因為溢位有兩種情況,一種是向上溢位,一個是向下溢位。allowed[_from][msg.sender] 的值是無符號型的整形,他如果是0去減去一個值的話,按照道理值是負數,但是這裡uint不儲存負數,所以這個值減去之後會變成一個巨大的正整數,就發生了下溢位錯誤,但是程式依然沒有處理到。所以你無論你有多少代幣,別人都可以轉走。

延伸

  • 代幣(Token) 專案的基礎,一個可以交易的內容

  • 區塊鏈思維 無法篡改的雙刃劍 OpenZeppelin/SafeMath

最後是一個簡單的總結:代幣是一個區塊鏈專案的基礎,它不單單是我們看到交易的錢,還可以代替很多的交易內容。我們剛剛講到了一些漏洞,這就涉及到區塊鏈的思維,在我們平時開發的時候講究的是網際網路思維,即快速迭代和不斷試錯;但是區塊鏈不能這樣做,區塊鏈有一個無法修改的特點,這是把雙刃劍。你釋出之後沒有辦法輕易地修改,所以我們在釋出智慧合約的時候要非常謹慎,我們要經過完善的設計還有細緻的測試。 推薦一個解決辦法是使用OpenZeppelin ,它把編寫智慧合約最佳的實踐做成了一些庫,即輪子。我們在編寫智慧合約的時候儘量不要自己去造輪子,而是用他們的程式碼,因為他們的程式碼經過很多審查。比如OpenZeppelin 就提供了一些 SafeMath 能避免我們傳送溢位。

以上就是今天的分享,謝謝大家。下方是我的微信二維碼,歡迎交流!

詳解 ERC20 代幣及眾籌 - 熊麗兵 | Jeth 第一期

相關文章