Intermediate

 

Compiling and deploying a Contract in LIGO

Welcome to our tutorial on compiling and deploying a contract in LIGO, a programming language for the Tezos blockchain network. In this tutorial, we will cover the process of creating and deploying smart contracts using the LIGO programming language. We will walk through the process of writing, compiling, and deploying a contract, as well as explain the key features and advantages of using LIGO.

By the end of this tutorial, you will have a solid understanding of how to write, compile and deploy smart contracts using the LIGO programming language on the Tezos network. You will also learn best practices for developing smart contracts in LIGO and how to interact with them. This tutorial is ideal for developers and researchers interested in creating and deploying smart contracts on the Tezos blockchain, using LIGO which offers a powerful type-safe and formally verified programming language for smart contract development.

Compiling a contract

In order to deploy a contract in a Tezos network, we first need to compile it. This is done using a compiler (aka LIGO compiler) that transforms LIGO code into Michelson code.

Michelson smart contracts are stored in a file with the .tz extension.

Here is how to transform a LIGO code into a Michelson code using the LIGO compiler in the command line.

ligo compile contract SOURCE_LIGO_FILE

Where:

  • SOURCE_LIGO_FILE is the path to your LIGO file containing the main function.

Example:

ligo compile contract examples/counter.ligo

Simulating (Dry-running) a contract

Testing a contract would be pretty tricky if you always had to set up a live node to run tests. Fortunately, an easy alternative is to use LIGO’s built-in dry-run feature. Dry-running is a simulated execution of the smart contract as if it was deployed on a real chain. It works by stimulating the main execution function, based on a mock storage value and a parameter.

ligo run dry-run [flags] SOURCE_LIGO_FILE 'ACTION(P)' 'STORAGE_STATE'

where:

  • ACTION(P) is a LIGO expression used to specify the action that triggers the associated entrypoint with the corresponding parameter p.

  • STORAGE_STATE is the state of the storage when simulating the execution of the entrypoint.

Example:

ligo run dry-run counter.ligo 'Increment(5)' 3
// Outputs: ( LIST_EMPTY() , 8 )

Defining the initial storage

The Michelson output of the following command can be used to init the storage when deploying the contract.

ligo compile storage SOURCE_LIGO_FILE 'STORAGE_STATE'

where:

  • STORAGE_STATE is a LIGO expression that defines the initial state of the storage.

Example:

ligo compile storage counter.ligo 5
// Outputs: 5

Invoking the contract with a parameter

The Michelson output of the following command can be used as the entrypoint name when invoking an entrypoint of the smart contract (as well as the parameters of that entrypoint).

ligo compile-parameter SOURCE_LIGO_FILE MAIN_FUNCTION 'ACTION(P)'

where:

  • ACTION(P) is a LIGO expression used to specify the action that triggers the associated entrypoint with the corresponding parameter p.

Example:

ligo compile parameter examples/counter.ligo main 'Increment(5)'
// Outputs: (Right 5)

Deploy and Invoke

Deploy

A smart contract must be deployed on the blockchain in order to be invoked. When deploying a smart contract on the blockchain, one must specify the initial state of the storage.

The deployment of a smart contract in Tezos is called “origination“.

Here is the syntax for the Tezos command line to deploy a smart contract:

octez-client originate contract CONTRACT_NAME transferring AMOUNT_TEZ from FROM_USER \\
             running MICHELSON_FILE \\
             --init 'INITIAL_STORAGE' --burn-cap GAZ_FEE

where:

  • CONTRACT_NAME is the name given to the contract.

  • MICHELSON_FILE is the path for the Michelson smart contract code (.tz file).

  • AMOUNT_TEZ is the quantity of Tez being transferred to the newly deployed contract. If a contract balance reaches 0 then it is deactivated.

  • FROM_USER account from which the Tez are taken (and transferred to the new contract).

  • INITIAL_STORAGE is a Michelson expression. The --init parameter is used to specify the initial state of the storage.

  • GAZ_FEE is a specified maximal fee the user is willing to pay for this operation (using the --burn-cap parameter).

Invoke

Once the smart contract has been deployed on the blockchain (contract-origination operation baked into a block), it is possible to invoke an entrypoint from the smart contract using the command line. Here is the syntax of the Tezos command line to invoke a smart contract:

octez-client transfer AMOUNT_TEZ from USER to CONTRACT_NAME --arg 'ENTRYPOINT_INVOCATION' --dry-run

where:

  • AMOUNT_TEZ is the quantity of Tez being transferred to the contract.

  • CONTRACT_NAME is the name given to the contract.

  • ENTRYPOINT_INVOCATION is a Michelson expression of the entrypoint and the corresponding parameter. The --arg parameter specifies an entrypoint call.

