NAC Usage: EVM to Michelson
Cross-interface calls go through the gateway contract of the caller's interface. This page describes how to initiate a cross-interface call from the EVM interface. For the opposite way, see Michelson to EVM.
The EVM-to-Michelson gateway is a precompile deployed at:
0xff00000000000000000000000000000000000007
callMichelson
Call a Michelson contract by providing:
| Parameter | Type | Description |
|---|---|---|
destination | string | The Michelson contract address (KT1… base58check) |
entrypoint | string | The target entrypoint name |
data | bytes | Michelson-encoded parameters |
A Michelson KT1… address is 22 bytes in base58check form and does not fit into a 20-byte EVM address type. The gateway therefore takes the destination as a string and parses it on the Michelson side.
interface INativeAtomicGateway {
function callMichelson(
string calldata destination,
string calldata entrypoint,
bytes calldata data
) external payable;
}
INativeAtomicGateway gateway = INativeAtomicGateway(
0xff00000000000000000000000000000000000007
);
gateway.callMichelson("KT1…", "default", michelsonParams);
The data parameter must be encoded in Michelson binary format. For simple types you can construct the payload inline: for example, passing a nat with value 42 encodes as hex"002a" (tag 0x00 = integer, value 0x2a = 42). Worked Solidity examples using this pattern are available in the solidity_examples/ directory of the Etherlink sources. For complex types, off-line tools such as Taquito's packData function can compute the encoding.
Return value
In the case of a regular Michelson call (callMichelson), there is no return value.
callMichelsonView
To call a read-only Michelson view and receive the result, use callMichelsonView:
| Parameter | Type | Description |
|---|---|---|
destination | string | The Michelson contract address (KT1… base58check) |
viewName | string | The name of the on-chain view |
input | bytes | Micheline-encoded input to the view |
This entry point performs a read-only crossing (no value transfer, no state mutation) and must be invoked via staticcall. The gateway returns the view's Micheline response ABI-encoded as bytes.
interface INativeAtomicGateway {
function callMichelsonView(
string calldata destination,
string calldata viewName,
bytes calldata input
) external view returns (bytes memory);
}
INativeAtomicGateway gateway = INativeAtomicGateway(
0xff00000000000000000000000000000000000007
);
bytes memory michelineResult = gateway.callMichelsonView(
"KT1…", // destination
"myView", // view name
hex"030b" // Micheline Unit — adjust to match the view's input type
);
A complete worked example (using the low-level staticcall pattern) is available in crac_michelson_view_staticcall.sol.
Address translation
Cross-interface calls run under the caller's alias (see Accounts and Aliases). The gateway exposes two view functions to translate between native addresses and aliases. Both take the address as a string in its printable form (0x… hex for the EVM interface, base58check for the Michelson interface) rather than as a Solidity address, and identify interfaces by a runtime id: 0 for the Michelson interface, 1 for the EVM interface.
interface INativeAtomicGateway {
function originOf(
string calldata addr,
uint8 sourceRuntime
) external view returns (uint8 kind, uint8 homeRuntime, string memory nativeAddress);
function resolveAddress(
string calldata addr,
uint8 sourceRuntime,
uint8 targetRuntime
) external view returns (bool classified, uint8 res, string memory translated);
error InvalidRuntimeId(uint8 received);
}
originOf
originOf(addr, sourceRuntime) returns how the address addr of the interface sourceRuntime is classified:
kind | Meaning | homeRuntime | nativeAddress |
|---|---|---|---|
0 (Unknown) | The address is malformed or has not been seen by the kernel yet | 0 | "" |
1 (Native) | An account native to sourceRuntime | sourceRuntime | addr |
2 (Alias) | The alias of an account native to the other interface | The interface the account is native to | The native address of that account |
The kernel records the origin of an account when it is first used: Michelson user accounts (tz1…, tz2…, tz3…) are always Native, Michelson smart contracts are recorded at origination, EVM accounts when they first sign a transaction or when they have code, and aliases when a cross-interface call creates them. In particular, an EVM address that has only received funds is Unknown.
For example, to check whether the caller is a Michelson account:
(uint8 kind, , string memory native) = gateway.originOf(
Strings.toHexString(msg.sender), // OpenZeppelin helper: lowercase "0x…" string
1
);
if (kind == 2) {
// msg.sender is the alias of the Michelson account `native` (tz1… or KT1…)
}
resolveAddress
resolveAddress(addr, sourceRuntime, targetRuntime) translates the address addr of the interface sourceRuntime into the corresponding address of the interface targetRuntime:
- If
addris malformed or Unknown,classifiedisfalseand the other values are zero. - If
addris Native,translatedis its alias intargetRuntime. - If
addris an Alias,translatedis the native address it stands for, rather than an alias of the alias.
res is 0 (Recorded) when the returned address already exists on chain and 1 (Derived) when it was computed with the derivation formula but no cross-interface call has created the alias yet. When sourceRuntime and targetRuntime are equal, a well-formed address is returned unchanged with res == 0.
(bool classified, uint8 res, string memory evmAlias) = gateway.resolveAddress(
"tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb", 0, 1
);
// classified == true; evmAlias is the "0x…" EVM alias of the Tezos account;
// res == 1 until a cross-interface call creates the alias
Failure behavior
callMichelson
If the Michelson callee fails for any reason — FAILWITH, type mismatch, non-existent contract, or forwarded gas exhaustion — the gateway precompile reverts the calling EVM transaction. There is no way to distinguish a Michelson-side revert from a Michelson-side out-of-gas at the Solidity level; both surface as an EVM revert with an error string of the form "Cross-runtime call failed with status 4xx: <reason>".
Because callMichelson is declared external payable (no return value), the revert propagates unconditionally to the EVM caller. To catch it without reverting your own transaction, call the precompile via a low-level call:
(bool success, ) = address(gateway).call{value: ...}(
abi.encodeWithSelector(
INativeAtomicGateway.callMichelson.selector,
destination, entrypoint, data
)
);
// success == false if Michelson reverted
callMichelsonView
callMichelsonView follows the same failure model. Any of the following causes a revert that propagates to the EVM caller:
| Cause | EVM outcome |
|---|---|
Michelson view fails (FAILWITH, etc.) | Revert |
| View name does not exist on the contract | Revert |
| Type mismatch on input | Revert |
| Forwarded gas exhausted in the Michelson view | Revert (not out-of-gas — catchable) |
Because callMichelsonView must be invoked via staticcall, catch failures with low-level staticcall:
(bool success, bytes memory result) = address(gateway).staticcall(
abi.encodeWithSelector(
INativeAtomicGateway.callMichelsonView.selector,
destination, viewName, input
)
);
// success == false if the Michelson view reverted or was not found
originOf and resolveAddress
Malformed addresses never revert: they are reported as Unknown (kind == 0) or unclassified (classified == false). Both functions revert with the custom error InvalidRuntimeId(uint8) when a runtime id is neither 0 nor 1.
Infrastructure failures
A 5xx response from the Michelson runtime indicates a kernel-internal error (storage I/O failure, host fault). This is treated as a block-level abort rather than a catchable revert, meaning the entire block is rolled back. These failures are not caused by contract logic and are not catchable by EVM code.
Observability
The EVM gateway precompile emits two events for cross-runtime calls involving the EVM interface. Both are emitted at the gateway precompile address and carry a crossRuntimeCallId field that correlates with the Michelson-side markers described in the Michelson nac-usage page.
CrossRuntimeCallSent
Emitted on every outgoing call (EVM → other runtime), before execution:
| Field | Type | Description |
|---|---|---|
crossRuntimeCallId | string | Unique identifier for this call |
targetRuntime | string | Name of the target runtime (e.g. "tezos") |
targetAddress | string | Address of the target contract |
amount | uint256 | Amount of tez forwarded with the call |
CrossRuntimeCallReceived
Emitted on every incoming call (other runtime → EVM), before execution:
| Field | Type | Description |
|---|---|---|
crossRuntimeCallId | string | Unique identifier for this call |
sourceRuntime | string | Native runtime of the originating address (follows the transitive origin on nested calls, e.g. "ethereum" for an EVM → Michelson → EVM chain) |
senderAddress | string | Immediate caller — the EVM alias of the Michelson sender |
sourceAddress | string | Transitive origin — the original address at the start of the cross-runtime chain |
targetAddress | string | Address of the called EVM contract |
amount | uint256 | Amount of tez forwarded with the call |