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 us 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 parameterarg
and the storagestorageIn
,- 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 typeoperation list
.
Let us do a very quick test with the online editor:
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 storage, the structure that has to be defined as part of the contract definition. The code and the storage type go together. 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 us 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 that represents the authorized 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 us 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 entrypoints 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.
LIGO offers a VSCode extension.
Let us 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 that can be used as an entrypoint.(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 anint
.block {skip}
when we don't want to calculate anythingwith ((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 look 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
Take a look at the types section in the LIGO documentation.
Afterwards, we define our entrypoint 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)