Implementing a FA1.2 token in pure Michelson (Part 2)

Take your Michelson skills to the next level with a more complex project

Claude Barde
Coinmonks
Published in
8 min readAug 26, 2020

--

Image from Pixabay

Part 1 is available here.

In Part 1, we had an overview of the TZIP-7 proposal, we set up our project in the Jupyter notebooks and we checked the structure of the parameter and the storage for our FA1.2 token.

In Part 2, we explore the code for the %transfer entrypoint. It is an important piece of the contract (and the longest one) as it allows or restricts transfers.

Now let’s dive into the code 👨‍💻

The transfer entrypoint

In general, what I like to do at the very beginning, is unwrapping the structure laid down by the parameter. Because the parameter is made of nested union values, there will be some logic switches to implement the entrypoints of the contract.

This is how it looks like:

The entrypoint structure

Notice the DUMP instruction at the bottom? This is not Michelson! It is used in the notebooks to print the state of the stack.

The first thing we do is to separate the parameter and the storage with the UNPAIR instruction. The parameter is made of nested union values with the following structure:

                         parameter
______________|______________
| |
or or
_______|_______ _______|_______
| | | |
transfer approve getAllowance or
_______|_______
| |
getBalance getTotalSupply

In order to implement this structure in Michelson, we will use the IF_LEFT instruction. A union value can only have a value in one of its sides. If there is a value on the left side, the first block following IF_LEFT will be executed. If there is a value on the right side, the second block after IF_LEFT will be executed. This is how entrypoints are simulated.

Let’s start with the most complex entrypoint, %transfer. The transfer entrypoint receives a nested pair, i.e a pair that contains an address on the left and another pair on the right, with an address on the left and a nat value on the right. We want to unpair these nested pairs and get the ledger so we can first verify that the spender exists in the ledger (indeed, it makes no sense to continue if the address we are supposed to deduct tokens from doesn’t exist in the ledger).

The UNPAPAIR macro indicates that we have a pair with a nested pair on the right and we want to unpair both pairs at the same time. From that moment, we will have access to the address with the@from annotation which is the sender of the tokens. We can then duplicate it and use it to verify if the sender has an account in the ledger.

After that, we want to check if the sender’s account has enough balance in order to process the transfer.

As usual, we copy the @from value (because we will use it again later) and get its balance in the ledger. The GET instruction returns an optional value that is None if the key/value pair couldn’t be found or (Some value) with the value associated with the key. Although it is pretty unlikely that we will have a None value here, let’s not take any risk and make the contract fail if it happens. The contract has to fail every time an error happens in order to prevent any unwanted change in the storage.

Now, we have the sender’s balance, so we can check if it is sufficient. We duplicate the amount of tokens to be sent, bring it to the top of the stack and compare it with the balance. If the amount of tokens is greater than the balance (IFCMPGT), the contract fails and has to return the NotEnoughBalance error code as per the standard. Otherwise, we can continue to the last check before the transfer.

At this point, we know that the account from which the tokens will be debited exists and has enough balance. We must make sure that the actual sender of the transaction has been allowed to debit the requested amount of tokens. Only two kinds of users can do it: the owner of the account or an address that has been approved by the owner of the account.

We start by ordering the elements of the stack to verify if the sender of the transaction is also the owner of the account. We use here SENDER instead of SOURCE to allow smart contracts to hold a balance in the ledger. If the owner’s address and the sender’s address are the same, we use empty curly braces {} to tell the compiler to continue with whatever code follows the condition. If they are not the same, we check if the sender’s address has been approved by the owner of the account.

We search for the sender’s address in the allowances map, if we cannot find it, we make the contract fail and return the NotEnoughAllowance error code as per the standard. If we find it, we have to make a last verification and check if the current allowance covers the amount of tokens for the transfer. If it doesn’t, the contract fails, otherwise, we can proceed with the transfer.

The first part of the code processing the transfer is simple: we get the sender’s balance and deduct the amount of tokens to transfer. Remember that the subtraction of 2 nats in Michelson yields an int value, so you have to use ABS to turn it back into a nat before updating the ledger big map.

Next, you may face 2 different situations: the recipient of the tokens may already have an account in the ledger, so we have to increase its balance or it may not have one, in which case we have to create a new key/value pair and set the balance. We use the MEM instruction to check if the key (i.e the recipient’s address) exists in the ledger.

Updating the recipient’s balance

If we found an account, we’ll get its balance. In the unlikely event of getting None from the GET instruction (we know the account exists so there must be a balance, even if it may be 0), we make the contract fail, otherwise, we get the balance, add to it the amount of tokens to be transferred and push it back paired with the allowances map into the recipient’s account.

If there is no account, we simply create one by pushing an empty map for the allowances, pairing it with the amount of received tokens and pushing it into the ledger (with UPDATE).

Updating the sender’s allowance

We are now in the final push of the transfer entrypoint. It is utterly important to update the sender’s allowance in order to avoid unwanted spendings.

We check first if the sender is the owner of the account, because there is then no allowance update to do and we will do some stack cleanup to remove the elements we don’t need to return the entrypoint.

If the sender is not the owner of the account, we fetch the owner’s account details (the balance and the allowances map), we unpair them and search for the sender’s address. We already verified earlier that the sender is allowed to transfer tokens, so no need to do it again. Once we get the allowance, we deduct the amount of tokens we’ve just transferred and put it back in the allowances map (don’t forget to wrap the result in an optional value with SOME before you UPDATE). After that, we can pair the map with the account’s balance (take care of putting the elements in the right order, the balance on the left side of the pair, the allowances on the right side) and push the updated account details back to the ledger! At this point, the stack should be made of the ledger big map and the totalSupply number, so we can use PAIR to pair them together and finally return our new storage 🥳

Conclusion

That’s it! You’ve successfully implemented the transfer entrypoint of a FA1.2 token contract! The Michelson code does a bunch of verifications before actually updating the token balances and allowances, but these verifications are absolutely necessary to ensure the safety of your contract and of your users’ balances.

Remember also that any unexpected value or behaviour should lead the contract to fail in order to preserve the integrity of the accounts in the ledger.

This is the end of Part 2. In Part 3, we will explore the approve entrypoint and the view entrypoints, stay tuned!

Also, Read

--

--

Claude Barde
Coinmonks

Self-taught developer interested in web3, smart contracts and functional programming