建立第一個加密代幣合約
我們已寫好並部署完成了第一個智能合約,也驗證了合約確實可以運作。在閱讀完本篇後,你將學會建立一個簡易的加密代幣🔒💵。本篇目的並非為了寫出一個安全可用的加密代幣,而是以介紹代幣合約的相關概念為主, 是以對合約做了適當地簡化,好更易於理解。
開發前的準備
延續上一篇的內容,在開發的過程中,我們將繼續使用ganache-cli
1工具在電腦上模擬智能合約所需的乙太坊區塊鏈測試環境。
首先確保已啟動ganache-cli,若尚未啟動,可以使用以下命令啟動
$ ganache-cli
...
這樣我們就可以開始建立加密代幣智能合約專案了。
代幣合約的基礎概念
代幣合約扮演像是銀行🏦的角色。使用者在代幣合約中,用自己的乙太幣帳戶地址當作銀行帳戶,可以透過代幣合約執行轉帳(transfer,將代幣由一個帳戶傳送到另一個帳戶),查詢餘額(balanceOf,查詢指定帳戶中擁有的代幣)等原本由銀行負責的工作。因為合約部署在公開區塊鏈上,所有的交易都是公開透明,可供檢驗的。
建立一個代幣合約
在contracts/
目錄下建立一個SimpleToken.sol
檔案。也可以使用以下命令來產生檔案:
$ truffle create contract SimpleToken
SimpleToken.sol
檔案內容如下:
pragma solidity ^0.4.19;
contract SimpleToken {
uint INITIAL_SUPPLY = 10000;
mapping(address => uint) balances;
function SimpleToken() public {
balances[msg.sender] = INITIAL_SUPPLY;
}
// transfer token for a specified address
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] > _amount);
balances[msg.sender] -= _amount;
balances[_to] += _amount;
}
// Gets the balance of the specified address
function balanceOf(address _owner) public constant returns (uint) {
return balances[_owner];
}
}
講解
將程式碼定義的合約畫成UML圖如下
{% plantuml %} class SimpleToken { INITIAL_SUPPLY : uint balances : map
- transfer()
- balanceOf() } {% endplantuml %}
pragma solidity ^0.4.19;
第一行指名目前使用的solidity版本,不同版本的solidity可能會編譯出不同的bytecode。
uint INITIAL_SUPPLY = 10000;
mapping(address => unit) balances;
我們定義了初始代幣數目INITIAL_SUPPLY
。這邊隨意選擇了一個數字10000
。uint
是很常用來儲存正整數
的型別,等同於uint256
,可以儲存256位元(bit)的資料,其他類似的還有uint8
,uint16
,uint32
等。
我們用mapping
來定義一個可以儲存鍵值對(key-value pair)的資料結構(類似Javascript中的{"0xaabbccddeeff": 888}
),同時也需要分別指定address
作為鍵的型別,指定uint
作為值的型別。和 Javascript 不同之處,在於 Solidity 中定義好型別後,並不能隨時更改之後要儲存的型別。
contract SimpleToken {
function SimpleToken() public {
balances[msg.sender] = INITIAL_SUPPLY;
}
}
和合約同名的SimpleToken
函式,就是SimpleToken
這個合約的建構函式(constructor)。函式中我們拿msg.sender
當作key,INITIAL_SUPPLY
當作值,將所有的初始代幣INITIAL_SUPPLY
都指定給msg.sender
帳戶。
msg
是一個全域(Global)物件2,msg.sender
表示用作呼叫當前函式的帳戶。由於建構函式只有在合約部署時會被執行,因此這邊用到的msg.sender
,也就代表著用來部署這個合約的帳戶。
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] > _amount);
balances[msg.sender] -= _amount;
balances[_to] += _amount;
}
transfer
函式定義了如何轉帳
,只要指定要傳送的帳戶與數目,就會從呼叫者手上把對應數目的代幣移轉到指定的帳戶上。
{% mermaid %} graph LR 傳送者 -- 轉帳 --> 代幣合約 代幣合約 -.-> 修改傳送者和接收者餘額 {% endmermaid %}
require(balances[msg.sender] > _amount);
語句判斷帳戶中是否還有足夠轉出的餘額,若存款小於想轉出的數目,就丟出錯誤。
這個函式這麼寫當然還是過度簡化了,若要能實際使用,需要檢查更多可能的狀況。但就先這樣吧。
function balanceOf(address _owner) public constant returns (uint256) {
return balances[_owner];
}
balanceOf
函式的作用,是讓使用者可以查詢任一帳戶的餘額。透過傳入_owner
帳戶,可以查詢_owner
帳戶儲存在balances
對照表中的值。
{% mermaid %} graph LR 傳送者 --> 代幣合約 代幣合約 -. 查詢結果 .-> 傳送者 {% endmermaid %}
如此一來,我們就寫好一個新加密代幣🔒💵合約囉!接下來將要編譯合約並部署到區塊鏈上。
編譯與部署
在migrations/
目錄下建立一個3_deploy_simple_token.js
檔案,內容如下:
var SimpleToken = artifacts.require("SimpleToken");
module.exports = function(deployer) {
deployer.deploy(SimpleToken);
};
現在可執行compile與migrate命令
$ truffle compile
...
$ truffle migrate
Using network 'development'.
Running migration: 3_deploy_token.js
Deploying HelloToken...
... 0x2c4659528c68b4e43d1edff6c989fba05e8e7e56cc4085d408426d339b4e9ba4
SimpleToken: 0x352fa9aa18106f269d944935503afe57a00a9a0d
Saving successful migration to network...
... 0x1434c1b61e9719f410fc6090ce37c0ec141a1738aba278dd320738e4a5d229fa
Saving artifacts...
如此一來我們已將SimpleToken
代幣合約部署到ganache上。
驗證
合約部署完成後,我們可以使用truffle console
命令開啟console,輸入以下命令來驗證合約是否能照我們設計的方式運作。
$ truffle console
> let contract
> SimpleToken.deployed().then(instance => contract = instance)
> contract.balanceOf(web3.eth.coinbase)
BigNumber { s: 1, e: 4, c: [ 10000 ] }
> contract.balanceOf(web3.eth.accounts[1])
BigNumber { s: 1, e: 0, c: [ 0 ] }
> contract.transfer(web3.eth.accounts[1], 123)
...
> contract.balanceOf(web3.eth.coinbase)
BigNumber { s: 1, e: 3, c: [ 9877 ] }
> contract.balanceOf.call(web3.eth.accounts[1])
BigNumber { s: 1, e: 2, c: [ 123 ] }
>
講解
> let contract
> SimpleToken.deployed().then(instance => contract = instance)
這邊使用SimpleToken.deployed().then
語句來取得SimpleToken合約的Instance(實例),並存到contract
變數中,以方便後續的呼叫。
> contract.balanceOf(web3.eth.coinbase)
BigNumber { s: 1, e: 4, c: [ 10000 ] }
> contract.balanceOf(web3.eth.accounts[1])
BigNumber { s: 1, e: 0, c: [ 0 ] }
還記得啟動ganache後預設會產生10個帳戶(Accounts)嗎?。web3.eth.coinbase
代表操作者預設的帳戶,即10帳戶的第一個帳戶web3.eth.accounts[0]
,所以這邊呼叫web3.eth.coinbase
或web3.eth.accounts[0]
結果是一樣的。
> contract.balanceOf(web3.eth.accounts[0])
BigNumber { s: 1, e: 4, c: [ 10000 ] }
這兩句的目的是在進行轉帳操作前,先查詢前兩個帳戶所擁有的代幣餘額。透過呼叫balanceOf
函式,可以看到第一個帳戶(部署合約的帳戶)裡存著所有的代幣。
> contract.transfer(web3.eth.accounts[1], 123)
...
接著使用transfer
函式來傳送123
個代幣到第二個帳戶web3.eth.accounts[1]
。如果轉帳成功,傳送者預設帳戶中會減少123個代幣,接收者帳戶中會增加123個代幣。
> contract.balanceOf(web3.eth.coinbase)
BigNumber { s: 1, e: 3, c: [ 9877 ] }
> contract.balanceOf.call(web3.eth.accounts[1])
BigNumber { s: 1, e: 2, c: [ 123 ] }
>
我們再次透過呼叫balanceOf
函式,查詢傳送者帳戶和接收者帳戶各自剩下的SimpleToken數目。發現轉帳真的成功了。
你知道剛剛的程式碼裡有一堆安全漏洞💣嗎?
寫智能合約看起來並不困難吧?但因為智能合約的運作是透明公開的,而且其中牽涉了代幣或金錢的流動,這提供了駭客很強的挑戰動機。
因此如果要妥善處理智能合約,會遇到的諸多安全問題。即使單純如本篇中的SimpleToken
,也至少會遇到幾個問題:例如transfer
函式中沒有禁止傳入負數金額,因此傳送者反過來可以從接收者那邊取得代幣。同時也沒有檢查接收者帳戶是否合法,因此傳送者可能會傳送失敗或因為送到黑洞中,白白損失了代幣。
有著一堆安全漏洞的合約,輕則執行失敗白花交易費用,嚴重則影響到合約中的代幣或以太幣。已有多起因為合約的漏洞,造成儲存在合約中的代幣或以太幣被駭客轉走,使得ICO失敗的案例。
有興趣的人可以進一步查看參考資料4和5了解智能合約當前的一些最佳實現。
結語
看完這篇除了學到本篇講解的SimpleToken
外,應該也可以大致看得懂truffle預設的MetaCoin.sol
合約了(在truffle 4.x之後,MetaCoin已經從專案建立後的預設範例中移出了,但仍可以在新增專案時透過truffle unbox metacoin
命令取得)。不同的細節可以查看solidity相關語法2。
下一篇會接著介紹如何使用經過驗證的函式庫,來建立一份可以放到乙太幣錢包👛的加密代幣🔒💵合約。
參考資料
- [1] https://github.com/trufflesuite/ganache-cli
- [2] Units and Globally Available Variables http://solidity.readthedocs.io/en/develop/units-and-global-variables.html
- [3] An Ethereum Hello World Smart Contract for Beginners part 1, part 2
- [4] Onward with Ethereum Smart Contract Security https://blog.zeppelin.solutions/onward-with-ethereum-smart-contract-security-97a827e47702
- [5] Ethereum Contract Security Techniques and Tips https://github.com/ConsenSys/smart-contract-best-practices
- [6] 範例網址 https://github.com/gasolin/learndapp/tree/master/examples/hello_simple_token