一文讀懂以太坊代幣合約
本文首發自 https://www.secpulse.com/archives/73696.html ,轉載請註明出處。
工欲善其事,必先利其器。要想挖掘和分析智能合約的漏洞,你必須要先學會看懂智能合約。而目前智能合約中有很大一部分是發行代幣的,那什麽是代幣,他們有什麽標準呢?本文就是帶領你入門,教會你看懂一個代幣的智能合約。
以太坊代幣
在以太坊系統中,存在作為基礎貨幣的 Ether(以太),以及同樣可以作為貨幣使用的 Token(代幣)。
以太坊與其他加密貨幣的主要不同在於,以太坊不是單純的貨幣,而是一個環境/平臺。在這個平臺上,任何人都可以利用區塊鏈的技術,通過智能合約來構建自己的項目和DAPPS(去中心化應用)。
如果把以太坊理解成互聯網,DAPPS則是在上面運行的網頁。DAPPS是去中心化的,意味著它不屬於某個人,而是屬於一群人。DAPPS發布的方式通常是采用被稱為 ICO 的眾籌方式。簡單來說,你需要用你的以太來購買相應DAPP的一些tokens。
一般有兩種Token:
- Usage Tokens: 就是對應 DAPP 的原生貨幣。Golem 就是一個很好的例子,如果你需要使用 Golem 的服務,你就需要為其支付 Golem Network Token(GNT)。由於這種 Tokens 有貨幣價值,所以通常不會有其他的權益。
- 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); }
函數:
註意:非常重要的一點是調用者應該處理函數返回的錯誤,而不是假設錯誤永遠不會發生。
- totalSupply: 返回token的總供應量
- balanceOf: 用於查詢某個賬戶的賬戶余額
- tansfer: 發送 _value 個 token 到地址 _to
- transferFrom: 從地址 _from 發送 _value 個 token 到地址 _to
- approve: 允許 _spender 多次取回您的帳戶,最高達 _value 金額; 如果再次調用此函數,它將用 _value 的當前值覆蓋的 allowance 值。
- allowance: 返回 _spender 仍然被允許從 _owner 提取的金額。
事件 :
- event Transfer: 當 tokens 被轉移時觸發。
- 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一文讀懂以太坊代幣合約