⚠️ Notice that the --dry-run parameter simulates the invocation of the entrypoint.

Example

Let’s consider a counter contract for our example.

Our counter contract will store a single int as its storage, and will accept an action variant in order to re-route our single main function to two entrypoints for add (addition) and sub (subtraction).

type parameter is
  Increment of int
| Decrement of int

type storage is int

type return is list (operation) * storage

function add (const n : int; const store : storage) : storage is store + n
function sub (const n : int; const store : storage) : storage is store - n

function main (const action : parameter; const store : storage) : return is
  ((nil : list(operation)),
  case action of [
    | Increment (n) -> add (n, store)
    | Decrement (n) -> sub (n, store)
  ])

Compile

ligo compile contract counter.ligo > counter.tz

The command above outputs the following Michelson code:

{ parameter (or (int %decrement) (int %increment)) ;
  storage int ;
  code { UNPAIR ; IF_LEFT { SWAP ; SUB } { ADD } ; NIL operation ; PAIR } }

Initial storage

However, in order to originate a Michelson contract on Tezos, we also need to provide its initial storage value, we can use compile storage to compile the LIGO representation of the storage to Michelson.

ligo compile storage counter.ligo 5
// Outputs: 5

Invocation parameter

The same rules apply to the parameters. We will need to use compile parameter to compile our action variant into Michelson, here’s how:

ligo compile parameter counter.ligo 'Increment(5)'
// Outputs: (Right 5)

We can now use (Right 5), which is a Michelson value, to invoke our contract via octez-client.

Simulating

To dry-run the counter-contract, we provide a main function with a variant parameter of value Increment (5) and an initial storage value of 3.

ligo run dry-run counter.ligo 'Increment(5)' 3
// Outputs: ( LIST_EMPTY() , 8 )

The simulation shows that our storage would have been incremented to 8.

Deploy

Now that we have verified that our code compiles well and that it was functional, we can deploy our contract on the blockchain.

octez-client originate contract counterContract for boostrap1 transferring 1 from boostrap2 \\
             running code.tz \\
             --init '0' --burn-cap 0.12525

Invoke

Let’s invoke the entrypoint Increment(5) of the smart contract. Remember that the output of the compile parameter of this entrypoint was (Right 5)

octez-client transfer 5 from boostrap1 to counterContract --arg '(Right 5)'

Note that you can simulate the invocation by adding the --dry-run parameter to the above command.

Accessing storage

You can access the stored value with the following command:

octez-client get contract storage for counterContract

Some specificities for Maps, Tuples, and Records

Consider the following LIGO code snippet for the storage definition

//starmap.ligo
type coordinates is ( int * int * int)
type storage is map (string, coordinates)

function main (const _action : unit; const store : storage) : list(operation) * storage is
  ((nil : list(operation)), store)

Maps

The initialization of the elements of a map is specified between map [ and ] and elements are separated by a semi-colon ;. Each element is a key/value pair separated by ->.

Initialization of the elements of a map follows the syntax:

map[ KEY1 -> VALUE1; KEY2 -> VALUE2 ]

Here is an example of a command-line ligo compile storage for transpiling a map.

ligo compile storage starmap.ligo 'map [ "earth" -> (2,7,1); "sun" -> (0,0,0) ]'

This command returns:

{ Elt "earth" (Pair (Pair 2 7) 1) ; Elt "sun" (Pair (Pair 0 0) 0) }

Tuples

Initialization of the tuple elements is specified between ( and ), and separated by comma ,.

(VALUE1, VALUE2, VALUE3)

Here is an example of a command-line ligo compile storage for compiling a map containing a tuple.

ligo compile storage starmap.ligo 'map [ "earth" -> (2,7,1) ]'

This command returns:

{ Elt "earth" (Pair (Pair 2 7) 1) }

When specifying an empty map, one must set the map [] into the expected type.

ligo compile storage starmap.ligo '(map []: map(string,coordinates))'

Records

Initialization of elements in a record is specified between map [ and ] and elements separated by a semi-colon ;.
Each element is a key/value pair separated by = and follows the syntax:

record[ KEY1 = VALUE1; KEY2 = VALUE2 ]

We should now have a record instead of a tuple for coordinates.

//starmap2.ligo
type coordinates is record [
  x : int;
  y : int;
  z : int
]
type storage is map (string, coordinates)

function main (const _action : unit; const store : storage) : list(operation) * storage is
  ((nil : list(operation)), store)

We can compile the storage as follows:

ligo compile storage starmap2.ligo 'map [ "earth" -> record [x=2;y=7;z=1] ]'

This command returns:

{ Elt "earth" (Pair (Pair 2 7) 1) }

With this, you complete this workshop successfully!!