Libra硬剛微信、支付寶?你也試試

2019-07-20     榮格財經



作者 | Second State

責編 | 喬治

出品 | 區塊鏈大本營(blockchain_camp)

本月17日,當 Marcus 被問到 Libra 未來是否會成為支付寶、微信的競爭對手時,Marcus 對此表示了默認,「是的,議員。」但無論 Libra 未來的命運如何,也無論 Libra 將如何與微信、支付寶競爭,焦點都會回歸到金融資產發行的問題上,那如何用代碼實現?今天,營長手把手教你如何基於 ERC20 標準在 Libra 上發布金融資產。


本文是 「Libra 編程」系列文章的第 3 篇,也是最後一篇。在之前的兩篇文章,我們分別探討了 Libra 項目的技術意義以及 Libra Client 與 Validator 內部處理與執行交易。

  • Part1:被 Libra 刷屏的你絕對不知道, 也許這才是它最大的"核武器"...
  • Part2:關於 Libra 幣交易, 你需要了解的一切...


Libra 作為法幣穩定幣,成為一個金融系統還需要具有債券股票以及各種證券資產與衍生品,本文將從技術角度入手,使用 ERC20 標準在 Libra 區塊鏈上發布金融資產。主要分為以下兩部分:

  • 編寫 token moudule
  • 編譯、部署 token moudule


希望這個教程可以讓你對 Libra 的技術細節有更深刻的了解。

編寫 token moudule


如何使用 Move IR 編寫一個簡單的 token module?

為方便理解,我們選擇 Ethereum 的 ERC20 token 作為範例,分別執行 mint、balanceOf 和 transfer 三個功能。

開始前,我們需要了解 Libra 與以太坊在處理 resource 的邏輯方面有什麼不同。

與 Ethereum global state 不同的是,Libra 並不設置統一集中存儲的 global resource,而是將 resource 分散在各個帳戶存放。因此,以太坊智能合約撰寫 "address storage owner = 0x" 這類變量需要用不同的邏輯來實現。每個人擁有多少 token 也分別存放在各自帳戶的 resource 下,而不是採用 "mapping(address=>uint256) " 這樣的統一存儲方式處理。

1、Capability

目前 Libra 開發團隊推薦的處理 global variable 的方式是使用一個 singleton pattern 的 module 來進行模擬。

因此,我們將事件擁有所有者的 (owner) 權限定義為一種只能被發布一次的 resource,比如 "resource T{}"。針對這個 T 進行的操作有兩個方法,一是在初始化時執行 "grant()" 用來確保 Token Capability 被移交給所有者;而 "borrow_sender_capability()" 則是檢查操作者是否擁有所有者的權限。

1module TokenCapability {
2 resource T {}
3 // Grant Token Capability to the owner
4 public grant() {}
5 // Return an immutable reference to the TokenCapability of the sender if it exists. This will only succeed if the transaction sender is the owner.
6 public borrow_sender_capability(): &mut R#Self.T {}
7}


a) grant()

如何執行 "grant()" ?

首先,我們需要定義兩個角色,調用這個函數的交易發起者與實際上的所有者 owner。很可惜的是目前 Move IR 並沒有提供類似 "Self.published_address" 的方式來讓我們獲得發布該 module 的帳戶,因此我們只能在代碼中寫死 module 所有者的地址,代碼如下:

 1// Grant Token Capability to the owner
2public grant() {
3 let sender: address;
4 let owner: address;
5 let t: R#Self.T;
6
7 sender = get_txn_sender();
8
9 owner = 0x1234;
10 // 假設 0x1234 是所有者的地址
11 assert(move(sender) == move(owner), 77);
12 // 檢查交易發起者是否為所有者
13
14 t = T{};
15 // 為所有者建立新的 Token Capability
16 move_to_sender(move(t));
17 // 將 TokenCapability.T 轉移給所有者。
18 return;
19}


