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.
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.
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
).
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:
In the command above, Increment
is the (capitalized) name of the entry point to call, and 5
is the delta
argument.
Using 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)
.
The file above can be run with e.g. the ligo run test
sub-command.
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.
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:
The contract can then be compiled using the following command:
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:
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.
Access Control
This example shows how Mavryk.get_sender
can be used to deny access to an
entrypoint.
Note that we do not use
Mavryk.get_source
, but insteadMavryk.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 (tz1, ...): 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.