B9lab Logo
Tezos Developer Portal
Developer PortalDeveloper Portal

LIGO - Writing Smart Contracts

Smart contract development with CameLIGO


LIGO

LIGO is a statically-typed, high-level language that compiles down to Michelson. The syntaxes currently supported are PascaLIGO (Pascal-like syntax), CameLIGO (Caml-like syntax), and ReasonLIGO (Reason-like syntax).

Similar to SmartPy, it is still in development. The idea is to offer a secure and simple tool. In the long term, the plan is to support multiple syntaxes.

The installation is easy. Since we have previously worked with Docker, we also want to use it for LIGO:

$ curl https://gitlab.com/ligolang/ligo/raw/dev/scripts/installer.sh | bash -s "next"

Although other syntaxes will be supported in the future, here we want to introduce two which are already quite advanced in their development.

CameLIGO

Along the way we have gained experience with smart contracts and you can have a look at our OCaml introduction (at the beginning of the section) to become familiar with the OCaml syntax. This will make it easier for us to engage with CameLIGO. Before diving into details, let's again write our prominent repeater contract.

Create repeater.mligo:

type storage = int
let main (arg,storageIn  : storage*storage) = (([] : operation list), arg)

It looks very similar to OCaml with the difference being that, momentarily, we have to explicitly pass all types. The rest should look familiar from OCaml and Michelson:

  • type storage = int is a type definition,
  • main takes two arguments, the parameter arg and the storage storageIn,
  • even though we named our input storage instance storageIn, we do nothing with it, so it could have been left as _, and
  • the value is a tuple ([], arg), whereas [] is of the type operation list.

Let's do a very quick test with the online editor:

repeateride

At the bottom you will see the Michelson output:

{ parameter int ; storage int ; code { UNPAIR ; SWAP ; DROP ; NIL operation ; PAIR } }

Well, we have worked with Michelson before and know that this code can be written in a bit more efficient way by replacing

UNPAIR ; SWAP ; DROP

with

CAR

which will do the same, take the left of the input pair parameter*storage.

Storage

Smart contracts have access to and can modify their own storage, the structure that has to be defined as part of the contract definition. The code and the storage type go together. In fact, the storage is just another type, and by convention we name it storage. The type is flexible, but, like the code, cannot be changed once deployed.

For instance, if you have a contract that does not need to save anything to the storage, you would declare:

type storage = unit

If your contract only needs to save a positive number, you would declare:

type storage = nat

If your contract only needs to save either a positive number or nothing, you would declare:

type storage = nat option

Where option is one of the native parameterised variants: type 'a option = None | Some 'a.

If your contract only needs to save a single person, you would declare it with a record:

type storage = {
    name: string;
    age: nat;
    streetAddress: string;
    tezAddress: address;
}

If your contract wants to act as a Tezos token ATM, you may declare its storage with:

type storage = {
    balances: (address, tez) map;
    totalSupply: tez;
}

This will create a namespace mapping addresses to balances in tez.

The smart contract to certify students

We want to start with an example that we already know from SmartPy and Michelson, the student certification contract. Let's begin by defining a student:

type student = {
    name : string ;
    certificate : bool;
  }

Since we don't want everyone to be able to issue certificates, we also want to save an address which represents the authorised person:

type storage = {
  students : student list;
  certifier : address;
}

Now, we can add the following function:

type student = {
    name : string ;
    certificate : bool;
  }
type storage = {
  students : student list;
  certifier : address;
}
let certifyStudent (name,oldState : string*storage) =
    let reqSender = sender in
    if reqSender = oldState.certifier 
    then 
      let newStudents:(student list) = 
      { name = name; certificate = true } :: oldState.students in
      let newState:storage = { students = newStudents; certifier = oldState.certifier } in
      ( ([]:operation list) , newState )
    else 
      ( ([]:operation list) , oldState ) 

It should be clear now what we mean with caml-like syntax.

We now can compile the code and get the Michelson output:

{ parameter string ; storage (pair (address %certifier) (list %students (pair (bool %certificate) (string %name)))) ; code { UNPAIR ; SENDER ; DIG 2 ; DUP ; DUG 3 ; CAR ; SWAP ; COMPARE ; EQ ; IF { SWAP ; DUP ; DUG 2 ; CDR ; SWAP ; PUSH bool True ; PAIR ; CONS ; SWAP ; CAR ; PAIR ; NIL operation ; PAIR } { DROP ; NIL operation ; PAIR } } }

You can store this Michelson code and deploy it with the help of the tezos-client, as we have done before; e.g. using --init 'Pair {} "tz1W4W2yFAHz7iGyQvFys4K7Df9mZL6cSKCp"'.

Let's take a look at the example at https://ligolang.org/:

type storage = int

(* variant defining pseudo multi-entrypoint actions *)

type action =
| Increment of int
| Decrement of int

let add (a,b: int * int) : int = a + b
let sub (a,b: int * int) : int = a - b

(* real entrypoint that re-routes the flow based on the action provided *)

let main (p,s: action * storage) =
 let storage =
   match p with
   | Increment n -> add (s, n)
   | Decrement n -> sub (s, n)
 in ([] : operation list), storage

This shows that we can use pattern matching as we know it from OCaml, and also add comments the same way. This is especially useful when used as shown in this example. You have been using multiple entry points already. When using LIGO, this alternative pattern is currently recommended.

PascaLIGO

Pascal is a relatively beginner friendly language and has many keywords. For this reason, a Pascal-like syntax should help with writing smart contracts that are easier to read and understand.

tip icon

LIGO offers a VSCode extension.

Let's start with the repeater contract again:

function main (const arg : int;  const storageIn : int) : (list(operation) * int) is
  block {skip} with ((nil : list(operation)), arg)

After a quick test via online IDE, save this as repeater.ligo and run:

ligo dry-run ./repeater.ligo main 5 0

Hopefully you will see the expected value.

For definitions, similar to Pascal we use:

  • function main to define a function which can be used as entry point.
  • (const arg : int; const storageIn : int) for our input parameters, as we have done with other languages.
  • (list(operation) * int) to set the return type, as seen before it is a tuple consisting of a (list(operation) and an int.
  • block {skip} when we don't want to calculate anything
  • with ((nil : list(operation)), arg) to return a the tuple together with the new input.

We define variables with const(immutable) and var(mutable), where var can only be used inside functions or blocks.

So, how would our certification contract for the students looks like?

type student is record
    name : string ;
    certificate : bool;
end
type certStorage is record
  students : list(student);
  certifier : address;
end
function certifyStudent (const studentName : string; const oldState:certStorage) : (list(operation) * certStorage) is
  begin
  if sender =/= oldState.certifier then failwith("Only certifier can call this function");
  else skip;
  const newStudent : student = record 
            name = studentName; 
            certificate= True;
            end;
  const oldList:list(student) = oldState.students;
  const newList:list(student) = cons(newStudent, oldList);
  const newState : certStorage = record
            students= newList;
            certifier= oldState.certifier;
            end;
  end with ((nil : list(operation)), newState)

In this sample, we define our storage similar to our implementation in CameLIGO:

type student is record
    name : string ;
    certificate : bool;
end
type certStorage is record
  students : list(student);
  certifier : address;
end
info icon

Take a look at the types section in the LIGO documentation.

Afterwards, we define our entry point with the right input and output parameter types:

function certifyStudent (const studentName : string; const oldState:certStorage) : (list(operation) * certStorage)

We add our usual check:

if sender =/= oldState.certifier then fail("Only certifier can call this function");
  else skip;

After that, we define a new student:

const newStudent : student = record 
            name = studentName; 
            certificate= True;
            end;

And extend our list with this new student:

const oldList:list(student) = oldState.students;
const newList:list(student) = cons(newStudent, oldList);

Next, we change the state with the new list:

const newState : certStorage = record
          students= newList;
          certifier= oldState.certifier;
          end;

to finally store this new state:

end with ((nil : list(operation)), newState)
reading icon
Discuss on Slack