Implementing a FA1.2 token in pure Michelson (Part 1)
Take your Michelson skills to the next level with a more complex project
In the last tutorial about Michelson programming language (the miniTez token), we created a simple token that was just transferring an amount of tokens from one account to another with minimal verifications.
The need for standardizing token interactions on Tezos has driven the community to create token standards that indicate how the tokens behave and how they can be interacted with. This has been the goal of the TZIP-7 proposal. After reading the miniTez tutorial, it should be very easy for you to understand what the TZIP-7 proposal adds to basic token transfer. We will explain the different points it makes and their motivation.
After understanding the requirements for an implementation of the TZIP-7 standard will come the time of getting our hands on the keyboard to write some Michelson. Although the code will be more complex, it is still based on common instructions and nothing dramatically difficult. You can check the code in the Github repository to give you an idea of what we are going to write. The contract we build will strictly follow the TZIP-7 proposal, so there are no entrypoints for minting tokens or other functionalities. However, the standard can be extended and you are free to add them yourself!
For this tutorial, we will use the notebooks with the Michelson kernel developed by Baking Bad as it will give us the flexibility we need to test our code while writing it. The tutorial will be divided into multiple parts: this first part is an introduction to the TZIP-7 standard and presents the values used for the parameter and storage of our smart contract. The different entrypoints of the contract require their own articles, which will be the subject of the following parts of this tutorial.
The TZIP-7 proposal
I previously wrote an article about the TZIP-7 standard so I won’t get into the details of the proposal here, but rather into the details of its implementation in Michelson.
The TZIP-7 proposal requires that the tokens implementing it have the following interface:
(address :from, (address :to, nat :value)) %transfer
(address :spender, nat :value) %approve
(view (address :owner, address :spender) nat) %getAllowance
(view (address :owner) nat) %getBalance
(view unit nat) %getTotalSupply
This interface only introduces the entrypoints of the contract but implies many things:
(address :from, (address :to, nat :value)) %transfer: the
transferentrypoint must receive a
addresson the left representing the account where the tokens will be taken from and a
pairon the right containing an
addresson the left representing the recipient of the tokens and a
naton the right representing the amount of tokens to be transferred. This doesn’t imply that the address requesting the transfer must be the same as the
:fromaddress, so transfers can also be initiated on behalf of another address.
(address :spender, nat :value) %approve: the
approveentrypoint must receive a
pairas well, with the address of the account to be approved on the left and the value granted to this account on the right. This implies that the address setting the approval has to be the owner of the account.
(view (address :owner, address :spender) nat) %getAllowance: the
getAllowanceentrypoint is marked as
view, which means that the entrypoint will be used by other contracts and will return a transaction containing the requested allowance. The entrypoint accepts a
paircontaining on the left another
pairwith the owner’s address on the left and the spender’s address on the right and on the right of the root pair, the set allowance as a
(view (address :owner) nat) %getBalance: the
getBalanceentrypoint is also a view entrypoint that will return a transaction. It expects a pair with an address on the left and a
natvalue on the right.
(view unit nat) %getTotalSupply: The
getTotalSupplyentrypoint is a view entrypoint that takes a
unitvalue and returns a value of type
Although being straightforward, the standard implies a few things:
- There must be a
totalSupplyvalue in the storage. The TZIP-7 proposal doesn’t enforce any type of storage but it implies that a contract implementing it must keep track of the total supply of tokens (otherwise the
getTotalSupplyentrypoint doesn’t have anything to return).
- The view entrypoints take a value of type
contract natinstead of simply
nat. Because the entrypoint has to return a transaction to the contract that requested the value, the
natvalue will actually be a
contract natvalue, a reference to a contract (and possibly its entrypoint) expecting a value of type
- The different values passed as parameters must be annotated. This is particularly important for the
transferentrypoint where we get two addresses, one that will lose tokens and one that will gain tokens.
The Michelson code
For this contract, the parameter is going to be a little more complex than what we are used to. Let’s have a look at it first:
This contract introduces a new type of value we haven’t encountered before: the
union type. You can imagine a value of type
union as a pair where only one side can contain a value at all time. Although this looks like a waste of space, this is actually very useful for conditional branching as you can change the behaviour of the contract according to the side that holds a value.
Imagine a value of type
union that looks like this:
(or int string). This tells us that if there is a value on the left side, it will be of type
int and if there is one on the right side, it will be of type
string. Now, if you want to set the value, you just have to indicate the side you want to fill and the value it should hold, for example,
(Left 6) or
(Right "hello"). When your contract will encounter this value, you can change its behaviour if an
int is present on the left or a
string on the right.
This pattern is the one used in Michelson to simulate entrypoints. Technically speaking, a Michelson contract only has a single entrypoint but it is possible to simulate more with values of type
union. In this contract, we create the
transfer and the
approve entrypoints as two sides of the same union value. The annotations, in addition to being required by the standard, are also essential to third-party applications like Taquito that depend on them to display in a friendlier way the available entrypoints.
Values of type
union can also be nested, which will be very valuable to create more than 2 entrypoints. As a design choice, I decided to put the entrypoints that modify the storage on the left side of the main union value and the view entrypoints on the right side.
In addition to the
union type, you can see that the values are annotated, which means that they are given a name through a special notation. Strings that start with
@ are annotations. Michelson has different types of annotations according to the function of the value you want to annotate, whose explanation would require its own tutorial. Just observe the annotations in this example and try it out in your own code.
The FA1.2 contract doesn’t require a complex storage, as it only needs to track users’ balances and approvals, as well as the total supply.
Once again, you can see that the values are annotated, which makes it easier to read not only for you but also for external tools like Taquito.
The storage is made of a pair with a
big_map on the left and a
nat value representing the total supply on the right. The big map will keep track of the users’ accounts, both of their balances and of the allowances they set. This kind of big map is generally called
ledger so we will go with this name too.
The notebook setup
We must prepare a basic setup in order to use the notebooks. First, we want to run the contract as we write, so we use the
BEGIN instruction after the parameter and storage declarations to achieve that. The
BEGIN instruction takes three arguments: the entrypoint we target, a value for the parameter and a value for the storage. Next, we are going to test functionalities that prevent people who are not the account’s owner to do certain actions, so we will use the
SENDER instruction in Michelson. As you can imagine, there is no sender if there is no transaction, so we have to simulate it in the notebooks. You can simply add
PATCH SENDER "tz1..." ; and the address of your choice to do it.
As the contract grows, you may prefer hiding all the information printed out about the stack manipulations. You can achieve it with
DEBUG False ; right at the beginning.
Next step: the entrypoints code in Part 2!
- The Best Crypto Trading Bot
- Crypto Copy Trading Platforms
- The Best Crypto Tax Software
- Best Crypto Trading Platforms
- Best Crypto Lending Platforms
- Best Blockchain Analysis Tools
- Crypto arbitrage guide: How to make money as a beginner
- Best Crypto Charting Tool
- Ledger vs Trezor
- What are the best books to learn about Bitcoin?
- 3Commas Review
- AAX Exchange Review | Referral Code, Trading Fee, Pros and Cons
- Deribit Review | Options, Fees, APIs and Testnet
- FTX Crypto Exchange Review
- NGRAVE ZERO review
- Bybit Exchange Review
- 3Commas vs Cryptohopper
- The Best Bitcoin Hardware wallet
- Best monero wallet
- ledger nano s vs x
- Bitsgap vs 3Commas vs Quadency
- Ledger Nano S vs Trezor one vs Trezor T vs Ledger Nano X
- BlockFi vs Celsius vs Hodlnaut
- Bitsgap review — A Crypto Trading Bot That Makes Easy Money
- Quadency Review- A Crypto Trading Bot Made For Professionals
- PrimeXBT Review | Leverage Trading, Fee and Covesting
- Ellipal Titan Review
- SecuX Stone Review
- BlockFi Review | Earn up to 8.6% interests on your Crypto