Liquidity is a language to program Smart Contracts for Tezos. It uses the syntax of OCaml, and strictly complies to Michelson security restrictions.
The Liquidity project contains:
- A compiler from Liquidity files (.liq extension) to Michelson
- A de-compiler from Michelson files (.tz extension) to Liquidity
- An evaluator of Michelson contracts
- An interface to a Tezos node for manipulating Liquidity contracts
See examples in the Github project.
All the contracts have the following form:
[%%version 0.3]
<... local declarations ...>
let%init storage
(x : TYPE)
(x : TYPE)
... =
BODY
let%entry main
(parameter : TYPE)
(storage : TYPE) =
BODY
The version
statement tells the compiler in which version of
Liquidity the contract is written. The compiler will reject any
contract that has a version that it does not understand (too old, more
recent). We expect to reach version 1.0 at the launch of the Tezos
network.
The main
function is the default entry point for the contract.
let%entry
is the construct used to declare entry points (there is
currently only one entry point, but there will be probably more in the
future). The declaration takes two parameters with names
parameter
, storage
, the arguments to the function. Their types must
always be specified. The return type of the function must also be
specified by a type annotation.
A contract always returns a pair (operations, storage)
, where
operations
is a list of internal operations to perform after
exectution of the contract, and storage
is the final state of the
contract after the call. The type of the pair must match the type of a
pair where the first component is a list of opertations and the second
is the type of the argument storage
of main
.
<... local declarations ...>
is an optional set of optional type and
function declarations. Type declarations can be used to define records
and variants (sum-types), described later in this documentation.
An optional initial storage or storage initializer can be given with
let%init storage
. When deploying a Liquidity contract, if the
storage is not constant it is evaluated in the prevalidation context.
Types in Liquidity are monomorphic. The built-in base types are:
unit
: whose only constructor is()
bool
: Booleansint
: unbounded integersnat
: unbounded naturalstez
: the type of amountsstring
: character stringsbytes
: bytes sequencestimestamp
: dates and timestampskey
: cryptographic keyskey_hash
: hashes of cryptographic keyssignature
: cryptographic signaturesoperation
: type of operations, can only be constructedaddress
: abstract type of contract addresses
The other types are:
- tuples: noted
(t1 * t2 * t3)
- option type:
'a option = None | Some of 'a
- variant type:
('a, 'b) variant = Left of 'a | Right of 'b
- lists:
'a list
is the type of lists of elements in'a
- sets:
'a set
is the type of sets of elements in'a
- maps:
('a, 'b) map
is the type of maps whose keys are of type'a
and values of type'b
- big maps:
('a, 'b) big_map
is the type of lazily deserialized maps whose keys are of type'a
and values of type'b
- contracts:
'a contract
for contracts whose parameter is of type'a
- functions:
'a -> 'b
is the type of functions from'a
to'b
Record and variant types must be declared beforehand and are referred to by their names.
Calling another contract is done by constructing an operation with the
built-in Contract.call
function, and returning this value at the
end of the contract. Internal contract calls are performed after
execution of the contract is over, in the order in which the resulting
operations are returned.
let op = Contract.call CONTRACT AMOUNT ARG in
BODY
( op :: OTHER_OPERATIONS, STORAGE)
where:
CONTRACT
is the value of the contract being called;AMOUNT
is the value of the amount of Tez sent to the contract;ARG
is the argument sent to the contract.BODY
is some code to be executed after the contract.
For the call to be actually performed by the blockchain, it has to be returned as part of the list of operations.
Here is a list of equivalences between MICHELSON instructions and Liquidity functions:
FAIL
/FAILWITH
:Current.failwith <object>
. Makes the contract abort.SELF
:Contract.self ()
. Returns the current contract being executed.BALANCE
:Current.balance ()
. Returns the current balance of the current contract.NOW
:Current.time ()
. Returns the timestamp of the block containing the transaction in the blockchain.AMOUNT
:Current.amount ()
. Returns the amount of tezzies that were transfered when the contract was called.STEPS_TO_QUOTA
:Current.gas ()
. Returns the current gas available to execute the end of the contract.SOURCE
:Contract.source
. Returns the address of the contract that initiated the current transaction.SENDER
:Contract.sender
. Returns the address of the last contract that called the current contract.CONS
:x :: y
NIL ele_type
:( [] : ele_type list )
BLAKE2B
:Crypto.blake2b x
. Returns the Blake2b hash of its argument. (Same forCrypto.sha256
andCrypto.sha512
)HASH_KEY
:Crypto.hash_key k
. Returns the hash of the keyk
.CHECK_SIGNATURE
:Crypto.check key signature data
. Returnstrue
if the public key has been used to generate the signature of the data.CREATE_ACCOUNT
:Account.create
. Creates a new account.CREATE_CONTRACT
:Contract.create
. Creates a new contract.SET_DELEGATE
:Contract.set_delegate
. Sets the delegate (or unset, if argument isNone
) of the current contract.CONTRACT param_type
:(Contract.at addr : param_type contract option)
: returns the contract stored at this address, if it existsEXEC
:Lambda.pipe x f
orx |> f
orf x
, is the application of the lambdaf
on the argumentx
.IMPLICIT_ACCOUNT
:Account.default key_hash
. Returns the default contract (of typeunit contract
) associated with a key hash.ADDRESS
:Contract.address
to retrieve the address of a contract
These operators take two values of the same type, and return a Boolean value:
COMPARE; EQ
:x = y
COMPARE; NEQ
:x <> y
COMPARE; LE
:x <= y
COMPARE; LT
:x < y
COMPARE; GE
:x >= y
COMPARE; GT
:x > y
The last one returns an integer:
COMPARE
:compare x y
GET
:Map.find
UPDATE
:Map.update
orSet.update
MEM
:Map.mem
orSet.mem
CONCAT
:@
SIZE
:List.size
orSet.size
orMap.size
ITER
:List.iter
orSet.iter
orMap.iter
orList.fold
orSet.fold
orMap.fold
MAP
:List.map
orSet.map
orMap.map
orList.map_fold
orSet.map_fold
orMap.map_fold
(it is possible to use the generic Coll.
prefix for all collections,
but not in a polymorphic way, i.e. Coll.
is immediately replaced by the
type-specific version for the type of its argument.)
Liquidity also provides additional operations:
List.rev : 'a list -> 'a list
: List reversalMap.add : 'a -> 'b -> ('a, 'b) map -> ('a, 'b) map
: add (or replace) a binding to a mapMap.remove : 'a -> ('a, 'b) map -> ('a, 'b) map
: remove a binding, if it exists, in a mapSet.add : 'a -> 'a set -> 'a set
: add an element to a setSet.remove : 'a -> 'a set -> 'a set
: remove an element, if it exists, in a set
OR
:x || y
orx lor y
AND
:x && y
orx land y
XOR
:x xor y
orx lxor y
NOT
:not x
orlnot x
ABS
:abs x
with the difference thatabs
returns an integerINT
:int x
NEG
:-x
ADD
:x + y
SUB
:x - y
MUL
:x * y
EDIV
:x / y
LSR
:x >> y
orx lsr y
LSL
:x << y
orx lsl y
ISNAT
:is_nat x
return(Some y)
iff x is positive, where y is of typenat
and y = x
For converting int
to nat
, Liquidity provides a special
pattern-matching construct match%nat
, on two constructors Plus
and
Minus
. For instance, in the following where x
has type int
:
match%nat x with
| Plus p -> p + 1p
| Minus m -> m + 1p
m
and p
are of type nat
and:
x = int m
whenx
is positive or nullx = - (int p)
whenx
is negative
The unique constructor of type unit
is ()
.
The two Booleans constants are:
true
false
As in Michelson, there are different types of integers:
- int : an unbounded integer, positive or negative, simply
written
0
,1
,2
,-1
,-2
,... - nat : an unbounded positive integer, written either with a
p
suffix (0p
,12p
, etc.) or as an integer with a type coercion ((0 : nat)
). - tez : an unbounded positive float of Tezzies, written either with
a
tz
suffix (1.00tz
, etc.) or as a string with type coercion (("1.00" : tez)
).
Strings are delimited by the characters "
and "
.
Bytes are sequences of hexadecimal pairs preceeded by 0x
, for
instance:
0x
0xabcdef
Timestamps are written in ISO 8601 format, like in Michelson:
2015-12-01T10:01:00+01:00
Keys, key hashes and signatures are base58-check encoded, the same as in Michelson:
tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUoc
is a key hashedpkuit3FiCUhd6pmqf9ztUTdUs1isMTbF9RBGfwKk1ZrdTmeP9ypN
is a public keyedsigedsigthTzJ8X7MPmNeEwybRAvdxS1pupqcM5Mk4uCuyZAe7uEk68YpuGDeViW8wSXMr Ci5CwoNgqs8V2w8ayB5dMJzrYCHhD8C7
is a signature
There are also three types of collections: lists, sets and maps. Constants collections can be created directly:
- Lists:
["x"; "y"]
; - Sets:
Set [1; 2; 3; 4]
; - Maps:
Map [1, "x"; 2, "y"; 3, "z"]
; - Big maps:
BigMap [1, "x"; 2, "y"; 3, "z"]
;
In the case of an empty collection, whose type cannot be inferred, the type must be specified:
- Lists:
([] : int list)
- Sets:
(Set : int set)
- Maps:
(Map : (int, string) map)
- Big maps:
(BigMap : (int, string) big_map)
Tuples in Liquidity are compiled to pairs in Michelson:
(x, y, z) <=> Pair x (Pair y z)
Tuples can be accessed using the field access notation of Liquidity:
let t = (x,y,z) in
let should_be_true = t.(2) = z in
...
A new tuple can be created from another one using the field access update notation of Liquidity:
let t = (1,2,3) in
let z = t.(2) <- 4 in
...
Tuples can be deconstructed:
(* t : (int * (bool * nat) * int) *)
let _, (b, _), i = t in
...
(* b : bool
i : int *)
Record types can be declared and used inside a liquidity contract:
type storage = {
x : string;
y : int;
}
Such types can be created and used inside programs:
let r = { x = "foo"; y = 3 } in
r.x
Records are compiled as tuples.
Deep record creation is possible using the notation:
let r1 = { x = 1; y = { z = 3 } } in
let r2 = r1.y.z <- 4 in
...
Variants should be defined before use, before the contract declaration:
type t =
| X
| Y of int
| Z of string * nat
Variants can be created using:
let x = X 3 in
let y = Z s in
...
The match
construct can be used to pattern-match on them, but only
on the first constructor:
match x with
| X -> ...
| Y i -> ...
| Z s -> ...
where i
and s
are variables that are bound by the construct to the
parameter of the variant.
Parameters of variants can also be deconstructed when they are tuples, so one can write:
match x with
| X -> ...
| Y i -> ...
| Z (s, n) -> ...
A special case of variants is the Left | Right
predefined variant,
called variant
:
type (`left, `right) variant =
| Left of `left
| Right of `right
All occurrences of these variants should be constrained with type annotations:
let x = (Left 3 : (int, string) variant) in
match x with
| Left left -> ...
| Right right -> ...
Another special variant is the Source
variant: it is used to refer to
the contract that called the current contract.
let s = (Source : (unit, unit) contract) in
...
As for Left
and Right
, Source
occurrences should be constrained by
type annotations.
Unlike Michelson, functions in Liquidity can also be closures. They can take multiple arguments and are curryfied. Because closures are lambda-lifted, it is however recommended to use a single tuple argument when possible. Arguments must be annotated with their (monomorphic) type, while the return type is inferred.
Function applications are often done using the Lambda.pipe
function
or the |>
operator:
let succ = fun (x : int) -> x + 1 in
let one = 0 |> succ in
...
but they can also be done directly:
...
let succ (x : int) = x + 1 in
let one = succ 0 in
...
A toplevel function can also be defined before the main entry point:
[%%version 0.2]
let succ (x : int) = x + 1
let%entry main ... =
...
let one = succ 0 in
...
Closures can be created with the same syntax:
let p = 10 in
let sum_and_add_p (x : int) (y : int) = x + y + p in
let r = add_p 3 4 in
...
This is equivalent to:
let p = 10 in
let sum_and_add_p =
fun (x : int) ->
fun (y : int) ->
x + y + p
in
let r = 4 |> (3 |> add_p) in
...
Functions with multiple arguments should take a tuple as argument because curried versions will generate larger code and should be avoided unless partial application is important. The previous function should be written as:
let sum_and_add_p ((x : int), (y : int)) =
let p = 10 in
x + y + p
in
let r = add_p (3, 4) in
...
Loops in liquidity share some syntax with functions, but the body of the loop is not a function, so it can access the environment, as would a closure do:
let end_loop = 5 in
let x = Loop.loop (fun x ->
...
(x < end_loop, x')
) x_init
in
...
As shown in this example, the body of the loop returns a pair, whose first part is the condition to remain in the loop, and the second part is the accumulator.