以太坊被稱為區塊鏈2.0,就是因為以太坊在應用層提供了虛擬機,使得開發者可以基於它自定義邏輯,通常被稱為智能合約,合約中的公共接口可以作為區塊鏈中的普通交易執行。本文就智能合約發代幣流程作一完整介紹(當然智能合約不局限於發代幣)。內容如下:
- Solidity
- ERC20
- 合約編寫與發布
- 合約源碼上傳
- 其它
Solidity
Solidity是用於在以太坊編寫智能合約的語言,目前最新版本0.5.11。這里對幾個關鍵概念作一介紹。
library
library常用於提供可復用方法,可以隨合約[作為合約的一部分]發布,不過最終它是單獨部署[到鏈上]的,有自己的地址。外部可以使用delegatecall方法調用庫函數。
我們使用library關鍵字來創建一個library,這和創建contract十分類似。但不像contract,在library中我們不能定義任何storage類型的變量。因為library只是意味着代碼的重用而不是進行state的狀態管理。
internal的庫函數對所有合約可見。
using for:指令using A for B;用來附着庫A里定義的函數到任意類型B,函數的第一個參數應是B的實例。可以使用using A for *,將庫函數賦予任意類型。庫函數可以重載的,你可以定義好幾個同名函數,但是第一個參數的類型不同,調用的時候自動的根據調用類型選擇某一種方法。
address indexed
The indexed parameters for logged events will allow you to search for these events using the indexed parameters as filters.
The indexed keyword is only relevant to logged events.
幾個預定義字段
address.balance:地址余額,指該地址相關的以太幣數額。
msg.sender:交易發起人。
this:合約地址。
msg.value:發起這筆交易支付的以太幣數額,和payable搭配使用。
似乎還有msg.sig、msg.data、msg.gas。
payable
修飾函數,表示在調用函數時,可以給這個合約充以太幣(合約也是一種賬戶,也有自己的地址)。合約本身持有以太幣,可使得用戶基於此合約進行跨幣交易更方便。
constant、view、pure
constant修飾常量或函數,修飾函數時表示該函數不修改合約狀態,即不產生需廣播的交易,也就不會消耗gas,一般用於讀狀態或狀態無關操作。0.4.17開始,改為view和pure修飾函數,前者表示讀狀態,后者表示狀態無關。
存儲區
storage:狀態變量,將存儲在鏈上;
memory:臨時變量
interface:搭配傳入的不同address,實現多態。
event,應用層(web3)可通過watch監聽,應該是用輪詢實現。
modifier,簡單的aop,編譯時會將代碼替換合並。
ERC20
我們可以在智能合約里隨意開發各種功能,相當一部分以代幣合約的形式存在。既然是代幣,自然有轉賬、余額查詢等功能,大家苦於對不同的代幣合約開發不同的錢包,於是有了一些規范的出現,其中最普遍的是ERC20。
ERC20定義了若干方法,網上有很多資料,這里不再贅述。稍微不好理解的是approve、transferFrom及allowance三個方法,這里舉例說明——賬戶A有1000個ETH,想授權B賬戶隨意調用A賬戶的100個ETH,則需調用approve(B,100)。當B賬戶想用這100個ETH中的10個ETH給C賬戶時,則調用transferFrom(A, C, 10)。這時調用allowance(A, B)可以查看B賬戶還能夠調用A賬戶多少個token。
本人試用了若干本地錢包和在線錢包,都支持轉賬,但鮮有支持授權的。
有人在github上匯總了眾多ERC20合約代碼,see 基於以太坊發行的ERC20代幣合約代碼大集合 。
合約編寫與發布
一個簡單的合約demo如下:

1 pragma solidity ^0.5.7; 2 3 import './safemath.sol'; 4 5 contract ErbCoin { 6 using SafeMath for uint256; 7 8 string constant public name = "Mask Coin"; // token name 9 string constant public symbol = "MASK"; // token symbol 10 uint256 public decimals = 18; // token digit 11 12 //mapping (address => uint256) public balanceOf; 13 mapping (address => mapping (address => uint256)) public allowance; //a授權給b表示b可轉賬給其他人的代幣數 14 mapping (address => uint256) public frozenBalances; //凍結余額 15 mapping (address => uint256) public balances; //可操作余額 16 17 uint256 public totalSupply = 0; 18 bool public stopped = false; 19 20 //uint256 constant valueFounder = 1000000; 21 address constant zeroaddr = address(0); 22 address owner = zeroaddr; //合約所有者 23 address founder = zeroaddr; //初始代幣持有者 24 25 modifier isOwner { 26 assert(owner == msg.sender); 27 _; 28 } 29 30 modifier isFounder { 31 assert(founder == msg.sender); 32 _; 33 } 34 35 modifier isAdmin { 36 assert(owner == msg.sender || founder == msg.sender); 37 _; 38 } 39 40 modifier isRunning { 41 assert (!stopped); 42 _; 43 } 44 45 modifier validAddress { 46 assert(zeroaddr != msg.sender); 47 _; 48 } 49 50 constructor(address _addressFounder,uint256 _valueFounder) public { 51 owner = msg.sender; 52 founder = _addressFounder; 53 totalSupply = _valueFounder*10**decimals; 54 balances[founder] = totalSupply; 55 emit Transfer(zeroaddr, founder, totalSupply); 56 } 57 58 function balanceOf(address _owner) public view returns (uint256) { 59 //賬戶余額 = 可操作余額 + 被凍結余額 60 return balances[_owner] + frozenBalances[_owner]; 61 } 62 63 function transfer(address _to, uint256 _value) public isRunning validAddress returns (bool success) { 64 balances[msg.sender] = balances[msg.sender].sub(_value); 65 balances[_to] = balances[_to].add(_value); 66 emit Transfer(msg.sender, _to, _value); 67 return true; 68 } 69 70 //msg.sender 將 _from 授權給他(msg.sender)的代幣轉給 _to 71 function transferFrom(address _from, address _to, uint256 _value) public isRunning validAddress returns (bool success) { 72 balances[_from] = balances[_from].sub(_value); 73 //balances[_to] = balances[_to].add(_value); 74 frozenBalances[_to] = frozenBalances[_to].add(_value); //代幣為凍結狀態 75 allowance[_from][msg.sender] = allowance[_from][msg.sender].sub(_value); 76 emit TransferFrozen(_to, _value); 77 return true; 78 } 79 80 //msg.sender 授權 _spender 可操作代幣數 81 function approve(address _spender, uint256 _value) public isRunning isFounder returns (bool success) { 82 require(_value == 0 || allowance[msg.sender][_spender] == 0,"illegal operation"); 83 allowance[msg.sender][_spender] = _value; 84 emit Approval(msg.sender, _spender, _value); 85 return true; 86 } 87 88 //凍結部分釋放 89 function release(address _target, uint256 _value) public isRunning isAdmin returns(bool){ 90 frozenBalances[_target] = frozenBalances[_target].sub(_value); 91 balances[_target] = balances[_target].add(_value); 92 emit Release(_target, _value); 93 return true; 94 } 95 96 function stop() public isAdmin { 97 stopped = true; 98 } 99 100 function start() public isAdmin { 101 stopped = false; 102 } 103 104 event Transfer(address indexed _from, address indexed _to, uint256 _value); 105 event Approval(address indexed _owner, address indexed _spender, uint256 _value); 106 event TransferFrozen(address _target, uint256 _value); 107 event Release(address _target, uint256 _value); 108 }
細心的朋友會發現,ERC20規范定義的都是函數,而這里的代碼並沒有name()、symbol()、decimals(),而只有同名字段,這是因為編譯器會自動將公共字段編譯為函數。
代碼里還import了一個庫文件,是為了避免數值溢出導致的bug,具體可看關於ERC20 Token智能合約的SafeMath安全。
發布合約我們可以直接在瀏覽器端操作,需要用到:
MetaMask:一款以瀏覽器插件形式存在的以太坊錢包,能接入主鏈和多個測試鏈。
Remix:https://remix.ethereum.org,編寫編譯發布合約的一個站點,能和MetaMask無縫合作。
具體如何操作就不作介紹了,網上已有較多資料。
本人踩過一個坑,發幣多次在imToken(一個較流行的錢包App)上顯示的代幣名稱都是XXX_UNKOWN,后面多了_UNKOWN后綴;后有一次將contract名稱和代幣name、symbol保持一致,才顯示正常,但不知是否是名稱不一致導致的問題。
另合約部署成功后,可以在Remix上調用合約接口。如果你知道其它人的合約代碼和合約地址,也可以在Remix上編譯后,輸入地址調用,如下圖:
如果你知道合約的ABI,那么使用https://www.myetherwallet.com調用合約接口則更方便,如下圖:
別人的合約代碼和ABI從哪里來?請看下節。
合約源碼上傳
為什么要上傳智能合約的代碼呢?
- 公開token的源碼,增加透明度和投資人的信任度;
- 上傳源碼后,人們可以在Etherscan查看當前token的源碼,同時也可以很方便的看到token的相關信息;
- 上述所說,如果不是標准接口,市面上的各類錢包不支持,那么可以通過Remix或myetherwallet直接調用接口,而不用另外開發定制化錢包。
代碼上傳是在https://etherscan.io上完成的,目前上傳很簡單,基本上上傳代碼文件(有幾個傳幾個)后next即可(不像網上有些資料說的需要另外輸入byteCode和ABI)。不過需要注意的是國內網絡問題(你懂的),在提交驗證合約時,總是提示 “Sorry! We encountered an unexpected error. Please try back again shortly ”,這是因為驗證碼被牆了,如何操作不需要我教了吧。
其它
在智能合約中,只能等待外部的調用,而無法執行定時任務。同時也無法主動發起外部請求。
默認情況下,geth創建的賬戶是被鎖住的,可以轉入不能轉出,除非解鎖(geth --unlock)。盡量避免在對外的節點服務器上做解鎖操作,否則將有被盜幣的風險。具體可看金錢難寐,大盜獨行——以太坊 JSON-RPC 接口多種盜幣手法大揭秘,本人亦有教訓,所幸損失不大。實際對外提供服務的全節點,做到以下一點或幾點,就能顯著降低風險:
不使用默認端口;
RPC只對內開放,對外須網關協議轉換;
服務器上不存儲賬戶,或不直接在服務器上使用賬戶。
助記詞(BIP32/BIP39/BIP44)的原理可參看 比特幣源碼研讀(7)—— 錢包的原理
智能合約的同一個接口,gas used 未必相同,要看實際執行到的步驟和對存儲的操作,規則如下:
EVM智能合約的實現原理:solidity雖然是編譯后上鏈的,但執行時依舊是由EVM(其實也是以太坊內的若干函數)解釋執行的,EVM通過字節碼確定操作符和操作數,然后其自身執行相應邏輯。從這個角度說,EVM就是solidity的解釋器。若將EVM看作操作系統,solidity編譯后即能被EVM運行,那么solidity也可認為是編譯型語言。可看EVM原理及其功能擴展。
其它參考資料: