11 KiB
id | title |
---|---|
entrypoints-contracts | Access function and Entrypoints |
Access Functions
A LIGO 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 called them access functions. An access 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.
When the contract is originated, the initial value of the storage is provided. When an access function is later called, only the parameter is provided, but the type of an access function contains both.
The type of the contract parameter and the storage are up to the
contract designer, but the type for list operations is not. The return
type of an entrypoint 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.)
type storage is ... // Any name, any type
type return is list (operation) * storage
type storage = ... // Any name, any type
type return = operation list * storage
type storage = ...; // Any name, any type
type return = (list (operation), storage);
The contract storage can only be modified by activating an access function. It is important to understand what that means. What it does not mean is that some global variable holding the storage is modified by the entrypoint. Instead, what it does mean is that, given the state of the storage on-chain, an entrypoint specifies how to create another state for it, depending on a parameter.
Here is an example where the storage is a single natural number that is updated by the parameter.
type storage is nat
type return is list (operation) * storage
function save (const parameter : nat; const store : storage) : return is
((nil : list (operation)), parameter)
type storage = nat
let save (parameter, store: nat * storage) : return =
(([] : operation list), parameter)
type storage = nat;
let main = ((parameter, store): (nat, storage)) : return => {
(([] : list (operation)), parameter);
};
Entrypoints
In LIGO, the design pattern is to have one access function that dispatches the control flow according to its parameter. Those functions called for those actions are called entrypoints.
As an analogy, in the C programming language, the main
function is
the unique access function and any function called from it would be an
entrypoint.
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 access 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.
type parameter is
Entrypoint_A of nat
| Entrypoint_B of string
type storage is record [
counter : nat;
name : string
]
type return is list (operation) * storage
function handle_A (const n : nat; const store : storage) : return is
((nil : list (operation)), store with record [counter = n])
function handle_B (const s : string; const store : storage) : return is
((nil : list (operation)), store with record [name = s])
function main (const param : parameter; const store : storage): return is
case param of
Entrypoint_A (n) -> handle_A (n, store)
| Entrypoint_B (s) -> handle_B (s, store)
end
type parameter =
Entrypoint_A of nat
| Entrypoint_B of string
type storage = {
counter : nat;
name : string
}
type return = operation list * storage
let handle_A (n, store : nat * storage) : return =
([] : operation list), {store with counter = n}
let handle_B (s, store : string * storage) : return =
([] : operation list), {store with name = s}
let main (param, store: parameter * storage) : return =
match param with
Entrypoint_A n -> handle_A (n, store)
| Entrypoint_B s -> handle_B (s, store)
type parameter =
| Entrypoint_A (nat)
| Entrypoint_B (string);
type storage = {
counter : nat,
name : string
};
type return = (list (operation), storage);
let handle_A = ((n, store): (nat, storage)) : return => {
(([] : list (operation)), {...store, counter : n}); };
let handle_B = ((s, store): (string, storage)) : return => {
(([] : list (operation)), {...store, name : s}); };
let main = ((param, store): (parameter, storage)) : return => {
switch (param) {
| Entrypoint_A (n) => handle_A ((n, store))
| Entrypoint_B (s) => handle_B ((s, store))
}
};
Tezos-specific Built-ins
A LIGO smart contract can query part of the state of the Tezos blockchain by means of built-in values. In this section you will find how those built-ins can be utilized.
Accepting or Declining Tokens in a Smart Contract
This example shows how amount
and failwith
can be used to decline
any transaction that sends more tez than 0mutez
, that is, no
incoming tokens are accepted.
type parameter is unit
type storage is unit
type return is list (operation) * storage
function deny (const param : parameter; const store : storage) : return is
if amount > 0mutez then
(failwith ("This contract does not accept tokens.") : return)
else ((nil : list (operation)), store)
type parameter = unit
type storage = unit
type return = operation list * storage
let deny (param, store : parameter * storage) : return =
if amount > 0mutez then
(failwith "This contract does not accept tokens.": return)
else (([] : operation list), store)
type parameter = unit;
type storage = unit;
type return = (list (operation), storage);
let deny = ((param, store): (parameter, storage)) : return => {
if (amount > 0mutez) {
(failwith("This contract does not accept tokens."): return); }
else { (([] : list (operation)), store); };
};
Access Control
This example shows how sender
or source
can be used to deny access to an entrypoint.
const owner : address = ("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx": address);
function filter (const param : parameter; const store : storage) : return is
if source =/= owner then (failwith ("Access denied.") : return)
else ((nil : list(operation)), store)
let owner : address = ("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx": address)
let filter (param, store: parameter * storage) : return =
if source <> owner then (failwith "Access denied." : return)
else (([] : operation list), store)
let owner : address = ("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx": address);
let main = ((param, store): (parameter, storage)) : storage => {
if (source != owner) { (failwith ("Access denied.") : return); }
else { (([] : list (operation)), store); };
};
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 our 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".
The following example shows how a contract can invoke another by emiting 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.ligo
contract that accepts a
parameter of type action
, and we have a proxy.ligo
contract that
accepts the same parameter type, and forwards the call to the deployed
counter contract.
// counter.ligo
type parameter is
Increment of nat
| Decrement of nat
| Reset
type storage is unit
type return is list (operation) * storage
// proxy.ligo
type parameter is
Increment of nat
| Decrement of nat
| Reset
type storage is unit
type return is list (operation) * storage
const dest : address = ("KT19wgxcuXG9VH4Af5Tpm1vqEKdaMFpznXT3" : address)
function proxy (const param : parameter; const store : storage): return is
block {
const counter : contract (parameter) = get_contract (dest);
(* Reuse the parameter in the subsequent
transaction or use another one, `mock_param`. *)
const mock_param : parameter = Increment (5n);
const op : operation = transaction (param, 0mutez, counter);
const ops : list (operation) = list [op]
} with (ops, store)
// counter.mligo
type paramater =
Increment of nat
| Decrement of nat
| Reset
// ...
// proxy.mligo
type parameter =
Increment of nat
| Decrement of nat
| Reset
type storage = unit
type return = operation list * storage
let dest : address = ("KT19wgxcuXG9VH4Af5Tpm1vqEKdaMFpznXT3" : address)
let proxy (param, store : parameter * storage) : return =
let counter : parameter contract = Operation.get_contract dest in
(* Reuse the parameter in the subsequent
transaction or use another one, `mock_param`. *)
let mock_param : parameter = Increment (5n) in
let op : operation = Operation.transaction param 0mutez counter
in [op], store
// counter.religo
type parameter =
| Increment (nat)
| Decrement (nat)
| Reset
// ...
// proxy.religo
type parameter =
| Increment (nat)
| Decrement (nat)
| Reset;
type storage = unit;
type return = (list (operation), storage);
let dest : address = ("KT19wgxcuXG9VH4Af5Tpm1vqEKdaMFpznXT3" : address);
let proxy = ((param, store): (parameter, storage)) : return => {
let counter : contract (parameter) = Operation.get_contract (dest);
(* Reuse the parameter in the subsequent
transaction or use another one, `mock_param`. *)
let mock_param : parameter = Increment (5n);
let op : operation = Operation.transaction (param, 0mutez, counter);
([op], store)
};