Libra硬刚微信、支付宝? 你也试试!

Libra硬刚微信、支付宝? 你也试试!

区块链大本营 发布在 海盗号 38262

本月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<T>(move(t)); 17    // 将 TokenCapability.T 转移给所有者。 18    return; 19}

从上面的代码中我们可以发现,只有通过 "sender == owner" 检查才能取得所有者的 resource T,因此我们可以确保 resource T 只会被所有者所拥有,其他的账户都没有机会获得这个 resource T。

此外,"move_to_sender<structure type>(resource)" 是 Move IR 提供的内建函数,它代表了将 resource 移交给交易发起者的账户。

b) Borrow_sender_capability()

如何检查并确认交易发起者拥有所有者的 resource?

借助 Move IR 提供的辅助函数 "borrow_global<structure type>(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<T>(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<T>(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<T>(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<T>(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<T>(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 <file name>",Libra 就会加载并将执行刚才所撰写的合约,结果如下图:

编译、部署到 local testnet

如今 Libra testnet 尚未开放直接部署 modules,只能通过建立自己的 local testnet 来进行测试。现在部署的工具还在非常早期的阶段,对开发者的使用上也不是十分友好,以下是整理后的部署流程

1、编译 Libra 后,可以在 "targe/debug/" 资料夹下找到 compiler 和 transaciton_builder 两个工具;

2、通过使用 compiler 将撰写的 mvir 编译成 program,

"./target/debug/compiler -o <output_file_name> <input.mvir>";

3、通过 transaction_builder 把 sender, program、argument 等封装成 raw transaction,

"./target/debug/transaction_builder <sender_address> <sequence_number> <path_to_program_file> <output_transaction_file_name> --args [<Arguments>]";

4、最后进到 libra cli 中使用

submit <sender_address/sender_account_ref> <path_to_transaction_file>

对 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" 即可。更改完以后,一定要重新编译使设定生效。

参考链接:

1、Singleton Pattern Capability:

https://community.libra.org/t/any-notion-of-global-variables/355/2

2、TokenCapability:

https://github.com/second-state/libra-research/blob/master/examples/ERC20Token/token.mvir#L3

3、Token Source Code:

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

 

作者 | Second State

责编 | 乔治

出品 | 区块链大本营(blockchain_camp)

文章标签: Facebook Libra
评论
登录 账号发表你的看法,还没有账号?立即免费 注册