Getting started with Ligo

Create smart contracts for the Tezos blockchain using Ligo (Part 1)

Claude Barde
Coinmonks
Published in
10 min readFeb 21, 2020

--

LigoLang

After writing my first article about Liquidity, I had a very interesting exchange of ideas on Twitter. Among others, it was brought to my knowledge that Liquidity was distancing itself from Tezos to work on the Dune network and the future of the language on the Tezos platform may be uncertain.

As I wanted to keep a functional language for my programming (and my general knowledge), I had a look at the other popular functional language for Tezos, LigoLang (or Ligo). It was not my first choice because there is no example of contracts in the online editor, except for a “simple storage” one that we will examine in this tutorial. There is also a tutorial on the website that is probably fine for someone with a good knowledge of OCaml, but that was too unclear for me.

I decided then to take on Ligo in my exploration of the Tezos development ecosystem and write a basic tutorial about it that will help me get a better understanding of the language and (I hope) help others who may stumble on the same obstacles. Like with Liquidity, I will choose the ReasonML syntax, I believe it looks easier for developers like me who come from JavaScript or Solidity.

We will write this contract which is the one available in the editor on Ligo website. The contract increments and decrements a variable in the storage. It’s a good example, as it will introduce the specifics of writing smart contracts in Ligo.

Structure of the contract

The storage

Every smart contract on a blockchain contains a storage, a part of the memory that keeps the state of the contract. Ligo is no exception. The storage is declared as a single type equal to the type of your choice(an integer, a string, a record, etc.)
In this case, we will create the type storage that will receive a value of type int:

type storage = int;

The pseudo entry points

Contracts in Ligo have the particularity of having one single entry point, the main function. Every incoming transaction will be matched against a pattern and redirected to the matching function.
In order to make it work, we must create a variant defining pseudo-multi-entry point actions. A variant is a type containing different cases whose value will change according to the case. This is a very powerful aspect of the language. Here is the example of our contract:

type storage = int;type action =
| Increment(int)
| Decrement(int);

As you can see, we declare a new type called “action”. It can have two values: it can be Increment and receives an integer or be Decrement and receives also an integer.

The functions

Now let’s write the functions for our pseudo entry points.
We will need two functions: one to increment the state and one to decrement it. They must receive two parameters: the first one will be the current storage and the second one is the value to add or subtract to the storage. Note that this is the general pattern in Ligo: you pass the storage to the function, you update it and you return it, there is no accessing the smart contract storage from inside the function. The Ligo documentation calls them “a” and “b” but for more clarity, we will call them “storage” and “count”:

type storage = int;type action =
| Increment(int)
| Decrement(int);
let add = ((storage, count): (int, int)): int => storage + count;
let sub = ((storage, count): (int, int)): int => storage - count;

Let’s have a look at the functions if you are unfamiliar with this syntax:

  1. let add: the function is declared using the “let” keyword followed by the name of the function (add).
  2. ((storage, count): (int, int)): this block indicates that we are expecting two parameters (actually a tuple, but more on that later), the storage and the number to add or subtract to it, both parameters being integers.
  3. : int: this part of the function indicates that we will return an integer, which will be our new storage.
  4. storage + count / storage — count: the “count” variable will be added or subtracted to the storage and it will be returned. Note that no explicit return is necessary.

The real entry point (main function)

Now it’s time we write the real entry point of the contract:

type storage = int;type action =
| Increment(int)
| Decrement(int);
let add = ((storage, count): (int, int)): int => storage + count;
let sub = ((storage, count): (int, int)): int => storage - count;
let main = ((p,s): (action, storage)) => {
let storage =
switch (p) {
| Increment(n) => add((s, n))
| Decrement(n) => sub((s, n))
};
([]: list(operation), storage);
};

This is actually easier than it looks like!

A smart contract in Ligo must include a main function, which will be the “real” entry point. This function takes two arguments: the parameters sent with the transaction and the storage. We will call in this example the current storage “s” and the new storage “storage” to avoid any confusion. The (action, storage) part indicates that the first argument is of type action (the variant we declared earlier) and the second argument is of type “storage” (the smart contract storage).

Next, we will update the storage with a new value. We will then get the new value in a variable called “storage” (our new storage). The next line is where the magic happens!

The switch

It would be a bit too long and outside of the scope of this article to describe all the intricacies of pattern matching in Ligo (and ML languages in general). If you are unfamiliar with this syntax, here is a simple way to understand it: the parameter you send is matched against different options and must be one of the available options. This makes it impossible to have undefined values like in JavaScript.
In this example, “p” is passed to the switch and matched against two options: “Increment” and “Decrement”. These are the only choices available. In addition to that, the contract will make sure that the value passed along is of the type declared earlier in the “action” variant (in this case, an integer). If the parameter matches one of the options, the associated function on the right side of the arrow will be called with the appropriate arguments.
For example, if you call “Increment” and pass “2”, the “add” function will be called and “2” will be added to the storage.

The returned storage

The main function returns two values: a list of operations (two empty square brackets in this case of type “list(operation)”) and the new storage (the value returned by one of the options of the switch). At the end of the transaction, the new storage is saved, given that everything went as planned.

Compiling to Michelson

Now we have to make sure that the contract works and compiles properly. This is very easy to do with the provided interface. On the right side, you will see a column with multiple options:

