This article is the third and final part of the “Libra First impressions” series. In the previous two articles, we explored the technical implications of the Libra project and the internal processing and execution of Libra Client and Validator.
- Libra First Impressions Part 2: Deep dive with a Libra transaction
- Libra First Impressions Part 1: Smart Contracts are not created equal
Write your token module
In this article, we will show you how to use Move IR to write a simple token module. For easier understanding, we chose Ethereum's ERC20 token as an example, and we focus on three kinds of core functions in this article: mint
, balanceOf
, and transfer
.
Before we start, we need to explain the logic of Libra’s processing of resource. Different from Ethereum with its global state, Libra doesn’t have a centrally stored global resource. Instead, it has resources distributed under different accounts. Therefore, using a storage variable like address storage owner = 0x..
in Ethereum smart contract should be implemented with different logic. Also, everyone’s token balance is stored in the resource of respective accounts instead of using mapping(address=>uint256)
to process in a centralized way.
1. Capability
Right now, Libra developers’ team’s recommended way of dealing with a global variable is to use a singleton pattern’s module.
Therefore, we define the possession of the owner as a resource that can only be published once, such as the following resource T{}
. And we will have two methods to operate on this T, the grant()
executed at initializing stage to ensure that the Token Capability is handed over to the owner; the borrow_sender_capability()
is to check if the operator is authorized as an Owner.
|
|
a) grant()
function
Before we implement grant
function, we need to define two roles: The transaction sender calling the function and the actual owner. But regretfully, right now the Move IR doesn’t provide Self.published_address
to allow us to access this module’s account. Therefore, we can only hardcode the module owner’s address in the source code, like in the following code:
|
|
From the above program, we can see that only if the sender == owner
can the resource T of the owner be had. So we can ensure that the resource T will only be owned by the owner, and other accounts will not be able to obtain resource T.
Also, move_to_sender<structure type>(resource)
is a built-in function provided by Move IR, which represents the transfer of the resource to the sender account.
b) borrow_sender_capability()
function
And what kind of check should be done to confirm that the transaction sender has the resource of the owner? We will use another built-in function borrow_global<structure type>(resource account)
provided by Move IR. borrow_global
will retrieve a reference related to the resource. If the resource is not held under the account, an exception will be triggered, causing the transaction to fail. If successful, it returns a mutable resource reference.
|
|
2. Token
We have discussed the authorization managing method. Next, let's talk about the Token Module!
|
|
a) Token Resource
The entire Token module will be the above structure; we will first define the resource T {value: u64} of this Token, representing the balance(T.value) of tokens that each account will hold in the future. Two helper functions related to T are also defined: zero() makes a Token. T with zero quantity and value() returns the actual value of the Token.T.
b) Publish
Like Capability, each account holds its own resource separately and Libra's design logic does not allow additional resources to be added without the consent of this account, unlike in Ethereum, where airdrop can happen with just knowing the address.
Therefore, we need to provide a helper function for our token user to allow our token user to call and create Token.T resource for themselves. This is what Publish does.
|
|
** c) Minting **
The next step of allocating account with resource Token.T is to mint some token. So let’s look at how the mint
function is implemented.
|
|
In the process of minting tokens, we will ensure that the sender has the right to mint. If not, this transaction will fail. Then create the Token.T to be issued to the payee. Finally, the Token.T newly created by the Token.mint function is merged with the resource Token.T originally under the payee account by Token.deposit function.
d) Balance
After issuing the token, we don’t yet have a way to query the amount of the tokens. So let's write the balance!
|
|
e) Transfer
Finally, we came to the most important step: transfer. To implement transfer in Libra, we need to split transfer operation into three steps:
- Borrow the ownership of resource Token.T from Sender.
- Split Sender's resource Token.T into two parts to be pulled and the rest. (See the withdraw function for more details)
- Merge Sender's extracted resource Token.T with Payee's resource Token.T. (See the deposit function for more details)
So the entire transfer function is implemented like this:
|
|
withdraw()
and deposit()
are implemented as follows:
|
|
3. Test our module
A mvir file will contain two sections: module, and script. In the module section, we will write all the modules we want to deploy in this transaction, and the script is the program we want to execute in this transaction.
a)Test Script
In our example, we will test by using the section of the transaction script. In this test, we use sender as the owner and mint 1314 tokens to the sender and check if the sender's balance is consistent with our mint's value 1314.
|
|
b) Test modules
After writing the complete modules and scripts, the easiest way for testing our modules is following the Libra team's suggestion: put the mvir file under language/functional_tests/tests/testsuite/modules/
and execute cargo test -p functional_tests <file name>
, Libra will load the contract we just wrote and list the results on the screen.
As shown below:
Compile and deploy to local testnet
Because Libra testnet is not yet open to be deployed with modules directly, this can only be explored by setting up one’s own local testnet. The tools deployed today are still immature, and not very developer-friendly. The following is the deployment process we sorted out.
- After compiling Libra, you can find two tools under the
targe/debug/
folder: compiler and transaciton_builder. - We need to compile the mvir into a program through the compiler. Command: `./target/debug/compiler -o <output_file_name> <input.mvir>
- Then package the sender, program, argument, etc. into a raw transaction via transaction_builder. Command:
./target/debug/transaction_builder <sender_address> <sequence_number> <path_to_program_file> <output_transaction_file_name> --args [<Arguments>]
- Back to libra cli and use
submit <sender_address/sender_account_ref> <path_to_transaction_file>
to send a transaction to Libra cli.
Use scenario: We also wrote several transaction scripts for Token operations, please refer to this link: https://github.com/second-state/libra-research/tree/master/examples/ERC20Token/transaction_scripts
The process of Token deployment
- First of all, you need to deploy token.mvir (contains two modules: Token, TokenCapability) to Libra.
- Before using Token, users should call init.mvir to publish Token.T to their account resource.
- The owner can mint tokens to other accounts via mint.mvir.
- Two accounts with resource Token.T can transfer tokens via transfer.mvir.
Enable publishing modules to local testnet
By default, Libra disables the module deployment. And libra will read this property from the genesis file at compilation time. In order to deploy modules to local testnet, we have to modify this setting before compiling.
In the language/vm/vm_genesis/genesis/vm_config.toml
file, modify the type=Locked
of [publishing_options]
to type=Open
. If you've compiled libra, don't forget to recompile after the change to enable the settings.
Reference Link: