The Love Smart Contract Language: Introduction & Key Features — Part II

David Declerck
Dune Network
Published in
7 min readFeb 17, 2020

By De Oliveira Steven & David Declerck

Introduction

In our previous article, we gave an overview of how smart contracts are written in Love. This second part will give details on how to enhance the code structure of a smart contract and cover contract interactions.

Modules

The code of a smart contract can be arranged in so-called modules. Modules are namespaces in which one can pack type and function declarations. Here is a (simple) module to define and evaluate basic arithmetic expressions:

module Exp = struct  type t =
| Const of int
| Add of t * t
| Sub of t * t
| Mul of t * t
| Div of t * t
val rec eval (f : t) : int =
match f with
| Const i -> i
| Add (f1, f2) -> eval f1 + eval f2
| Sub (f1, f2) -> eval f1 - eval f2
| Mul (f1, f2) -> eval f1 * eval f2
| Div (f1, f2) ->
begin match eval f1 / eval f2 with
| None -> failwith [:string] "Division by 0 !" [:int]
| Some (n, _) -> n
end
end

It contains a sum type t that defines the constructors we need to build arithmetic expressions, and a (recursive) eval function that computes the result of a given formula (note that for simplicity, we just discard the remainder of the division).

To use the elements defined in this module, one simply needs to prefix them with the module name. For instance, we can build and evaluate a formula as follows:

let f = Exp.Add (Exp.Const 42) (Exp.Const 14) in
let result = Exp.eval f in
...

Subcontracts and Originating Contracts With Code

Similarly to modules, (sub)contracts may be defined inside contracts. They look like modules, but start with the contract keyword, and can contain all the declarations allowed inside a top-level contract, including entrypoints. Their main purpose is to allow a contract to originate another contract. Here is an example of such (sub)contract:

#lovetype storage = unitcontract C = struct
type storage = int
val%init storage (i : int) = i
val%entry set s a (i : int) = [][:operation], i
end

The subcontract C has a simple integer storage, an initializing function and an entrypoint set, allowing to change its value. For now, its contents cannot be used as such: C only represents the source code of a contract. To use C as an independent contract, it is necessary to originate it. To do so,the Contract.create primitive must be called from an entrypoint of the top-level contract:

val%entry main s a (_ : unit) =
let op, addr =
Contract.create [:int] (None [:keyhash]) 1.0dn (contract C) 42
in
[op][:operation], s

This primitive is polymorphic: it takes as first argument the type of the storage initializer, then an optional delegate for the originated contract (which we set to None), an amount of DUN to give to the contract, the name of the subcontract that contains the code to deploy, and the value of the storage initializer. It returns an operation and the address of the contract. The operation must be added to the list of the operations returned by the entrypoint: indeed, operations are not instantly performed, but performed after the entrypoint has finished executing. If an operation is not added to this list, it will never be performed.

Calling Other Contracts

Whether a contract was deployed from a client application or directly from another contract as shown in the previous section, we often need to call its entrypoints from another contract. There are two ways in which contracts can interact with each other: a dynamic approach, similar to Liquidity’s; and a static approach, through explicit declarations at the beginning of the contract definition.

The Dynamic Approach

Let’s say we want to call the contract we’ve just originated in the previous section. The calling contract should specify which functions and entrypoints it is going to call in this contract, and possibly what types it will use. To do so, it relies on the notion of contract signature. A contract signature resembles a contract definition, except that function and entrypoint bodies are absent. Moreover, entrypoints only need to have the type of their argument specified, not the whole type of the corresponding function. For instance, if we only need to call the set entrypoint, then our signature can be as simple as the following one:

contract type CT = sig
val%entry set : int
end

Then, the next step consists in retrieving the contract, which is done using the Contract.at primitive. This primitive takes the signature of the contract as an extended argument, i.e. between <! >, followed by the address of the contract, and returns the contract, if it succeeded. If the contract was successfully retrieved, its content must be “lifted” to the level of types before we can use it. This is achieved by matching the returned contract with a (contract C : S) pattern : C is the name by which the contract will be known locally, and S is its signature (which should be included in the signature given earlier to Contract.at). We can now use the contract contents as if we used the contents of a module. The following example shows how one can call the set entrypoint of the contract given earlier:

val%entry call_set s a (addr : address) =
let c_opt = Contract.at<!CT> addr in
match c_opt with
| None ->
[][:operation], s
| Some (contract MyContract : CT)->
let op = MyContract.set 0dn 14 in
[op][:operation], s

Here, we assume the contract’s address to be given as argument to the call_set entrypoint. The Contract.at primitive will fail if it can not find a suitable contract at the given address: the address may not exist, or the contract at the specified address may not match the provided signature. In this case, the primitive will return the value None.

If the contract was successfully retrieved, then we "turn it into a module” by matching it with (contract MyContract : CT) in the pattern matching branch). We then call the entrypoint set with an amount of 0 DUN and the value 14 as argument, which returns an operation: like for origination, calling a contract entrypoint generates a pending operation, which will be performed after the calling entrypoint has finished running, and which must be added to the list of operations to perform.

The Static Approach

The static approach is much easier to perform than the dynamic approach, but it requires the user to know the address of the contract to call prior to deploying the calling contract. To use this approach, the user must write the contract dependencies at the top of his contract. These dependencies may then be used directly by the calling contract. This is illustrated by the following example (in which the called contract is the same as above):

#loveuse MyContract : KT1Bi2cR9RDNJBmuJ2fYrtLwHRfvxENNuheoval%entry call_set s a (_ : unit) =
let op = MyContract.set 0dn 14 in
[op][:operation], s

Note that we do not need to specify a signature: checking that dependencies expose all the entrypoints and functions used in the calling contract will be done during the origination, hence the contract will not fail at runtime because of an unsatisfied dependency.

Which Approach Should I Use?

The approach to use depends on what you want to achieve. In general, if you just want to occasionally call some entrypoints of another contract, you may prefer the dynamic approach. If however you make intensive use of another contract, then you’d prefer the static approach, if you can. Typically, that will be the case if you call a library contract.

One aspect to consider for choosing one of these approaches is the cost of deserializing the called contract. With the dynamic approach, the contract is deserialized when Contract.at is called, while with the static approach, the contract is deserialized when the calling contract is loaded, even if calling an entrypoint that does not use it (note that this should change in the future, so that the called contracts will be deserialized lazily, when they are actually used).

Storage Viewers

An interesting new feature in Love is the ability to define functions that allow the current contract’s storage to be read by another contract. Viewers are introduced with the val%view keyword and take the storage as first argument, and a user-defined second argument. For instance, in the example given in the Subcontracts section, we could add the following viewer to access the integer stored in the contract:

contract C = struct
...
val%view get s (_ : unit) : int = s
end

We then need to extend the CT signature to include the viewer (which should only mention the user-defined argument, and not the storage):

contract type CT = sig
...
val%view get : unit -> int
end

The viewer can then be called as any ordinary function:

val%entry call_get s a (_ : unit) =
let i = MyContract.get () in
...
[][:operation], s

Closing Words

We’ve briefly exposed in these articles the novelties of the Love language, with the hope they will allow plenty of new smart contracts to be written and deployed on the Dune Network. Stay tuned for more technical Love articles! Meanwhile, don’t hesitate to follow the Love tutorial and to read the documentation.

These are some of the resources you might find interesting in building your own smart contracts:

And other resources on the Dune Network:

--

--