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) =
BODYThe 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 listis the type of lists of elements in'a - sets:
'a setis the type of sets of elements in'a - maps:
('a, 'b) mapis the type of maps whose keys are of type'aand values of type'b - big maps:
('a, 'b) big_mapis the type of lazily deserialized maps whose keys are of type'aand values of type'b - contracts:
'a contractfor contracts whose parameter is of type'a - functions:
'a -> 'bis the type of functions from'ato'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:
CONTRACTis the value of the contract being called;AMOUNTis the value of the amount of Tez sent to the contract;ARGis the argument sent to the contract.BODYis 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 :: yNIL ele_type:( [] : ele_type list )BLAKE2B:Crypto.blake2b x. Returns the Blake2b hash of its argument. (Same forCrypto.sha256andCrypto.sha512)HASH_KEY:Crypto.hash_key k. Returns the hash of the keyk.CHECK_SIGNATURE:Crypto.check key signature data. Returnstrueif 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 forx |> forf x, is the application of the lambdafon the argumentx.IMPLICIT_ACCOUNT:Account.default key_hash. Returns the default contract (of typeunit contract) associated with a key hash.ADDRESS:Contract.addressto retrieve the address of a contract
These operators take two values of the same type, and return a Boolean value:
COMPARE; EQ:x = yCOMPARE; NEQ:x <> yCOMPARE; LE:x <= yCOMPARE; LT:x < yCOMPARE; GE:x >= yCOMPARE; GT:x > y
The last one returns an integer:
COMPARE:compare x y
GET:Map.findUPDATE:Map.updateorSet.updateMEM:Map.memorSet.memCONCAT:@SIZE:List.sizeorSet.sizeorMap.sizeITER:List.iterorSet.iterorMap.iterorList.foldorSet.foldorMap.foldMAP:List.maporSet.maporMap.maporList.map_foldorSet.map_foldorMap.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 || yorx lor yAND:x && yorx land yXOR:x xor yorx lxor yNOT:not xorlnot xABS:abs xwith the difference thatabsreturns an integerINT:int xNEG:-xADD:x + ySUB:x - yMUL:x * yEDIV:x / yLSR:x >> yorx lsr yLSL:x << yorx lsl yISNAT:is_nat xreturn(Some y)iff x is positive, where y is of typenatand 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 + 1pm and p are of type nat and:
x = int mwhenxis positive or nullx = - (int p)whenxis negative
The unique constructor of type unit is ().
The two Booleans constants are:
truefalse
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
psuffix (0p,12p, etc.) or as an integer with a type coercion ((0 : nat)). - tez : an unbounded positive float of Tezzies, written either with
a
tzsuffix (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:
0x0xabcdef
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:
tz1YLtLqD1fWHthSVHPD116oYvsd4PTAHUocis a key hashedpkuit3FiCUhd6pmqf9ztUTdUs1isMTbF9RBGfwKk1ZrdTmeP9ypNis a public keyedsigedsigthTzJ8X7MPmNeEwybRAvdxS1pupqcM5Mk4uCuyZAe7uEk68YpuGDeViW8wSXMr Ci5CwoNgqs8V2w8ayB5dMJzrYCHhD8C7is 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.xRecords 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 * natVariants 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 `rightAll 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.