1. 程式人生 > >一文讀懂以太坊代幣合約

一文讀懂以太坊代幣合約

規則 sta ini class 2015年 交易 存在 部分 生活

本文首發自 https://www.secpulse.com/archives/73696.html ,轉載請註明出處。

工欲善其事,必先利其器。要想挖掘和分析智能合約的漏洞,你必須要先學會看懂智能合約。而目前智能合約中有很大一部分是發行代幣的,那什麽是代幣,他們有什麽標準呢?本文就是帶領你入門,教會你看懂一個代幣的智能合約。

以太坊代幣

在以太坊系統中,存在作為基礎貨幣的 Ether(以太),以及同樣可以作為貨幣使用的 Token(代幣)。

以太坊與其他加密貨幣的主要不同在於,以太坊不是單純的貨幣,而是一個環境/平臺。在這個平臺上,任何人都可以利用區塊鏈的技術,通過智能合約來構建自己的項目和DAPPS(去中心化應用)。

如果把以太坊理解成互聯網,DAPPS則是在上面運行的網頁。DAPPS是去中心化的,意味著它不屬於某個人,而是屬於一群人。DAPPS發布的方式通常是采用被稱為 ICO 的眾籌方式。簡單來說,你需要用你的以太來購買相應DAPP的一些tokens。

一般有兩種Token:

  1. Usage Tokens: 就是對應 DAPP 的原生貨幣。Golem 就是一個很好的例子,如果你需要使用 Golem 的服務,你就需要為其支付 Golem Network Token(GNT)。由於這種 Tokens 有貨幣價值,所以通常不會有其他的權益。
  2. Work Tokens: 此類 Tokens 可以標識你對於 DAPP 的某種股東權益。以 DAO tokens 為例,如果你擁有 DAO tokens,那麽你有權就DAO是否資助某款 DAPP 來進行投票。

  類比到股權,可以把 Usage Tokens 簡單理解為普通流通股,可以與真實貨幣兌換,本身具有價值。而 Work Token,則大致相當於投票權。

為何需要Token:

不是有以太基礎貨幣了,那為什麽還需要 token 呢?可以想下現實生活的真實場景,在遊樂場裏,我們需要用現金兌換代幣,然後用代幣支付各種服務。 類比到以太坊,現金就是以太,代幣就是 token,用 token 來執行合約中的各項功能。

以太坊Token標準

這個是本文學習的重點,所有遵循 ERC20 標準的函數,都要事先它定義的標準接口。搞懂這些,你也就能很快看懂一些智能合約代幣的邏輯。

ERC-20 標準是在2015年11月份推出的,使用這種規則的代幣,表現出一種通用的和可預測的方式。任何 ERC-20 代幣都能立即兼容以太坊錢包(幾乎所有支持以太幣的錢包,包括Jaxx、MEW、imToken等),由於交易所已經知道這些代幣是如何操作的,它們可以很容易地整合這些代幣。這就意味著,在很多情況下,這些代幣都是可以立即進行交易的。簡單理解就是,ERC20是開發者在自己的tokens中必須采用的一套具體的公式/方法,從而確保不同DAPP的token與ERC20標準兼容。

ERC-20 標準規定了各個代幣的基本功能,非常方便第三方使用,在開發人員的編程下,5 分鐘就可以發行一個 ERC-20 代幣。因為它可以快速發幣,而且使用又方便,因此空投幣和空氣幣基本上就是利用 ERC-20 標準開發的。基於 ERC-20 標準開發的同種代幣價值都是相同的,它們可以進行互換。ERC-20 代幣就類似於人民幣,你的 100 元和我的 100 元是沒有區別的,價值都是 100 元,並且這兩張 100 元可以進行互換。有了這套標準,相當於全世界都使用人民幣,而不用去別的國家還要計算匯率換成別的貨幣。想象下,每個Dapp都有不同格式的幣,那對於這些應用的交互簡直是種災難。

etherscan上開源的 ERC20 標準的智能合約:https://etherscan.io/tokens

ERC20 Token標準接口:

contract ERC20 {
    uint256 public totalSupply;

    function balanceOf(address who) constant public returns (uint256);

    function transfer(address to, uint256 value) public returns (bool);

    function allowance(address owner, address spender) constant public returns (uint256);

    function transferFrom(address from, address to, uint256 value) public returns (bool);

    function approve(address spender, uint256 value) public returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);

    event Approval(address indexed owner, address indexed spender, uint256 value);
}

函數:

