Skip to main content

建立第一個加密代幣合約

我們已寫好並部署完成了第一個智能合約,也驗證了合約確實可以運作。在閱讀完本篇後,你將學會建立一個簡易的加密代幣🔒💵。本篇目的並非為了寫出一個安全可用的加密代幣,而是以介紹代幣合約的相關概念為主, 是以對合約做了適當地簡化,好更易於理解。

開發前的準備

延續上一篇的內容,在開發的過程中,我們將繼續使用ganache-cli1工具在電腦上模擬智能合約所需的乙太坊區塊鏈測試環境。

首先確保已啟動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。這邊隨意選擇了一個數字10000uint是很常用來儲存正整數的型別,等同於uint256,可以儲存256位元(bit)的資料,其他類似的還有uint8uint16uint32等。

我們用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)物件2msg.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.coinbaseweb3.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失敗的案例。

有興趣的人可以進一步查看參考資料45了解智能合約當前的一些最佳實現。

結語

看完這篇除了學到本篇講解的SimpleToken外,應該也可以大致看得懂truffle預設的MetaCoin.sol合約了(在truffle 4.x之後,MetaCoin已經從專案建立後的預設範例中移出了,但仍可以在新增專案時透過truffle unbox metacoin命令取得)。不同的細節可以查看solidity相關語法2

下一篇會接著介紹如何使用經過驗證的函式庫,來建立一份可以放到乙太幣錢包👛的加密代幣🔒💵合約。

參考資料