Compiler step

By default, the “Compile” option should already be selected, you only have to click on the “Run” button. You will get the following code:

If that’s the case, congratulations! You wrote and compiled your first Ligo code 🥳

Improvements

Now let’s improve the contract!

Let’s say we would like to save a name passed as a parameter. Let’s make a list of the changes we will have to make:

  • We have to change the storage type because we must now save two different values of two different types (integer and string).
  • We have to add a new option in our action variant to dispatch the right function and save the name.
  • We have to write the function to save the name in storage.
  • We have to add our option to the main switch.

Let’s do it step by step:

For the storage, we now use a type called “record” that allows us to store data of different types: the “count” field that will hold the integer we increment and decrement and the “name” field that will hold the string we send:

type storage = {
count: int,
name: string
};

We add now a “SaveName” option that accepts a string to the pseudo entry points of the contract:

type storage = {
count: int,
name: string
};
type action =
| Increment(int)
| Decrement(int)
| SaveName(string);

Now, let’s (re)write the functions that will update our state. There are two things to take into account here: first, we must write a new function from scratch to store the name, second, we must update the previous functions that currently return an integer for the storage, and not a record.

The syntax will be very similar for the new function: we pass two arguments, the current storage and the name, and we return the new storage:

let saveName = 
((s, name): (storage, string)): storage => {...s, name: name};

If you know JavaScript, the syntax must look familiar! (This is one of the reasons I think ReasonLigo is a great choice.) The function expects a tuple with two components (or values), one of type “storage” and one of type “string”. For the sake of clarity, I call the actual storage “s” in order to avoid confusing it with its type which is also storage.
The function returns a value of type storage. The Reason syntax allows very convenient shorthands to update and return a record: first, the spread operator copies all the fields of the record into a new record, then the punning technique allows us to write only the name of the value when it matches the name of the field (i.e “name” instead of “name: name”).

Following that, we can update the two previous functions:

type storage = {
count: int,
name: string
};
type action =
| Increment(int)
| Decrement(int)
| SaveName(string);
let add =
((s, count): (storage, int)): storage =>
{...s, count: s.count + count};
let sub =
((s, count): (storage, int)): storage =>
{...s, count: s.count - count};
let saveName =
((s, name): (storage, string)): storage => {...s, name: name};

Almost there! The only thing missing now is updating the switch of the main function, the actual entry point of the contract:

type storage = {
count: int,
name: string
};
type action =
| Increment(int)
| Decrement(int)
| SaveName(string);
let add =
((s, count): (storage, int)): storage =>
{...s, count: s.count + count};
let sub =
((s, count): (storage, int)): storage =>
{...s, count: s.count - count};
let saveName =
((s, name): (storage, string)): storage => {...s, name: name};
let main = ((p,s): (action, storage)) => {
let storage =
switch (p) {
| Increment(n) => add((s, n))
| Decrement(n) => sub((s, n))
| SaveName(n) => saveName((s, n))
};
([]: list(operation), storage);
};

Here it is! Now let’s make sure it compiles properly. Select the “Compile” option in the right column and click on “Run”, if you get the following code, you did it!

{ parameter (or (or (int %decrement) (int %increment)) (string %saveName)) ;
storage (pair (int %count) (string %name)) ;
code { DUP ;
DUP ;
CDR ;
DIP { DUP } ;
SWAP ;
CAR ;
IF_LEFT
{ DUP ;
IF_LEFT
{ DIP 2 { DUP } ;
DIG 2 ;
DIP { DUP } ;
PAIR ;
DUP ;
CAR ;
DUP ;
DIP { DUP } ;
SWAP ;
CAR ;
DIP { DIP 2 { DUP } ; DIG 2 ; CDR } ;
SUB ;
SWAP ;
CDR ;
SWAP ;
PAIR ;
DIP { DROP 3 } }
{ DIP 2 { DUP } ;
DIG 2 ;
DIP { DUP } ;
PAIR ;
DUP ;
CAR ;
DUP ;
DIP { DUP } ;
SWAP ;
CAR ;
DIP { DIP 2 { DUP } ; DIG 2 ; CDR } ;
ADD ;
SWAP ;
CDR ;
SWAP ;
PAIR ;
DIP { DROP 3 } } ;
DIP { DROP } }
{ DIP { DUP } ;
SWAP ;
DIP { DUP } ;
PAIR ;
DUP ;
CAR ;
DIP { DUP } ;
SWAP ;
CDR ;
SWAP ;
CAR ;
PAIR ;
DIP { DROP 2 } } ;
DUP ;
NIL operation ;
PAIR ;
DIP { DROP 4 } } }

If you have any issue with the code or if it doesn’t work for you, do not hesitate to ask a question in the comments, I’ll be happy to help 😊
You can also check the full code by following this link, directly in the Ligo online editor.

This article is available on my blog DecentraDev (the articles are hosted on the IPFS).

In the next article, we will play a little bit with the online editor and test the code we just wrote.
In future articles, we will start diving into Ligo and learn advanced concepts to write more complex smart contracts.

See you soon!!

Read the Part 2 here

Get Best Software Deals Directly In Your Inbox

--

--

Claude Barde
Coinmonks

Self-taught developer interested in web3 and functional programming