註意:非常重要的一點是調用者應該處理函數返回的錯誤,而不是假設錯誤永遠不會發生。

  1. totalSupply: 返回token的總供應量
  2. balanceOf: 用於查詢某個賬戶的賬戶余額
  3. tansfer: 發送 _value 個 token 到地址 _to
  4. transferFrom: 從地址 _from 發送 _value 個 token 到地址 _to
  5. approve: 允許 _spender 多次取回您的帳戶,最高達 _value 金額; 如果再次調用此函數,它將用 _value 的當前值覆蓋的 allowance 值。
  6. allowance: 返回 _spender 仍然被允許從 _owner 提取的金額。

事件 :

  1. event Transfer: 當 tokens 被轉移時觸發。
  2. event Approval: 當任何成功調用 approve(address _spender, uint256 _value) 後,必須被觸發。

代幣合約實例分析

talk is cheap show me the code 。前面給的函數說明是簡單的概括,大家可能還似懂非懂,下面就將用實例說明。《AMR智能合約漏洞分析》這篇文章用實例講解了智能合約的一種漏洞,合約代碼在 https://etherscan.io/address/0x96c833e43488c986676e9f6b3b8781812629bbb5#code ,我們就以這個代碼做個詳細的分析。

開始一行行分析這個智能合約:

library SafeMath {
    function mul(uint256 a, uint256 b) internal pure returns (uint256){
        uint256 c = a * b;
        assert(a == 0 || c / a == b);
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256){
        assert(b > 0);
        uint256 c = a / b;
        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256){
        assert(b <= a);
        return a - b;
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256){
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
} 

這個比較簡單,定義安全函數的庫,用來防止整數溢出漏洞。

contract ERC20 {
    uint256 public totalSupply;

    function balanceOf(address who) constant public returns (uint256);

    function transfer(address to, uint256 value) public returns (bool);

    function allowance(address owner, address spender) constant public returns (uint256);

    function transferFrom(address from, address to, uint256 value) public returns (bool);

    function approve(address spender, uint256 value) public returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);

    event Approval(address indexed owner, address indexed spender, uint256 value);
}

ERC20 標準接口,上一節說過了。

contract Ownable {
    address  owner;

    // 把當前合約的調用者賦值給owner
    function Ownable() public{
        owner = msg.sender;
    }

    // 只有智能合約的所有者才能調用的方法
    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }

    // 合約的所有者可以把權限轉移給其他用戶
    function transferOwnership(address newOwner) onlyOwner public{
        require(newOwner != address(0));
        owner = newOwner;
    }
}

這個合約接口的功能是判斷和修改該合約的所有者。其中函數 onlyOwner 用到了 modifiers(函數修改器) 關鍵字。函數修改器可以用來改變一個函數的行為,比如用於在函數執行前檢查某種前置條件。如果你了解 python 的裝飾器,這個就很容易理解了。還不理解?沒關系,我們再詳細說明下這個接口。首先你需要理解下這邊的幾個概念:

1. msg.sender 內置變量,代表當前調用該合約的賬戶地址。

2. Ownable() 函數,和合約接口同名,這是個構造函數,只能在創建合約期間運行,不能在事後調用。所以這個owner是創建該合約人的地址,無法被篡改,除非合約創始人授權。

3. 特殊字符串 _; 用來替換使用修改符的函數體。比如上述代碼就是把 _; 替換成 transferOwnership ,也就是執行 transferOwnership 函數時候會先判斷 require(msg.sender == owner);

下面把這個合約加一個打印owner的函數,然後放到 remix 調試,這樣更直觀理解。

pragma solidity ^0.4.24;

contract Ownable {
    address  owner;

    // 把當前合約的調用者賦值給owner
    function Ownable() public{
        owner = msg.sender;
    }
    
    function CurrentOwner() public returns (address){
        return owner;
    }

    // 只有智能合約的所有者才能調用的方法
    modifier onlyOwner(){
        require(msg.sender == owner);
        _;
    }

    // 合約的所有者可以把權限轉移給其他用戶
    function transferOwnership(address newOwner) onlyOwner public{
        require(newOwner != address(0));
        owner = newOwner;
    }
}

a) 使用賬戶 A 創建合約,則 owner 則是 A 的地址,切換到用戶 B 點擊 onlyOwner 函數,看到owner的值是賬戶 A 的地址。這時候如果點擊 transferOwnership 會報錯,因為這個函數被 onlyOwner 修飾了,會先判斷當前調用合約的是否是合約所有者。當前合約所有者是賬戶 A,合約調用者賬戶 B 是沒權限轉移權限的。

