How to extend an FA2 contract in CameLigo

0
239

A quick tutorial on how to add extra features to an FA2 contract

Image from Pixabay

I decided to work on a little project during the weekend and try the updated version of the contracts offered by TQ Tezos. These contracts implement the basic features of an FA2 contract according to the TZIP-12 standard. So I made some coffee, opened VSCode and started diving into the code written in CameLigo (the whole thing being made a lot easier now with the new VSCode extension from Ligo that allows syntax highlighting within the editor). While I was reading it, I thought it could be interesting to document the thought process and the different steps involved in extending an FA2 contract or even any contract written with Ligo for that matter. As it turns out, it is a lot easier than you may think and you can succeed with a minimal debugging if you observe the code attentively first.

For this short tutorial, we will extend the multi-asset contract by adding a “mint” function to create new tokens (the FA2 standard doesn’t describe a unique way of creating tokens, this is left to the developers’ discretion). This is in general the kind of thing you want to do when you implement a token yourself.

So get some coffee too and join me ☕️

Downloading the repository

First things first, we have to download the code. Go to the implementation of the FA2 contracts by TQ Tezos, click the green button with “Code” on it and copy the URL. Next, create a new folder, open a new terminal or iTerm window and type git clone + the URL you’ve just copied. It should take a couple of seconds to download. Then, open the folder in VSCode, it should look something like this:

The repo structure

For this tutorial, we are going to hack into the multi_asset folder but as you can see, the repository contains other kinds of FA2 contracts you can play with. Now, navigate to multi_assetligo.

You will find a file called “fa2_multi_token.mligo”. This is the first one we are going to check. At the bottom of the file, you can find the fa2_main “real” entrypoint with the pseudo entrypoints below. At the beginning of the file, you can see the storage structure:

type multi_token_storage = {
ledger : ledger;
operators : operator_storage;
token_total_supply : token_total_supply;
token_metadata : token_metadata_storage;
}

This defines the ledger (where the address/balance pairs are kept), the operators (who is allowed to transfer tokens on behalf of who), the token_total_supply and the token_metadata (providing more information about each token).

Gathering information about the contract

Before starting to write code, we have to stop a minute and observe the structure of the contract we are working with. Indeed, minting tokens implies adding new data into the contract, so we have to understand how the contract stores and manipulates its data. Let’s check the storage first!

The ledger (the place where you store the addresses and their balance) has the following structure:

type ledger = ((address * token_id), nat) big_map

It’s a big map whose keys are pairs of address and token_id (which is itself a nat value) and whose values are nat. Now we know that the balances will be identified by the address of their owner and the token ID they refer to.

type token_total_supply = (token_id, nat) big_map

The token_total_supply is also a big map where the keys are the token ID (a nat value) and the values are nat. This makes sense because we are working with a contract that can store and manipulate multiple fungible tokens at once.

We can also look at how the contract stores the token metadata:

type token_metadata_storage = (token_id, token_metadata_michelson) big_map

This is also a big map where the token ID is used to match the metadata. The token_metadata_michelson type is just a conversion from a record you can find in fa2_interface.mligo:

type token_metadata = {
token_id : token_id;
symbol : string;
name : string;
decimals : nat;
extras : (string, string) map;
}
type token_metadata_michelson = 
token_metadata michelson_pair_right_comb

As you would expect, the token metadata are pretty standard, but we will have to take into account the fact that they are converted to a Michelson type before being stored. We are also going to add a new field, admin : address, where we will keep the address of the token creator.

Setting up the contract

It’s time to write some code 👨‍💻

According to the behaviour you want from your token minter, you will have a lot of different options to mint tokens. For this tutorial, we will let anyone create and mint new tokens with a limited supply.

Let’s start by creating a new empty file in the fa2 folder called “mint_tokens.mligo” and write the function declaration:

This is a simple function that only returns the storage, for now, that will help us test the setup before we start coding the logic inside.

The function takes an argument of type mint_tokens_params that contains different values we need to mint a new token. We will create this type in the fa2_interface.mligo file. It is important that you place the new type carefully inside the file so that you can have access to the types that were declared before. We will put it just before the type fa2_entry_points =:

We will need all the details above to create a new token and we convert it to a Michelson pair to enforce the right structure (otherwise this could cause unexpected behaviours).

Let’s just add our new entrypoint Mint_tokens right now as the type for the entrypoints is just below:

Let’s go back to the mint_tokens function!

Minting tokens

Let’s pause a minute to think about what the minting function is going to achieve. It takes in all the parameters described in mint_tokens_params and updates the ledger, the token_total_supply and the token_metadata parts of the storage. The user will pass the token ID, the contract will verify it doesn’t already exist and will create it with all the provided data.

So first, let’s check if the token ID doesn’t exist in the contract:

Note: During development, I generally return the expected value at the end so that the interpreter doesn’t return errors about that and focuses on errors in the code. You are also free to write the whole function in one shot and debug it step by step.

We use Big_map.mem to determine if the token ID doesn’t already exist in the token_total_supply big map. If it does, the contract fails. Otherwise, it continues creating the token.

Now that we have checked the token ID, let’s create the metadata:

First, you may notice that we created a new type above that will make our code clearer. For the metadata, we will just fill a token_metadata record with the information gathered from the parameters. We add our new token ID and various details like the symbol and the name of the token. We also add the Tezos.sender address as the admin of the token. The extras field is a bit special: we let our users pass a list of tuples containing 2 strings to populate the extras field as they please but ultimately, this field is a map, so we use List.fold and the make_extras function to loop through the list and create the extras map we need.

After the record is created, we convert it to a right-combed structure (because the storage demands it) before adding our new token metadata into the token_metadata field of the storage.

Next, we create the new token supply:

If you remember from earlier, the total supply is a big map with keys representing the token ID and values indicating its total supply, so we just update our token_total_supply big map with the token ID and total supply.

For the last step in updating the storage, we add the new account in the ledger with the indicated total supply:

Finally, we can return the updated storage:

Nothing complicated here, we return the storage with the new ledger, the new token supply big map and the new token metadata big map.

And that’s it! You’ve successfully extended an FA2 contract with a minting function 🥳

Wrap up

Extending an existing contract always seems like a daunting task, you have to carefully plug in your code without breaking anything. However, it doesn’t have to be and it is actually pretty simple. You just have to observe the way the code was structured and use the existing types or values to create your own code. Place your #include statement in the right position and here you go, you created a new contract! Now the possibilities are endless!

Acknowledgements

This tutorial wouldn’t have been possible without Eugene Mishura’s work in the TQ Tezos repository where his well-crafted contracts allow other developers to save a lot of time!

Get Best Software Deals Directly In Your Inbox

How to extend an FA2 contract in CameLigo was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.