Dynamic entrypoints
Dynamic entrypoints are lambda functions stored in a contract within a big_map. A contract can update its dynamic entrypoints without deploying a new contract. In this way, contracts can use dynamic entrypoints to change their internal logic.
Only the contract itself can call its dynamic entrypoints, not clients or other contracts. If you want a client or other contract to be able to call a dynamic entrypoint, you can create an ordinary non-dynamic entrypoint as a wrapper for the dynamic entrypoint.
A contract with dynamic entrypoints must have at least one non-dynamic entrypoint with the @entry
declaration, like any other contract.
They must also obey the following convention on storage type definition and have at least one function with the @dyn_entry
declaration.
Storage
To contain dynamic entrypoints, the contract storage must be a record with two fields:
storage
: The contract's storage, just like in an ordinary contractdynamic_entrypoints
: The code of the dynamic entrypoints, which must be of the typebig_map<nat, bytes>
For convenience, the type of the dynamic_entrypoints
storage field is defined as Dynamic_entrypoints.t
in the standard library.
The LIGO compiler can generate the initial value of the storage for you; see Compiling dynamic entrypoints.
Defining dynamic entrypoints
To set the dynamic entrypoints that the contract has at origination time, define functions at the top level of the contract just like ordinary entrypoints but with these differences:
- They have the
@dyn_entry
decorator instead of the@entry
decorator - They must be exported with the
export
keyword - Their storage and return types are different, as explained next
Ordinary entrypoints in the contract receive a parameter and the value of the contract storage as usual, which, as described in Storage, is always a record with a storage
field and a dynamic_entrypoints
field.
Also as usual, the ordinary entrypoints return a tuple with a list of operations and the new value of the storage, including both of these fields.
By contrast, dynamic entrypoints receive a parameter and the storage
field of the contract storage, not the dynamic_entrypoints
field.
Similarly, they return a tuple with a list of operations and the new value of that storage
field.
Smart contracts cannot add dynamic entrypoints after the contract is originated. Therefore, you must define all dynamic entrypoints in the contract source code.
For example, this contract stores an integer but none of its ordinary entrypoints change it directly. Instead, it contains dynamic entrypoints that manipulate it:
- The
double
dynamic entrypoint doubles the value in storage - The
square
dynamic entrypoint squares the value in storage - The
currentAction
dynamic entrypoint initially does nothing
The contract has two ordinary entrypoints that manipulate the dynamic entrypoints:
- The
runAction
entrypoint runs thecurrentAction
dynamic entrypoint and updates the contract storage based on its result - The
changeAction
entrypoint changes thecurrentAction
dynamic entrypoint to be a copy of either thedouble
or thesquare
entrypoint
Calling dynamic entrypoints
To call a dynamic entrypoint, use the function Dynamic_entrypoints.get
to retrieve a dynamic entrypoint by name.
The function returns an option that contains a function that you can call like any other function.
This section from an earlier example attempts to retrieve the dynamic entrypoint named currentAction
and runs it if it is found:
Contracts cannot call dynamic entrypoints directly because they are stored as typed keys in the dynamic_entrypoints
big map, not as functions.
LIGO uses an abstract type dynamic_entrypoint<a, b>
to denote such keys.
Updating dynamic entrypoints
After the contract is deployed, it can update its dynamic entrypoints by rewriting the dynamic_entrypoints
field in its storage.
You can use the Dynamic_entrypoints.set
function to get the updated value of this field by passing the current value, the name of a dynamic entrypoint to update, and the code of that dynamic entrypoint.
This function does not directly update the contract storage; you must use its return value as the new value of the dynamic_entrypoints
field in the entrypoint return value.
This section from an earlier example changes the entrypoint named currentAction
to the code of the entrypoint named square
:
You can also set a dynamic entrypoint to new code by passing bytecode to the Dynamic_entrypoints.set_bytes
function.
This function does not verify that the bytecode is valid, only that it is a valid LIGO bytes data type.
If the encoding is wrong, any call to Dynamic_entrypoints.get
for the dynamic entrypoint fails.
For example, the dynamic entrypoint double
from an earlier example compiles to the bytecode 0x0502000000250320093100000019035b0765055f036d035b020000000a03210312053d036d034200000000
.
To set a dynamic entrypoint to this bytecode, pass the name of the entrypoint and the bytecode to the Dynamic_entrypoints.set_bytes
function, as in this example:
Opted out dynamic entrypoints
Because a contract cannot add dynamic entrypoints, you must define all dynamic entrypoints in the contract source code.
If you want to include a dynamic entrypoint in the contract but provide the code for it later, you can make the function a no-op (a function that does nothing) and change it later or you can use the special expression OPT_OUT_ENTRY
.
This expression makes the LIGO compiler include the entrypoint without any initial code.
If you call the entrypoint in a test before setting code for it, it behaves as a no-op.
If you call the entrypoint in a deployed contract before setting code for it, the operation fails.
When you run a test with an opted out dynamic entrypoint like this, the compiler prints the message unsupported primitive OPT_OUT_ENTRY
.
You can safely ignore this message.
Compiling dynamic entrypoints
When you compile the storage for a contract with dynamic entrypoints, you provide only the value of the storage
field, not the full value of the storage.
The compiler automatically compiles the dynamic entrypoints and provides the full value of the storage including the dynamic_entrypoints
field.
For example, to compile the example contract in Defining dynamic entrypoints, pass only the value of the integer in storage, as in this example:
The response is the full value of the contract storage. You can use this value as the initial storage when you originate the contract.
Testing dynamic entrypoints
To simplify testing contracts with dynamic entrypoints, you can use the function Test.Next.storage_with_dynamic_entrypoints
to generate the initial storage.
Like the ligo compile storage
command, this function takes only the value of the storage
field in the contract storage, not the full storage value.
It returns the full storage value with both the storage
and dynamic_entrypoints
field.
Then you can use this return value to originate the contract in the test.
After origination, testing a contract with dynamic entrypoints is the same as testing any other contract. You cannot call the dynamic entrypoints directly in a test because this is the same behavior that the originated contract has.
For example, this is a test for the contract in Defining dynamic entrypoints: