You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
arc: 9
title: Dynamic Dispatch
authors: @ProvableHQ
discussion: TODO
topic: Protocol
status: Draft
created: December 11, 2025
Abstract
Static function calls in the Aleo Virtual Machine (AVM) require programs to know at compile time which functions they will invoke. While this provides strong guarantees, it limits the expressiveness of Aleo programs and prevents common smart contract patterns found in other ecosystems.
This proposal introduces dynamic dispatch to the AVM, enabling programs to invoke functions determined at runtime. Dynamic dispatch unlocks powerful programming paradigms including interfaces, plugin architectures and upgradeable libraries—while maintaining the security and privacy guarantees that define Aleo.
Goals
We propose a system for dynamic dispatch with the following properties:
Dispatch should be secure. Dynamic dispatch has enabled rich application ecosystems in other VMs, but has also introduced vulnerabilities like code hijacking and reentrancy bugs. Security is the first priority.
Dispatch should be flexible. The core purpose of dynamic dispatch is to allow calling multiple programs under a single interface. The more flexible the dispatch, the more utility it provides.
Dispatch should be backwards compatible. This feature should not change the circuits or behavior of existing programs. Functions with dynamic calls should be able to invoke functions with static calls and vice versa. Existing records must be usable in dynamic dispatch.
Dispatch should preserve privacy. Dynamic calls should not leak unnecessary information about the nature of the call beyond what is inherently revealed by the resulting transitions.
Specification
Data Types
Dynamic dispatch introduces two new data types to handle AVM-native constructs that require special treatment when passed across dynamic call boundaries.
record.dynamic
A record.dynamic is a fixed-size, general representation of a record. Unlike static records, a dynamic record:
Has a constant size regardless of its data contents
Is NOT checked for existence or uniqueness at the call site
Can be instantiated as a concrete record when passed to the appropriate function
Structure:
Field
Type
Description
owner
address
The owner of the record
_root
field
Merkle root of the record's data entries, including the entry name and visibility
Important: A record.dynamic cannot be instantiated directly within a program. It can only be:
Received as input to a function
Returned from a call.dynamic
Created via the cast instruction from a static record
Record consumption: A record is only consumed (nullified) when it is input to a function that expects its static definition. Functions that accept record.dynamic or external records do NOT consume the record or verify ownership.
Record creation: A record is only created (its commitment and
ciphertext being published onto the ledger) when it is output by a function that
declares its (static) type as an output. Functions that output record.dynamic or
external records do NOT create an on-chain record that can be subsequently
consumed later on.
future.dynamic
A future.dynamic is a fixed-size representation of a future returned from a dynamic call. It is produced exclusively as a result of call.dynamic when the callee returns a future.
When await is invoked on a dynamic future, the runtime looks up the corresponding future in the global context, retrieves the static future, and executes it. The runtime guarantees that all futures (static and dynamic) are awaited exactly once.
Note: A future.dynamic cannot be directly output by a function; it must be wrapped in a static future via the async instruction.
Instructions
call.dynamic
The call.dynamic instruction invokes a function determined at runtime.
Syntax:
call.dynamic <PROG> <NET> <FUN> with <INPUTS> (as <INPUT_TYPES>) into <OUTPUTS> (as <OUTPUT_TYPES>);
The call.dynamic instruction can only be used in function bodies, not in finalize blocks
The program, network, and function identifiers are witnessed as public inputs. Consequently, private variables that influence the target of a dynamic call are implicitly leaked. Developers should take care to ensure that sensitive material is not used to determine the target.
The existence of the target program and function is verified at execution time
A transition/request associated with a dynamic call is a "dynamic" transition/request. They are differentiated with additional (unsigned) metadata.
Note: Dynamic targets cannot be resolved from mappings at runtime. Callers must provide target identifiers as inputs (queried off-chain), and on-chain logic can verify these match stored values.
get.record.dynamic
Retrieves a specific entry from a dynamic record by name.
Syntax:
get.record.dynamic r<i>.<name> into r<j> as <TYPE>;
Circuit behavior: The instruction verifies a Merkle proof that the requested entry exists in the record's data tree with the specified identifier and type.
Note: The owner of a dynamic record can be accessed directly, e.g r0.owner. This does not involve Merkle-path verification.
cast ... as record.dynamic
Converts a static record to a dynamic record.
Syntax:
cast r<i> into r<j> as record.dynamic;
The input register must contain a record or external record. The output is a record.dynamic with:
The same owner, nonce, and version as the static record
A _root computed as the Merkle root of the record's entries
Important limitation: A dynamic record created via cast cannot be passed to a function expecting a static record, as this would constitute either a double-spend (if the original was an input) or an attempt to spend an uncommitted record (if the original was created locally). Furthermore, casting a static record into a dynamic one, does not consume it.
Dynamic Mapping Operations
Three finalize-scope commands enable dynamic access to mappings:
get.dynamic
get.dynamic <PROG> <NET> <MAP>[<KEY>] into <DEST> as <TYPE>;
get.or_use.dynamic
get.or_use.dynamic <PROG> <NET> <MAP>[<KEY>] <DEFAULT> into <DEST> as <TYPE>;
contains.dynamic
contains.dynamic <PROG> <NET> <MAP>[<KEY>] into <DEST>;
These commands behave identically to their static counterparts, except the program and mapping are resolved from field element operands at runtime.
Identifier Literals
To improve ergonomics, this proposal introduces a syntax for specifying identifiers as field element literals using single quotes:
'credits' // Program name
'aleo' // Network name
'transfer_public' // Function name
The identifier (excluding quotes) must fit within Field::SIZE_IN_DATA_BITS.
To support this, we will be introducing a new literal type called identifier. An identifier can always be cast into a field element. This allows users to pass in human readable targets to call.dynamic.
Note: In examples throughout this document, 'aleo' refers to the network identifier (e.g., mainnet). The network parameter allows programs to target functions on specific networks.
Translation Circuits
When records cross dynamic call boundaries, a translation circuit proves consistency between the dynamic and static representations. The AVM introduces a new circuit type parameterized by each record definition in a program.
Translation is required when:
A caller passes a record.dynamic to a callee expecting a static record or external record
A callee returns a static record or external record to a caller expecting a record.dynamic
The translation circuit verifies:
Owner, nonce, and version match between representations
The Merkle root of the static record's data equals the dynamic record's _root
The record IDs (commitments or serial numbers) are correctly computed
Deployment: Programs must include translation verifying keys for each record type they define. Programs deployed before this feature can be redeployed to add translation keys.
Spec
The following specification details the circuit inputs and constraints for protocol implementers.
Input
constant program_id (ProgramID): The ID of the program where the static record is defined.
constant record_name (Identifier): The name of the static record (irrelevant if static_is_external is true.)
public is_input (bool): True iff translation is happening for an input to dynamic.call
public static_is_external (bool): Whether the value type corresponding to the static record is Record or ExternalRecord.
public function_id (Field): The function ID of the callee in the dynamic call.
public input_output_index (u16): Index of the input operand or output destination that contains the (dynamic and static) record.
public translation_index (u16): The index of the current translation invocation.
public id_dynamic (Field): The ID of the dynamic record.
public id_static (Field):
If the static record is external, this is its InputID = OutputID.
If the static record is not external, this is
Its InputID, i. e. its serial number, if the record is an input.
Its OutputID, i. e. its commitment, if the record is an output.
private R_s (Record): The static record (whether external or not).
private R_d (DynamicRecord): The dynamic record.
private tvk (Field): The view key of the transition containing the dynamic call
private record_view_key (Field): The record view key of the static record (irrelevant if static_is_external is true)
private gamma (Group): The additional point used to produce the serial number (irrelevant if is_input is false or static_is_external is true).
Compute
cm := commit(program_id, record_name, record_view_key, R_s)
This section summarizes when type translation occurs between dynamic and static representations.
Static calls (call)
Types must match exactly. A record.dynamic can only be passed to or received from a function that explicitly declares record.dynamic in its signature.
Dynamic calls (call.dynamic)
Types must match except for these automatic translations:
Caller declares
Callee declares
Translation
record.dynamic
record or external record
dynamic → static
record.dynamic
record.dynamic
none
future.dynamic
future
dynamic → static
Root transitions
Root transitions never perform translation. Arguments must match declared types exactly.
A root transition can accept record.dynamic if declared—the key point is that translation only occurs at call.dynamic boundaries, not at the program entry point.
Usage
Dynamic dispatch enables programming patterns previously impossible or impractical on Aleo. This section illustrates key use cases.
Interfaces and Polymorphism
Dynamic dispatch enables interface-like patterns where multiple programs implement a common function signature.
Token Interface Pattern:
Any program implementing transfer, approve, and balance_of with compatible signatures can be used interchangeably:
program token_router.aleo;
// Route a transfer to any compatible token programfunction transfer:
inputr0asfield.public; // token program nameinputr1asaddress.public; // recipientinputr2asu64.public; // amountcall.dynamicr0'aleo''transfer'withr1r2 (asaddress.publicu64.public) intor3 (asfuture.dynamic);
async transfer r3intor4;
outputr4as token_router.aleo/transfer.future;
finalize transfer:
inputr0asfuture.dynamic;
awaitr0;
This pattern mirrors ERC-20 compatibility in Ethereum, allowing DEXs, lending protocols, and other DeFi applications to work with any compliant token.
Record Interface Pattern:
The get.record.dynamic instruction can enforce that a record.dynamic contains specific fields, regardless of which program defined it. When get.record.dynamic is executed, it verifies a Merkle proof that the requested entry exists in the record's data tree with the specified identifier and type. If the field doesn't exist or has an incompatible type, the proof fails.
This enables programs to define implicit interfaces for records:
program collateral_manager.aleo;
// Accept any record that has 'owner' and 'value' fields// The record can come from any token programfunction deposit_collateral:
inputr0asrecord.dynamic; // any record with required fields// Extract required fields - this enforces the interface// If the record lacks these fields, the Merkle proof failsget.record.dynamicr0.ownerintor1asaddress;
get.record.dynamicr0.valueintor2asu64;
// Use the extracted valuesassert.eqr1self.caller; // caller must own the recordgter21000u64intor3; // minimum collateral checkassert.eqr3true;
// ... rest of collateral logic using r1 (owner) and r2 (value) ...
Any record definition that includes owner as address and value as u64 fields satisfies this interface:
// These records from different programs all satisfy the interface:// token_a.aleorecord credit:
ownerasaddress.private;
valueasu64.private;
// token_b.aleorecord coin:
ownerasaddress.private;
valueasu64.private;
memo asfield.private; // extra fields are allowed// nft.aleorecord asset:
ownerasaddress.private;
valueasu64.private; // could represent appraised value
token_id asu128.private;
This pattern is analogous to structural typing or duck typing. Combined with call.dynamic, this enables generic protocols that work with any compliant record type without needing to know the specific program at compile time. Note that the presence of a dynamic record does not imply the existence of a corresponding minted, unspent record. A dynamic record needs to be translated and passed into a function that expects a (non-external) static record for the existence check to happen.
Strategy Pattern (Library Dispatch)
Dynamic dispatch enables the strategy pattern, where a program delegates computation to interchangeable library programs that implement a common interface. Since call.dynamic executes in the callee's context (there is no shared state), this pattern works best for pure computational functions.
Example: Pluggable Pricing Algorithms
A DEX can support multiple pricing algorithms (constant product, stableswap, concentrated liquidity) by dispatching to different library programs:
// Library interface: compute_output(reserve_in, reserve_out, amount_in) -> amount_out// Each pricing library implements this interface as a pure functionprogram dex.aleo;
function swap:
inputr0asfield.public; // pricing_library program nameinputr1asu64.public; // reserve_ininputr2asu64.public; // reserve_outinputr3asu64.public; // amount_in// Dispatch to the pricing library to compute output amount// More complex logic could be introduced to determine the library used. For example, checking an auth token.call.dynamicr0'aleo''compute_output'withr1r2r3 (asu64.publicu64.publicu64.public) intor4 (asu64.public);
// Use computed amount_out for the swap// ... transfer logic using r4 ...outputr4asu64.public;
Constant Product Library:
program constant_product_lib.aleo;
// x * y = k pricing: amount_out = (reserve_out * amount_in) / (reserve_in + amount_in)function compute_output:
inputr0asu64.public; // reserve_ininputr1asu64.public; // reserve_outinputr2asu64.public; // amount_inmulr1r2intor3; // reserve_out * amount_inaddr0r2intor4; // reserve_in + amount_indivr3r4intor5; // amount_outoutputr5asu64.public;
Swappable algorithms without redeploying the main program
Third parties can deploy new pricing libraries
Main program can allowlist approved libraries via finalize verification
Conditional Execution
The Strategy Pattern above is actually a special case of conditional execution.
Dynamic dispatch enables runtime branching between different execution paths:
program router.aleo;
function conditional_call:
inputr0asboolean.private; // conditioninputr1asfield.public; // program if trueinputr2asfield.public; // program if falseinputr3asfield.public; // function nameinputr4asu64.public; // argumentternaryr0r1r2intor5; // select program based on conditioncall.dynamicr5'aleo'r3withr4 (asu64.public) intor6 (asfuture.dynamic);
async conditional_call r6intor7;
outputr7as router.aleo/conditional_call.future;
finalize conditional_call:
inputr0asfuture.dynamic;
awaitr0;
Privacy note: The resulting transaction will reveal which program was actually called. The condition itself may be inferred from the transaction structure.
Recursive Calls
Note: The restrictions on recursive calls are currently under review. These may or may not be allowed in the final version.
With conditional execution, it follows that dynamic dispatch enables recursive program structures:
program recursive.aleo;
function fibonacci:
inputr0asu64.private;
// Base case checkis.eqr00u64intor1;
is.eqr01u64intor2;
orr1r2intor3;
// Recursive case setupsub.wr01u64intor4;
sub.wr02u64intor5;
// Select function based on base caseternaryr3'base''fibonacci'intor6;
// Recursive callscall.dynamic'recursive''aleo'r6withr4 (asu64.private) intor7 (asu64.private);
call.dynamic'recursive''aleo'r6withr5 (asu64.private) intor8 (asu64.private);
// Combine resultsaddr7r8intor9;
ternaryr3r7r9intor10;
outputr10asu64.private;
function base:
inputr0asu64.private;
outputr0asu64.private;
Universal Token Operations
A unified interface for interacting with any token:
program universal_swap.aleo;
struct SwapRequest:
token_in asfield; // input token program
token_out asfield; // output token program
amount_in asu64;
min_amount_out asu64;
recipient asaddress;
function swap:
inputr0as SwapRequest.public;
inputr1asrecord.dynamic; // input token record// Transfer input tokens to this contractcall.dynamicr0.token_in 'aleo''transfer'withr1self.addressr0.amount_in
(asrecord.dynamic address.publicu64.public)
intor2r3 (asrecord.dynamic future.dynamic);
// ... pricing logic and output transfer ...async swap r3intor4;
outputr2asrecord.dynamic;
outputr4as universal_swap.aleo/swap.future;
finalize swap:
inputr0asfuture.dynamic;
awaitr0;
Extension Hooks
Programs can support extensibility through hooks that modify behavior at defined extension points. Since call.dynamic executes in the callee's context with no shared state, hooks work best as pure functions that return values the caller uses:
program lending.aleo;
function borrow:
inputr0asfield.public; // fee_calculator program (hook)inputr1asu64.public; // collateral_amountinputr2asu64.public; // borrow_amountinputr3asaddress.public; // borrower// Call the fee calculator hook - returns the fee to charge// Different hooks can implement different fee strategiescall.dynamicr0'aleo''calculate_fee'withr1r2 (asu64.publicu64.public) intor4 (asu64.public);
// Use the hook's result in the main logicaddr2r4intor5; // total_debt = borrow_amount + fee// ... rest of borrow logic using r5 ...async borrow r0r5intor6;
outputr6as lending.aleo/borrow.future;
finalize borrow:
inputr0asfield.public;
inputr1asu64.public;
// Optionally verify the hook is on an allowlist// ... allowlist check ...
Core implementation of dynamic dispatch, translation circuits, new data types
snarkOS
Transaction validation, future resolution during finalization
Leo
Language support for dynamic types and instructions
Aleo SDK
Client-side support for constructing dynamic calls
Backwards Compatibility
Existing programs are unaffected. Dynamic dispatch does not change the circuits or behavior of programs that do not use it.
Interoperability:
Functions with dynamic calls can invoke functions with static calls
Functions with static calls can be invoked by functions with dynamic calls
Existing records can be used with dynamic dispatch after one-time deployment of translation keys
Migration for existing programs: We plan to support a migration pathway for all existing programs to redeploy with translation verifying keys, without changing the state of the program on-chain.
Consensus versioning: Features are guarded by consensus version to ensure backwards compatibility for node operators.
Security & Compliance
For Application Developers
Record Ownership and Consumption
A record is only consumed (nullified) when passed as a static record input. This has important implications:
Functions accepting record.dynamic or external record do NOT verify ownership or consume the record
Passing an untrusted record.dynamic that is never converted to a static record provides no ownership guarantees
Always convert to static records when ownership verification is required
Information Leakage
Dynamic calls reveal information through transaction structure:
The target program and function are publicly visible in transitions
Observers can determine which branch was taken in conditional execution
The number and structure of dynamic calls may leak information about program logic
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
arc: 9
title: Dynamic Dispatch
authors: @ProvableHQ
discussion: TODO
topic: Protocol
status: Draft
created: December 11, 2025
Abstract
Static function calls in the Aleo Virtual Machine (AVM) require programs to know at compile time which functions they will invoke. While this provides strong guarantees, it limits the expressiveness of Aleo programs and prevents common smart contract patterns found in other ecosystems.
This proposal introduces dynamic dispatch to the AVM, enabling programs to invoke functions determined at runtime. Dynamic dispatch unlocks powerful programming paradigms including interfaces, plugin architectures and upgradeable libraries—while maintaining the security and privacy guarantees that define Aleo.
Goals
We propose a system for dynamic dispatch with the following properties:
Dispatch should be secure. Dynamic dispatch has enabled rich application ecosystems in other VMs, but has also introduced vulnerabilities like code hijacking and reentrancy bugs. Security is the first priority.
Dispatch should be flexible. The core purpose of dynamic dispatch is to allow calling multiple programs under a single interface. The more flexible the dispatch, the more utility it provides.
Dispatch should be backwards compatible. This feature should not change the circuits or behavior of existing programs. Functions with dynamic calls should be able to invoke functions with static calls and vice versa. Existing records must be usable in dynamic dispatch.
Dispatch should preserve privacy. Dynamic calls should not leak unnecessary information about the nature of the call beyond what is inherently revealed by the resulting transitions.
Specification
Data Types
Dynamic dispatch introduces two new data types to handle AVM-native constructs that require special treatment when passed across dynamic call boundaries.
record.dynamicA
record.dynamicis a fixed-size, general representation of a record. Unlike static records, a dynamic record:Structure:
owneraddress_rootfield_noncegroup_versionu8String representation:
Usage contexts:
A
record.dynamiccan appear in:input r0 as record.dynamic;output r0 as record.dynamic;callorcall.dynamiccallorcall.dynamicImportant: A
record.dynamiccannot be instantiated directly within a program. It can only be:call.dynamiccastinstruction from a static recordRecord consumption: A record is only consumed (nullified) when it is input to a function that expects its static definition. Functions that accept
record.dynamicor external records do NOT consume the record or verify ownership.Record creation: A record is only created (its commitment and
ciphertext being published onto the ledger) when it is output by a function that
declares its (static) type as an output. Functions that output record.dynamic or
external records do NOT create an on-chain record that can be subsequently
consumed later on.
future.dynamicA
future.dynamicis a fixed-size representation of a future returned from a dynamic call. It is produced exclusively as a result ofcall.dynamicwhen the callee returns a future.Structure:
program_namefieldprogram_networkfieldfunction_namefieldrootfieldString representation:
Awaiting a future.dynamic:
Like static futures, a
future.dynamicmust be consumed by anasyncinstruction and awaited in the corresponding finalize block:When
awaitis invoked on a dynamic future, the runtime looks up the corresponding future in the global context, retrieves the static future, and executes it. The runtime guarantees that all futures (static and dynamic) are awaited exactly once.Note: A
future.dynamiccannot be directly output by a function; it must be wrapped in a static future via theasyncinstruction.Instructions
call.dynamicThe
call.dynamicinstruction invokes a function determined at runtime.Syntax:
<PROG>field<NET>field<FUN>field<INPUTS><INPUT_TYPES><OUTPUTS><OUTPUT_TYPES>Example:
Type restrictions:
record.dynamicrecord, external record,future,future.dynamicrecord.dynamic,future.dynamicrecord, external record,futureBehavior:
call.dynamicinstruction can only be used in function bodies, not in finalize blocksNote: Dynamic targets cannot be resolved from mappings at runtime. Callers must provide target identifiers as inputs (queried off-chain), and on-chain logic can verify these match stored values.
get.record.dynamicRetrieves a specific entry from a dynamic record by name.
Syntax:
r<i>record.dynamic<name>r<j><TYPE>Example:
Circuit behavior: The instruction verifies a Merkle proof that the requested entry exists in the record's data tree with the specified identifier and type.
Note: The owner of a dynamic record can be accessed directly, e.g
r0.owner. This does not involve Merkle-path verification.cast ... as record.dynamicConverts a static record to a dynamic record.
Syntax:
The input register must contain a
recordor external record. The output is arecord.dynamicwith:owner,nonce, andversionas the static record_rootcomputed as the Merkle root of the record's entriesImportant limitation: A dynamic record created via
castcannot be passed to a function expecting a static record, as this would constitute either a double-spend (if the original was an input) or an attempt to spend an uncommitted record (if the original was created locally). Furthermore, casting a static record into a dynamic one, does not consume it.Dynamic Mapping Operations
Three finalize-scope commands enable dynamic access to mappings:
get.dynamicget.or_use.dynamiccontains.dynamicThese commands behave identically to their static counterparts, except the program and mapping are resolved from field element operands at runtime.
Identifier Literals
To improve ergonomics, this proposal introduces a syntax for specifying identifiers as field element literals using single quotes:
The identifier (excluding quotes) must fit within
Field::SIZE_IN_DATA_BITS.To support this, we will be introducing a new literal type called
identifier. Anidentifiercan always be cast into a field element. This allows users to pass in human readable targets tocall.dynamic.Note: In examples throughout this document,
'aleo'refers to the network identifier (e.g., mainnet). The network parameter allows programs to target functions on specific networks.Translation Circuits
When records cross dynamic call boundaries, a translation circuit proves consistency between the dynamic and static representations. The AVM introduces a new circuit type parameterized by each record definition in a program.
Translation is required when:
record.dynamicto a callee expecting a staticrecordor external recordrecordor external record to a caller expecting arecord.dynamicThe translation circuit verifies:
_rootDeployment: Programs must include translation verifying keys for each record type they define. Programs deployed before this feature can be redeployed to add translation keys.
Spec
The following specification details the circuit inputs and constraints for protocol implementers.
Input
constant program_id(ProgramID): The ID of the program where the static record is defined.constant record_name(Identifier): The name of the static record (irrelevant ifstatic_is_externalis true.)public is_input(bool): True iff translation is happening for an input to dynamic.callpublic static_is_external(bool): Whether the value type corresponding to the static record is Record or ExternalRecord.public function_id(Field): The function ID of the callee in the dynamic call.public input_output_index(u16): Index of the input operand or output destination that contains the (dynamic and static) record.public translation_index(u16): The index of the current translation invocation.public id_dynamic(Field): The ID of the dynamic record.public id_static(Field):private R_s(Record): The static record (whether external or not).private R_d(DynamicRecord): The dynamic record.private tvk(Field): The view key of the transition containing the dynamic callprivate record_view_key(Field): The record view key of the static record (irrelevant if static_is_external is true)private gamma(Group): The additional point used to produce the serial number (irrelevant if is_input is false or static_is_external is true).Compute
cm := commit(program_id, record_name, record_view_key, R_s)sn := serial_number(cm, gamma)id_static_non_external := is_input ? sn : cmid_static_external := HashPSD8(function_id | R_s | tvk | input_output_index)id_static’ := static_is_external ? id_static_external : id_static_non_externalid_dynamic’ := HashPSD8(transition_function_id | R_d | tvk | input_output_index)Check
R_s.owner == R_d.ownerR_s.nonce == R_d.nonceR_s.version == R_d.versionmerkle(R_s.data) == R_d.rootid_static == id_static’id_dynamic == id_dynamic’Type Translation Between Caller and Callee
This section summarizes when type translation occurs between dynamic and static representations.
Static calls (
call)Types must match exactly. A
record.dynamiccan only be passed to or received from a function that explicitly declaresrecord.dynamicin its signature.Dynamic calls (
call.dynamic)Types must match except for these automatic translations:
record.dynamicrecordor external recordrecord.dynamicrecord.dynamicfuture.dynamicfutureRoot transitions
Root transitions never perform translation. Arguments must match declared types exactly.
A root transition can accept
record.dynamicif declared—the key point is that translation only occurs atcall.dynamicboundaries, not at the program entry point.Usage
Dynamic dispatch enables programming patterns previously impossible or impractical on Aleo. This section illustrates key use cases.
Interfaces and Polymorphism
Dynamic dispatch enables interface-like patterns where multiple programs implement a common function signature.
Token Interface Pattern:
Any program implementing
transfer,approve, andbalance_ofwith compatible signatures can be used interchangeably:This pattern mirrors ERC-20 compatibility in Ethereum, allowing DEXs, lending protocols, and other DeFi applications to work with any compliant token.
Record Interface Pattern:
The
get.record.dynamicinstruction can enforce that arecord.dynamiccontains specific fields, regardless of which program defined it. Whenget.record.dynamicis executed, it verifies a Merkle proof that the requested entry exists in the record's data tree with the specified identifier and type. If the field doesn't exist or has an incompatible type, the proof fails.This enables programs to define implicit interfaces for records:
Any record definition that includes
owner as addressandvalue as u64fields satisfies this interface:This pattern is analogous to structural typing or duck typing. Combined with
call.dynamic, this enables generic protocols that work with any compliant record type without needing to know the specific program at compile time. Note that the presence of a dynamic record does not imply the existence of a corresponding minted, unspent record. A dynamic record needs to be translated and passed into a function that expects a (non-external) static record for the existence check to happen.Strategy Pattern (Library Dispatch)
Dynamic dispatch enables the strategy pattern, where a program delegates computation to interchangeable library programs that implement a common interface. Since
call.dynamicexecutes in the callee's context (there is no shared state), this pattern works best for pure computational functions.Example: Pluggable Pricing Algorithms
A DEX can support multiple pricing algorithms (constant product, stableswap, concentrated liquidity) by dispatching to different library programs:
Constant Product Library:
Stableswap Library:
This pattern enables:
Conditional Execution
The Strategy Pattern above is actually a special case of conditional execution.
Dynamic dispatch enables runtime branching between different execution paths:
Privacy note: The resulting transaction will reveal which program was actually called. The condition itself may be inferred from the transaction structure.
Recursive Calls
Note: The restrictions on recursive calls are currently under review. These may or may not be allowed in the final version.
With conditional execution, it follows that dynamic dispatch enables recursive program structures:
Universal Token Operations
A unified interface for interacting with any token:
Extension Hooks
Programs can support extensibility through hooks that modify behavior at defined extension points. Since
call.dynamicexecutes in the callee's context with no shared state, hooks work best as pure functions that return values the caller uses:Standard Fee Hook:
Risk-Based Fee Hook:
This pattern enables:
Test Cases
Implementations must correctly handle:
record.dynamicto functions expecting static recordsfuture.dynamicin finalize blocksReference Implementation
The reference implementation can be found in the snarkVM repository:
feat/dynamic-dispatchDependencies
This proposal affects:
Backwards Compatibility
Existing programs are unaffected. Dynamic dispatch does not change the circuits or behavior of programs that do not use it.
Interoperability:
Migration for existing programs: We plan to support a migration pathway for all existing programs to redeploy with translation verifying keys, without changing the state of the program on-chain.
Consensus versioning: Features are guarded by consensus version to ensure backwards compatibility for node operators.
Security & Compliance
For Application Developers
Record Ownership and Consumption
A record is only consumed (nullified) when passed as a static record input. This has important implications:
record.dynamicor external record do NOT verify ownership or consume the recordrecord.dynamicthat is never converted to a static record provides no ownership guaranteesInformation Leakage
Dynamic calls reveal information through transaction structure:
Reentrancy Considerations
Dynamic dispatch introduces potential reentrancy vectors:
Interface Validation
The VM enforces type compatibility at the call boundary, but:
Future Execution Order
When multiple
future.dynamicvalues are awaited:awaitstatementsFor Protocol Implementers
References
All reactions