技術分享圖片

b) 把賬戶切換到 A,transferOwnership 地址填賬戶 B,這時候你就可以把合約所有者權限轉移給賬戶 B 了。而再一次執行,發現提示錯誤了,因為此時合約所有者已經是賬戶 B,賬戶 A 沒權限。

技術分享圖片


contract StandardToken is ERC20 { using SafeMath for uint256;  // 使用 SafeMath 函數庫 mapping (address => mapping (address => uint256)) allowed;  // 類比二維數組 mapping(address => uint256) balances;  // 類比一維數組
   // 把合約調用者的余額轉移 _value 個tokens給用戶 _to function transfer(address _to, uint256 _value) public returns (bool){ assert(0 < _value); assert(balances[msg.sender] >= _value); balances[msg.sender] = balances[msg.sender].sub(_value); balances[_to] = balances[_to].add(_value); emit Transfer(msg.sender, _to, _value); return true; }    // 查詢 _owner 賬戶的余額 function balanceOf(address _owner) constant public returns (uint256 balance){ return balances[_owner]; }    // 從地址 _from 轉移 _value 個 tokens 給地址 _to function transferFrom(address _from, address _to, uint256 _value) public returns (bool){ uint256 _allowance = allowed[_from][msg.sender]; assert (balances[_from] >= _value); assert (_allowance >= _value); assert (_value > 0); balances[_to] = balances[_to].add(_value); balances[_from] = balances[_from].sub(_value); allowed[_from][msg.sender] = _allowance.sub(_value); emit Transfer(_from, _to, _value); return true; } // 允許 _spender 多次取回您的帳戶,最高達 _value 金額; 如果再次調用此函數,它將用 _value 的當前值覆蓋的 allowance 值 function approve(address _spender, uint256 _value) public returns (bool){ require((_value == 0) || (allowed[msg.sender][_spender] == 0)); allowed[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; } // 返回 _spender 仍然被允許從 _owner 提取的金額 function allowance(address _owner, address _spender) constant public returns (uint256 remaining){ return allowed[_owner][_spender]; } } 

這邊邏輯不復雜,有個概念可能不太好理解,這裏詳細說明下。allowed 這個變量(類比成二維數組),是用來存取授信的額度,在 approve 函數中定義。

allowed[msg.sender][_spender] = _value;

這個 msg.sender 是當前合約調用者,_spender 是被授權人,額度是 _value 。可以通俗的理解成,銀行(msg.sender)給用戶( _spender) 授權了 _value 額度的 tokens 。在銀行轉賬,相應的額度也會減少,而用戶在此銀行最多可以轉被授權的 _value 個 tokens,不同的銀行(msg.sender)可以給用戶(_spender)授信不同的額度(_value)。

把 allowd 的概念理解了,allowance 函數也就很好理解了,第一個參數 _owner 類比成銀行,第二個參數 _spender 類比成用戶,這個函數就用來查詢用戶(_spender)在銀行(_owner)剩余的額度(tokens)。通過上述的講解,可以知道 transfer 和 transferfrom 函數區別如下:

1. transfer 是把當前合約調用者的 tokens 轉移給其他人

2. transferFrom 則是可以把 ”銀行“ 授信額度的錢(tokens)轉給自己或者他人,轉移的是 “銀行” 的 tokens

contract Ammbr is StandardToken, Ownable {
    string public name = ‘‘;
    string public symbol = ‘‘;
    uint8 public  decimals = 0;
    uint256 public maxMintBlock = 0;

    event Mint(address indexed to, uint256 amount);

    // 給地址 _to 初始化數量 _amount 數量的 tokens,註意 onlyOwner 修飾,只有合約創建者才有權限分配
    function mint(address _to, uint256 _amount) onlyOwner  public returns (bool){
        assert(maxMintBlock == 0);
        totalSupply = totalSupply.add(_amount);
        balances[_to] = balances[_to].add(_amount);
        emit Mint(_to, _amount);
        maxMintBlock = 1;
        return true;
    }

    // 轉帳操作,可以同時轉給多個人
    function multiTransfer(address[] destinations, uint[] tokens) public returns (bool success){
        assert(destinations.length > 0);
        assert(destinations.length < 128);
        assert(destinations.length == tokens.length);
        uint8 i = 0;
        uint totalTokensToTransfer = 0;
        for (i = 0; i < destinations.length; i++){
            assert(tokens[i] > 0);
            totalTokensToTransfer += tokens[i]; // 存在溢出
        }
        assert (balances[msg.sender] > totalTokensToTransfer);
        balances[msg.sender] = balances[msg.sender].sub(totalTokensToTransfer);
        for (i = 0; i < destinations.length; i++){
            balances[destinations[i]] = balances[destinations[i]].add(tokens[i]);
            emit Transfer(msg.sender, destinations[i], tokens[i]);
        }
        return true;
    }

    // 構造函數,可選
    function Ammbr(string _name , string _symbol , uint8 _decimals) public{
        name = _name;   // 設定代幣的名字,比如: MyToken
        symbol = _symbol; // 返回代幣的符號,比如: ARM
        decimals = _decimals;   // 設置 token 的精度
    }
}

現在來詳細說明下 decimals 這個參數。首先我們來理解下常說的以太(ether)到底是怎麽換算的。在以太坊交易中,最小的單位是 wei ,1 ether = 10^18 wei 。單位換算在線地址: https://converter.murkin.me/

ether單位對照表:

技術分享圖片

調用合約轉發 Token 的時候,傳入的值是要轉發的 Token 數乘上精度(默認decimals=18),比如轉1個Token,傳入合約的值是1000000000000000000 wei

代幣(Token)參數對照表:

技術分享圖片

前面把我認為的難點、疑惑點都說完了,想必大家看懂這個合約也沒什麽難度。看懂合約後,如果看過以太坊智能合約安全漏洞入門之類的文章,應該一看就能看出 multiTransfer 存在溢出漏洞。原理就是 totalTokensToTransfer 沒有使用安全函數,可以導致整數上溢出。詳情可參考 這篇文章 。下面我們就手動調試下這個漏洞

1. 首先運行 mint 函數,給賬戶 A 初始化 100000 個tokens

技術分享圖片

2. 向賬戶 B,C 分別充值 57896044618658097711785492504343953926634992332820282019728792003956564819968, 相加得 115792089237316195423570985008687907853269984665640564039457584007913129639936,而賬戶 unit256 最大值15792089237316195423570985008687907853269984665640564039457584007913129639935,導致溢出 totalTokensToTransfer 的值為0

註意: 在 remix 調試時候,傳入的數組可以用 [......, ......] 表示,但是地址必須用雙引號包裹,傳入的數字如果較大也必須用雙引號包裹,否則會報錯

技術分享圖片

技術分享圖片

技術分享圖片

總結:

其實智能合約的代碼比平常分析逆向的程序代碼都簡單多了,只要你掌握 ERC20 的標準幾個接口,了解函數修改器的概念以及一些以太坊基本的概念,相信看懂一個 ERC20 標準的合約並不難。也希望大家能把上述分析自己動手實踐一下,之前我很多地方也有疑問,通過動手實踐很快就明白問題的所在了。如果學習有捷徑的話就是多動手,多調試。Solidity 沒有打印的函數,有時候會給調試帶來不便,下面補個 Solidity 調試的代碼,方便大家打印變量值。

pragma solidity ^0.4.21;

//通過log函數重載,對不同類型的變量trigger不同的event,實現solidity打印效果,使用方法為:log(string name, var value)

contract Console {
    event LogUint(string, uint);
    function log(string s , uint x) internal {
    emit LogUint(s, x);
    }
    
    event LogInt(string, int);
    function log(string s , int x) internal {
    emit LogInt(s, x);
    }
    
    event LogBytes(string, bytes);
    function log(string s , bytes x) internal {
    emit LogBytes(s, x);
    }
    
    event LogBytes32(string, bytes32);
    function log(string s , bytes32 x) internal {
    emit LogBytes32(s, x);
    }

    event LogAddress(string, address);
    function log(string s , address x) internal {
    emit LogAddress(s, x);
    }

    event LogBool(string, bool);
    function log(string s , bool x) internal {
    emit LogBool(s, x);
    }
}

把文件保存成 Console.sol ,其他程序中引用這個文件即可。具體用法如下

技術分享圖片

以上是全文內容,如果文中說的有誤,或者大家有什麽更好的想法,歡迎大家和我交流。

參考:

http://yinxiangblog.com/?id=10

https://github.com/ethereum/EIPs/issues/20

https://blog.csdn.net/diandianxiyu_geek/article/details/78082551

https://theethereum.wiki/w/index.php/ERC20_Token_Standard

http://www.cnblogs.com/huahuayu/p/8593774.html https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit#heading=h.wqhvh2y0obwt

一文讀懂以太坊代幣合約