Skip to main content
Version: 1.4.0

Main functions and Entrypoints

Entry points

A LIGO smart contract is made of a series of constant and function declarations. Only functions having a special type can be called when the contract is activated: we call them entry points. An entry point function takes two parameters, the contract parameter and the on-chain storage, and returns a pair made of a list of operations and a (new) storage value.

A smart contract can export more than one entry point function. An entry point can be selected by specifying its name when calling the contract. For example, the following contract exports two functions, named increment and decrement. The increment function can be called by passing Increment (10) to the contract (notice the capitalization of Increment). More examples on how to perform this call are given below.

export namespace IncDec {
type storage = int;
type result = [list<operation>, storage];
// Four entrypoints
@entry
const increment = (delta : int, store : storage) : result =>
[list([]), store + delta];
@entry
const @default = (_u : unit, store : storage) : result =>
increment(1, store)
@entry
const decrement = (delta : int, store : storage) : result =>
[list([]), store - delta];
@entry
const reset = (_p : unit, _s : storage) : result =>
[list([]), 0];
};

When the contract is originated, the initial value of the storage is provided. When an entry point is later called, only the parameter is provided by the user, and the blockchain (or testing framework) supplies the current storage value as a second argument.

The type of the contract parameter and the storage are up to the contract designer, but the type for the list of operations is not. The return type of an entry point is as follows, assuming that the type storage has been defined elsewhere. (Note that you can use any type with any name for the storage.)

Note that the name default has a special meaning for a Mavryk entry point, and denotes the default entry point to be called unless another one is specified. Due to the fact that default is a reserved keyword in JsLIGO, we use the escape notation @default to write the function name, without it being misinterpreted as a keyword.

type storage = ...; // Any name, any type
type result = [list<operation>, storage];

The contract storage can only be modified by activating an entry point: given the state of the storage on-chain, an entry point function specifies how to create another state for it, depending on the contract's parameter.

Calling a contract

Using the dry-run command

In order to call the increment entry point of the smart contract, we can pass the -m IncDec option to specify the module and the Increment(...) constructor to specify the entry point (note the capitalization of Increment).

ligo run dry-run -m IncDec gitlab-pages/docs/advanced/src/entrypoints-contracts/incdec.jsligo 'Increment(5)' '0'

In the command above, 0 is the initial storage, and 5 is the delta argument.

Calling an on-chain contract

When a contract is deployed on-chain, the Michelson value for the parameter can be obtained with:

ligo compile parameter -m IncDec gitlab-pages/docs/advanced/src/entrypoints-contracts/incdec.jsligo 'Increment(5)'

In the command above, Increment is the (capitalized) name of the entry point to call, and 5 is the delta argument.

Using the WebIDE

Clicking on Dry Run in the WebIDE

Using the ligo run test command

A LIGO program can instantiate a new contract (or obtain an existing contract from its address), and call one of its entry points by passing e.g. the parameter Increment(5).

#import "gitlab-pages/docs/advanced/src/entrypoints-contracts/incdec.jsligo" "C"
const test = do {
let {addr , code , size} = Test.originate(contract_of(C.IncDec), 0, 0mav);
Test.transfer_exn(addr, Increment(42), 0mav);
assert(42 == Test.get_storage(addr));
};

The file above can be run with e.g. the ligo run test sub-command.

ligo run test --library . gitlab-pages/docs/advanced/src/entrypoints-contracts/test.jsligo

Main function

For more control over the contract's API, it used to be possible to declare one main function called main, that dispatches the control flow according to its parameter. When declaring entrypoints using the @entry annotation, LIGO automatically generates a main function, but it used to be possible to write such a function by hand instead of using the @entry facility.

This feature is now deprecated, future versions of LIGO will not allow the declaration of a single main function. A workaround is given at the end of this section.

While it is still possible to define a single function called main and mark it as the sole entry point using @entry, this is not what most programs should do. The following paragraphs are intended for programs which need more fine control over the behaviour of the entire program than what is possible using the automatic @entry mechanism.

As an analogy, in the C programming language, the main function is the unique main function and any function called from it would be an entrypoint.

Usually, the parameter of the contract is then a variant type, and, depending on the constructors of that type, different functions in the contract are called. In other terms, the unique main function dispatches the control flow depending on a pattern matching on the contract parameter.

In the following example, the storage contains a counter of type nat and a name of type string. Depending on the parameter of the contract, either the counter or the name is updated.