從上面的代碼中我們可以發現,只有通過 "sender == owner" 檢查才能取得所有者的 resource T,因此我們可以確保 resource T 只會被所有者所擁有,其他的帳戶都沒有機會獲得這個 resource T。

此外,"move_to_sender(resource)" 是 Move IR 提供的內建函數,它代表了將 resource 移交給交易發起者的帳戶。

b) Borrow_sender_capability()

如何檢查並確認交易發起者擁有所有者的 resource?

藉助 Move IR 提供的輔助函數 "borrow_global(resource account)" 處理。borrow_global 會去該帳戶下面調取出 resource,如果該帳戶下沒有持有這個 resource 則會觸發意外情況,交易也會失敗。若成功則會返回可變的 resource reference。

 1// 如果存在,則返回對交易發起者的 TokenCapability 的不可變引用。
2// 只有在交易發起者是所有者才會執行成功。
3public borrow_sender_capability(): &mut R#Self.T {
4 let sender: address;
5 let t_ref: &mut R#Self.T;
6
7 sender = get_txn_sender();
8 t_ref = borrow_global(move(sender));
9
10 return move(t_ref);
11 }

2、Token

Libra 的權限管理方式比較特別,上文已著重介紹,接下來撰寫 Token module!

 1module Token {
2
3 import Transaction.TokenCapability;
4
5 // Token resource, 代表一個帳戶的總餘額.
6 resource T {
7 value: u64,
8 }
9
10 // 建立一個新的 Token.T , value值為0
11 public zero(): R#Self.T {
12 return T{value: 0};
13 }
14
15 // 返回 Token的值
16 public value(token_ref: &R#Self.T): u64 {
17 return *&move(token_ref).value;
18 }
19
20 // 為交易發起者發布初始餘額為0的Token
21 public publish() {}
22
23 // `mint_to_address` 只能由所有者調用.
24 // 這會給收款人一個新的Token ,價值是amount
25 public mint_to_address(payee: address, amount: u64) {}
26
27 // Mint 一個新的Token,價值是`value`.
28 mint(value: u64, capability: &mut R#TokenCapability.T): R#Self.T {}
29 // 返回Token 餘額 `account`.
30 public balanceOf(account: address): u64 {}
31
32 // 返回交易發起者的Token 餘額.
33 public balance(): u64 {}
34
35 // 將 `to_deposit` 的token 存入 the `payee`\\'s 帳戶
36 public deposit(payee: address, to_deposit: R#Self.T) {}
37 public withdraw(to_withdraw: &mut R#Self.T, amount: u64): R#Self.T {}
38 // 將Token從交易發起者轉到收款人
39 public transfer(payee: address, amount: u64) {}
40}


a) Token Resource

整個 Token module 的結構如上。定義這個 Token 的 resource T {value: u64} 代表了未來每個帳戶將會持有多少數量 (T.value) 的 token,也要定義兩個跟 T 相關的輔助函數 zero() 製作一個數量為零的 Token.T,value() 回傳該 Token.T 的實際數值。

b) Publish

如同 Capability 一樣,每個帳戶都是分別持有自己的 resource。Libra 的設計邏輯中並不允許在沒經過某帳戶的同意下為其增加額外的 resource,不像以太坊中只要有地址就可以收到別人的轉帳。因此,我們需要一個輔助函數供 Token 的所有者調用,為他們建立 Token.T 的 resource。這是 Publish 負責的事情。

 1// 為交易發起者 publish 一個初始餘額為0的 token
2
3 public publish() {
4 let t: R#Self.T;
5 // 建立一個新的數值為0的 Token.T
6 t = Self.zero();
7 // 將 Token.T 轉移到交易發起者的帳戶下
8 move_to_sender(move(t));
9 return;
10}


c) Minting

讓帳戶擁有 resource Token.T 的下一步便是發送一些 token,因此接下來將具體解釋 mint 功能如何實現!

 1public mint_to_address(payee: address, amount: u64) {
2 let capability_ref: &mut R#TokenCapability.T;
3 let mint_token: R#Self.T;
4
5 // 使用 TokenCapability 來確保只有所有者有權限可以增發 token
6 capability_ref = TokenCapability.borrow_sender_capability();
7
8 // 呼叫下方的 mint() 來建立數量為 amount 的 Token.T
9 mint_token = Self.mint(copy(amount), move(capability_ref));
10
11 // 將 mint 出來的 Token.T 合併到收款人的名下,這個函數我們在下面解釋。
12 Self.deposit(move(payee), move(mint_token));
13 return;
14}
15
16mint(amount: u64, capability: &mut R#TokenCapability.T): R#Self.T {
17 // 為確保只有交易發起者擁有 TokenCapability.T,直接發布 resource 即可。
18 release(move(capability));
19
20 // 建立一個有 amount 數量的 Token.T
21 return T{value: move(amount)};
22}


增發 token 時,我們應先確保 sender 有增發的權限,如果沒有這個權限,transaction 便會失效;然後建立要增發給 payee 的Token.T,最後通過 Token.deposit 函數將新建的Token.T 與 payee account 下的 resource Token.T 合併。

d) Balance

增發 token 後,還缺乏查詢名下 Token 數量的方法,這就需要撰寫 balance 了!

 1public balanceOf(account: address): u64 { 
2 let token_ref: &mut R#Self.T;
3 let token_const_ref: &R#Self.T;
4 let token_val: u64;
5
6 // 從該帳戶下取得 resource reference
7 token_ref = borrow_global(move(account));
8
9 // 因為我們沒有計劃改動 resource 的數值,因此把可變的 reference 凍結,改成不可變的 reference
10 token_const_ref = freeze(move(token_ref));
11
12 // 調用 value() 來取得實際的餘額
13 token_val = Self.value(move(token_const_ref));
14 return move(token_val);
15}
16
17
18// 這個 balance() 是直接包裝 balanceOf() ,提供交易發起者一個簡單的接口可以查詢。
19public balance(): u64 {
20 let sender: address;
21 let balance_val: u64;
22 sender = get_txn_sender();
23 balance_val = Self.balanceOf(move(sender));
24 return move(balance_val);
25}


e) 轉帳 Transfer

重頭戲當然是轉帳,transfer 一共分為三個步驟:

  • 從交易發起者借用 resource Token.T;
  • 將交易發起者的 resource Token.T 分割成要轉帳的部分與餘額 (由 withdraw 函數負責);
  • 將交易發起者轉帳的部分與付款人的 resource Token.T 合併 (deposit 函數負責)。


因此整個 transfer 函數如下:

 1public transfer(payee: address, amount: u64) {
2 let to_pay: &mut R#Self.T;
3 let sender: address;
4 let to_withdraw: R#Self.T;
5
6 sender = get_txn_sender();
7
8 // 借用交易發起者的 resource Token.T
9 to_pay = borrow_global(move(sender));
10
11 // 分割出要給收款人的部分
12 to_withdraw = Self.withdraw(move(to_pay), move(amount));
13
14 // 將要給收款人的部分與收款人帳戶下原有的 Token.T 合併
15 Self.deposit(move(payee), move(to_withdraw));
16
17 return;
18}


而 Withdraw 與 Deposit 實現如下:

 1public deposit(payee: address, to_deposit: R#Self.T) {
2 let deposit_value: u64;
3 let payee_token_ref: &mut R#Self.T;
4 let payee_token_const_ref: &R#Self.T;
5 let payee_token_value: u64;
6
7 // 取出要合併的數值
8 T{ value: deposit_value } = move(to_deposit);
9
10 // 獲得付款人的 Token.T reference 與現有的數值
11 payee_token_ref = borrow_global(move(payee));
12 payee_token_const_ref = freeze(copy(payee_token_ref));
13 payee_token_value = Self.value(move(payee_token_const_ref));
14
15 // 修改付款人的 Token.T 的數值
16 *(&mut move(payee_token_ref).value) = move(payee_token_value) +
17 move(deposit_value);
18 return;
19}
20
21public withdraw(to_withdraw: &mut R#Self.T, amount: u64): R#Self.T {
22 let value: u64;
23
24 // 取得交易發起者的 Token.T 數量,並確認是否足夠支付這次轉帳
25 value = *(&mut copy(to_withdraw).value);
26 assert(copy(value) >= copy(amount), 10);
27
28 // 修改交易發起者的 Token.T 數量,並將分割後的 Token.T 轉出去
29 *(&mut move(to_withdraw).value) = move(value) - copy(amount);
30 return T{value: move(amount)};
31}


3、測試 module

一個 mvir 的檔案含有兩個區塊,分別是 modules 與 script,在 modules 中會撰寫交易中需要部署的所有 modules,script 是在這次交易中我們想執行的程序。

a) Test Script

在我們的範例中,通過使用交易 script 的區塊來進行測試。在這個測試中,我們把交易發起者作為所有者,並且 mint 1314 個 token 給交易發起者,最後檢查交易發起者的餘額是否跟 mint 的數值:1314 一致。

 1script:
2import Transaction.TokenCapability;
3import Transaction.Token;
4main() {
5 let sender: address;
6 let balance_val: u64;
7 let sender_balance: u64;
8 sender = get_txn_sender();
9
10 // Grant owner\\'s capability
11 TokenCapability.grant();
12
13 // Publish an Token account
14 Token.publish();
15
16 // Mint 1314 tokens to the owner
17 Token.mint_to_address(copy(sender), 1314);
18
19 // Check balance == 1314
20 balance_val = Token.balanceOf(copy(sender));
21 assert(copy(balance_val) == 1314, 2);
22 sender_balance = Token.balance();
23 assert(move(sender_balance) == move(balance_val), 1);
24 return;
25}


b) 測試

在撰寫完 modules 與 script 後,依據 Libra 團隊的建議,需將檔案放入 "language/functional_tests/tests/testsuite/modules/" 下,並執行 "cargo test -p functional_tests ",Libra 就會加載並將執行剛才所撰寫的合約,結果如下圖:


編譯、部署到 local testnet


如今 Libra testnet 尚未開放直接部署 modules,只能通過建立自己的 local testnet 來進行測試。現在部署的工具還在非常早期的階段,對開發者的使用上也不是十分友好,以下是整理後的部署流程

1、編譯 Libra 後,可以在 "targe/debug/" 資料夾下找到 compiler 和 transaciton_builder 兩個工具;

2、通過使用 compiler 將撰寫的 mvir 編譯成 program,

"./target/debug/compiler -o ";

3、通過 transaction_builder 把 sender, program、argument 等封裝成 raw transaction,

"./target/debug/transaction_builder --args []";

4、最後進到 libra cli 中使用

submit

對 Libra cli 發出交易。

註:我們也編寫了幾筆交易的 scripts 用來操作 Token

請參考此連結:

https://github.com/second-state/libra-research/tree/master/examples/ERC20Token/transaction_scripts


部署與使用 Token 的順序

1、先將 token.mvir (含有 Token、TokenCapability 的 module) 部署到 Libra;

2、要想使用該 token 帳戶,必須先調用 init.mvir 將 Token.T 發布到帳戶的 resource 中;

3、所有者可通過 mint.mvir 給其他擁有 resource Token.T 的帳戶增發 token;

4、兩個擁有 resource Token.T 的 account 可以通過 transfer.mvir 進行 token 轉移。

開啟允許部署 modules 的權限


Libra 在編譯時期 (compilation-time) 時從 genesis file 裡面讀取是否可以設定部署 modules 的權限。因此,為把 modules 部署到 local testnet,我們需要在編譯前修改這項設定。

在 "language/vm/vm_genesis/genesis/vm_config.toml" 這個檔案,只需將 "[publishing_options]" 中的 "type=Locked" 改為 "type=Open" 即可。更改完以後,一定要重新編譯使設定生效。

文章來源: https://twgreatdaily.com/zh-mo/eOYBDmwBmyVoG_1ZxFH3.html