Learn how to use the Michelson programming language and write smart contracts on Tezos
(Please check first Part 1 and Part 2)
In this new installment of our series about Michelson language, we are going to crank it up a notch!
In the last parts, we were having a quite simple stack and doing some basic manipulations, adding elements, removing them, duplicating them, etc. However, one of the powers of smart contracts is access-control: you can request your smart contract to verify if the person sending a request is allowed to modify the storage. If they are allowed, they can continue performing their operation. If they are not, the execution of the smart contract stops and all subsequent code is ignored.
Unlike Ethereum smart contracts, smart contracts on Tezos do not modify the storage at any point in the execution of the transaction. The new storage is returned at the end of the execution if everything went well. This makes smart contracts on Tezos extremely secure: Michelson will make sure that everything is executed as it should before modifying the storage. Solidity will modify the storage and hope there will be no problem down the road 😊
In this article, we are going to write a smart contract that verifies that the sender of the transaction is allowed to modify the storage. If they are, we are going to concatenate the string they provide with the string in the storage. If they are not, the contract will just stop executing and return an error.
The whole code of the smart contract is available in the Jupyter notebook binder. After navigating to the page and waiting a minute for the Michelson kernel to load, select MichelsonTutorial-Demo4.ipynb to see the code. As for the last lesson, you can see the inline code or run the code step-by-step.
Let’s have a look at the inline code:
It is amazing how much power such a small piece of code packs 😅
The storage looks different this time: it contains a pair with an address on the left side and a string on the right side. We are going to use the address in the storage to compare it to the sender’s address and give/refuse access.
You probably recognized a few opcodes that were introduced in the previous lessons: DUP, CAR, PAIR, NIL, etc. There are also new instructions: DIG, SENDER, IFCMPEQ, FAIL, etc. No worries, we are about to see what they do.
As usual, you are free to play with the step-by-step code in the Jupyter notebook so you can get a better understanding of what the code does and how it modifies the stack at each step.
Now let’s go line by line and see what the code does!
First, a pair containing the provided parameter and the storage is pushed onto the stack:
DUP
We want to duplicate the first element of the storage. As you are probably beginning to understand, this is a normal step to separate the parameter from the storage when you need both in two different elements:
CAR
Remember CAR? It takes the pair at the top of the stack and extracts its left value:
DIP { CDR ; DUP ; CDR }
This one is a very interesting and useful instruction! You can see that the instruction is made of two parts: the DIP part and the part between curly braces.
The DIP instruction “protects” the element of the stack that it refers to. If there is no number after DIP, it will protect the first element on the top of the stack. You can also use DIP 2 or DIP 10 according to the number of elements. The DIP instruction is followed by some code between curly braces that will affect the element below the element protected by the instruction. As such, if you write DIP 0 { code }, it would be as if you wrote code and if you write DIP 1 { code }, as if you wrote DIP { code }:
The code between curly braces should have no secret for you: it will extract the right part of the pair (CDR) which is also a pair, duplicate it (DUP) and extract its right part (CDR):
DIG 2
Here is another very useful instruction! The DIG instruction moves an element to the top of the stack and is always followed by a number which indicates the index of the element you want to bring to the top (remember that the index starts at 0 in Michelson, so “element 2” will actually be the third one from the top):
CAR
Now, we have our storage on top of the stack and we can continue working with it. We want to extract the address saved in the storage to compare it with the sender’s address. The CAR instruction will get the address that is located in the left part of the storage pair:
DUP
You may be wondering why we need to duplicate the address we have just extracted from the storage. When we are going to compare this address with the sender’s address, they will both be removed from the stack. As we want to keep the address in the storage for the next transaction, we copy it now and keep a copy that we will save in the storage later:
SENDER
Another new opcode! SENDER pushes the address that initiated the current transaction on top of the stack. There is a subtle difference in Tezos smart contract between two instructions: SENDER and SOURCE. SENDER will be the address that created the current transaction, which means that if your smart contract receives a transaction from another smart contract, SENDER will be that smart contract address. SOURCE is always an implicit account address (i.e the account from which the very first transaction was initiated).
In this case, it doesn’t matter much, but it can be important in other situations:
IFCMPEQ {} { FAIL }
Welcome to your first conditional structure in Michelson 👏🏻IFCMPEQ stands for “IF CoMPare EQual” and will tell you if the two values on top of the stack are equal. Note that the two values have to be of a comparable type and of the same type.
After the IFCMPEQ instruction, you can see two blocks, each surrounded by curly braces. If the result of the comparison is true, the code in the left block will be executed. If it is false, the code in the right block will be executed.
The only thing we want to do here is to make sure the two addresses are the same, so we want to make the contract fail if they are not. We are not going to run any code if they are the same, the contract will just continue its execution on the next line. The FAIL opcode will cease the execution of the contract and return an error.
After the comparison is done, the two values that were at the top of the stack are removed:
DIP { SWAP ; CONCAT }
Another DIP instruction! We verified that the user who sent the transaction is allowed to alter the storage, so we can concatenate the string they provided with the one that’s in the storage!
First, we want to protect the address at the top of the stack that we keep for later to save it in the new storage. The DIP instruction will just do that.
Next, we want to reorder the two strings that we have in our stack to have “Hello ” before “world”. This is a job for the SWAP opcode. Once they are in the right order, we can concatenate them with CONCAT. Because we used DIP and put the next two instructions between curly braces, the code will ignore the first element of the stack and work with the ones below:
PAIR
I guess you are more familiar with the last steps of smart contracts in Michelson now! Remember the very first step, we had a storage made of a pair containing an address on the left and a string on the right. You have to recreate the same kind of setup to exit your smart contract. It will be very easy because we have now an address followed by a string in our stack!
Using the PAIR instruction, we create a new pair whose left element is the element at index 0 on the stack and whose right element is the one at index 1:
NIL operation
This instruction should have no secret for you now! We push an empty list of operations on top of our new storage:
PAIR
This is now the end of the transaction. We create a new pair containing a list of operations (which is empty here) and the storage (a pair with an address and a string):
When the stack only contains a pair with a list of operations and the storage, we are done and the execution can end successfully 🥳
You can also visualize the whole process of the execution of the smart contract in this video:
This ends Part 3 of our tutorial about Michelson language. It introduced a few new instructions that will be extremely useful in your smart contracts: DIP, DIG, IFCMPEQ, FAIL, SENDER, etc.
You now have a better understanding of the general flow of a smart contract written in Michelson, how the different elements move and work with each other. The conditional structures are a very powerful tool to check the data entering the smart contract through transactions, for example, if the sender is allowed to perform an action, if the right amount of tezzies has been sent, etc. DIG and DIP are also essential instructions as we were restricted to the very top of the stack in the previous tutorials, but these instructions allow us to manipulate elements deeper in the stack.
In the next tutorial, we will continue exploring the capabilities of Michelson, introduce new opcodes and dive into control flow and domain-specific data types.
Stay tuned!