export type parameter =
| ["Action_A", nat]
| ["Action_B", string];
export type storage = {
counter : nat,
name : string
};
type result = [list<operation>, storage];
const entry_A = (n: nat, store: storage): result =>
[list([]), {...store, counter: n}];
const entry_B = (s: string, store: storage): result =>
[list([]), {...store, name: s}];
@entry
const main = (action: parameter, store: storage): result =>
match(action) {
when(Action_A(n)): entry_A(n, store);
when(Action_B(s)): entry_B(s, store)
};

Workaround for the deprecation of the main function

In most cases, adding [@entry] for CameLIGO or @entry for JsLIGO before the existing main function should suffice. However in cases where it is not possible or desirable to convert an existing contract_main contract to the new @entry format (e.g. generated code or a code review process that forbids making changes to an already-audited file), the deprecation can be circumvented by adding a proxy file which declares a single entry point and calls the existing main function, as follows:

#import "gitlab-pages/docs/advanced/src/entrypoints-contracts/contract_main.jsligo" "C"
namespace Proxy {
@entry
const proxy =
(p: C.parameter, s: C.storage): [list<operation>, C.storage] =>
C.main(p, s)
}

The contract can then be compiled using the following command:

ligo compile contract --library . \
-m Proxy \
gitlab-pages/docs/advanced/src/entrypoints-contracts/contract_main_proxy.jsligo

Notice that to compile a parameter for this contract, now we need to pass the either -e proxy or construct a value using the Proxy constructor:

ligo compile parameter --library . \
-m Proxy -e proxy \
gitlab-pages/docs/advanced/src/entrypoints-contracts/contract_main_proxy.jsligo \
"Action_A(42n)"
ligo compile parameter --library . \
-m Proxy \
gitlab-pages/docs/advanced/src/entrypoints-contracts/contract_main_proxy.jsligo \
"Proxy(Action_A(42n))"

Mavryk-specific Built-ins

A LIGO smart contract can query part of the state of the Mavryk blockchain by means of built-in values. In this section you will find how those built-ins can be utilised.

Accepting or Declining Tokens in a Smart Contract

This example shows how Mavryk.get_amount and failwith can be used to decline any transaction that sends more mav than 0mav, that is, no incoming tokens are accepted.

type parameter = unit;
type storage = unit;
type result = [list<operation>, storage];
@entry
const no_tokens = (action: parameter, store: storage): result => {
if (Mavryk.get_amount() > 0mav) {
return failwith("This contract does not accept tokens.");
} else {
return [list([]), store];
};
};

Access Control

This example shows how Mavryk.get_sender can be used to deny access to an entrypoint.

const owner = "mv18Cw7psUrAAPBpXYd9CtCpHg9EgjHP9KTe" as address;
const owner_only = (action: parameter, store: storage): result => {
if (Mavryk.get_sender() != owner) { return failwith("Access denied."); }
else { return [list([]), store]; };
};

Note that we do not use Mavryk.get_source, but instead Mavryk.get_sender. In our tutorial about security you can read more about it.

Inter-Contract Invocations

It would be somewhat misleading to speak of "contract calls", as this wording may wrongly suggest an analogy between contract "calls" and function "calls". Indeed, the control flow returns to the site of a function call, and composed function calls therefore are stacked, that is, they follow a last in, first out ordering. This is not what happens when a contract invokes another: the invocation is queued, that is, follows a first in, first out ordering, and the dequeuing only starts at the normal end of a contract (no failure). That is why we speak of "contract invocations" instead of "calls".

It is possible to obtain the behaviour of normal function "calls" using views, which are pure functions that do not modify the callee's on-chain state. However, this section describes inter-contract invocations which are queued, and may modify the callee's state.

The following example shows how a contract can invoke another by emitting a transaction operation at the end of an entrypoint.

The same technique can be used to transfer tokens to an implicit account (mv1, ...): all you have to do is use a unit value as the parameter of the smart contract.

In our case, we have a counter contract that accepts an action of type parameter, and we have a proxy contract that accepts the same parameter type, and forwards the call to the deployed counter contract.

// gitlab-pages/docs/advanced/src/entrypoints-contracts/incdec.jsligo
export namespace IncDec {
type storage = int;
type ret = [list<operation>, storage];
@entry
const increment = (delta : int, store : storage) : ret =>
[list([]), store + delta];
// And so on, as above
};
// proxy.jsligo
type parameter =
| ["Increment", int]
| ["Decrement", int]
| ["Reset"];
type storage = unit;
type result = [list<operation>, storage];
const dest = "KT19wgxcuXG9VH4Af5Tpm1vqEKdaMFpznXT3" as address;
const proxy = (action: parameter, store: storage): result => {
let counter : contract<parameter> = Mavryk.get_contract_with_error(dest, "not found");
let op = Mavryk.transaction(Increment(5), 0mav, counter);
return [list([op]), store];
};