From 670e09a5e5ef5703a48eff02da6437930d20ce54 Mon Sep 17 00:00:00 2001 From: Nic Volanschi Date: Wed, 12 Aug 2026 17:29:38 +0200 Subject: [PATCH 1/6] document staticcall_evm --- docs/michelson/nac-usage.md | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/michelson/nac-usage.md b/docs/michelson/nac-usage.md index 336e8694..27f9d3a3 100644 --- a/docs/michelson/nac-usage.md +++ b/docs/michelson/nac-usage.md @@ -19,6 +19,44 @@ KT18oDJJKXMKhfE1bSuAPGp92pYcwVDiqsPw Call an EVM contract by providing the same parameters as the EVM-to-Michelson gateway: the destination address, entrypoint selector, and ABI-encoded data. Additionally, it accepts as an optional parameter a callback, which takes the form of a Michelson contract address. This accommodates the specifics of Michelson, in which contract calls do not return a value. +## `staticcall_evm` + +To call a read-only EVM function and receive the result, use the `staticcall_evm` on-chain view: + +| Parameter | Type | Description | +|---|---|---| +| `destination` | `string` | The EVM contract address (hex string, e.g. `"0x…"`) | +| `calldata` | `bytes` | Full ABI calldata: 4-byte function selector followed by ABI-encoded arguments | + +This view performs a read-only crossing — no value transfer, no state mutation — and returns the ABI-encoded response as `bytes`. It is invoked with the Michelson `VIEW` instruction, which yields an `option bytes`: + +```michelson +VIEW "staticcall_evm" + (pair string bytes) (* input type *) + bytes (* return type *) +``` + +Because `VIEW` returns `option`, the caller must handle the `None` case with `IF_NONE`. The kernel maps outcomes as follows: + +| EVM response | Michelson result | +|---|---| +| Success (2xx) | `Some bytes` — ABI-encoded return value | +| Revert / bad request (4xx) | `None` | +| Out of gas (429) | Operation fails with out-of-gas | +| Error (5xx) | Operation fails | + +Unlike `%call_evm`, which accepts a method signature string and computes the 4-byte Keccak256 selector internally, `staticcall_evm` requires the caller to supply the complete calldata — selector and ABI-encoded arguments already concatenated. + +```michelson +PUSH string "0x…"; (* destination *) +PUSH bytes 0x…; (* selector ++ ABI-encoded args *) +PAIR; +VIEW "staticcall_evm" bytes; +IF_NONE + { FAIL } (* revert or bad destination *) + { … } (* use the returned bytes *) +``` + ## ERC-20 wrapper For calling ERC-20 token contracts specifically, a convenience contract is available at: From cca15dc9d8842b591351f9574f8be7801d478dfe Mon Sep 17 00:00:00 2001 From: Nic Volanschi Date: Wed, 12 Aug 2026 17:48:45 +0200 Subject: [PATCH 2/6] document failure behavior in evm/nac-usage.md --- docs/evm/nac-usage.md | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/evm/nac-usage.md b/docs/evm/nac-usage.md index f0410912..3e8b9be3 100644 --- a/docs/evm/nac-usage.md +++ b/docs/evm/nac-usage.md @@ -91,3 +91,48 @@ bytes memory michelineResult = gateway.callMichelsonView( ``` A complete worked example (using the low-level `staticcall` pattern) is available in [`crac_michelson_view_staticcall.sol`](https://gitlab.com/tezos/tezos/-/blob/master/etherlink/kernel_latest/solidity_examples/crac_michelson_view_staticcall.sol). + +## 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: "`. + +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`: + +```solidity +(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`: + +```solidity +(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 +``` + +### 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. From d557fe9251f80fa027622ff7e93a852e8f989b9b Mon Sep 17 00:00:00 2001 From: Nic Volanschi Date: Wed, 12 Aug 2026 18:04:19 +0200 Subject: [PATCH 3/6] extend michelson/nac-usage.md to be on par with the inverse call --- docs/michelson/nac-usage.md | 129 +++++++++++++++++++++++++++++++----- 1 file changed, 114 insertions(+), 15 deletions(-) diff --git a/docs/michelson/nac-usage.md b/docs/michelson/nac-usage.md index 27f9d3a3..af4c1dee 100644 --- a/docs/michelson/nac-usage.md +++ b/docs/michelson/nac-usage.md @@ -14,10 +14,115 @@ The Michelson-to-EVM gateway is an enshrined contract deployed at: KT18oDJJKXMKhfE1bSuAPGp92pYcwVDiqsPw ``` -## `call_evm` +## `%call_evm` -Call an EVM contract by providing the same parameters as the EVM-to-Michelson gateway: the destination address, entrypoint selector, and ABI-encoded data. -Additionally, it accepts as an optional parameter a callback, which takes the form of a Michelson contract address. This accommodates the specifics of Michelson, in which contract calls do not return a value. +Call an EVM contract by invoking the `%call_evm` entrypoint on the gateway with the following parameters: + +| Parameter | Michelson type | Description | +|---|---|---| +| `destination` | `string` | EVM contract address (hex string, e.g. `"0x…"`) | +| `method_signature` | `string` | ABI function signature, e.g. `"transfer(address,uint256)"` | +| `abi_params` | `bytes` | ABI-encoded arguments, **without** the 4-byte selector | +| `callback` | `option (contract bytes)` | Optional contract to receive the return value; `None` for fire-and-forget | + +The full Michelson entrypoint type is: + +``` +%call_evm : + pair string + (pair string + (pair bytes + (option (contract bytes)))) +``` + +The gateway computes the 4-byte Keccak256 selector from `method_signature` and prepends it to `abi_params` before dispatching the call. Supply only the encoded arguments in `abi_params` — not the selector. + +### SmartPy example — ERC-20 `transfer` + +The following contract calls `transfer(address,uint256)` on any ERC-20 deployed on the EVM interface. The ABI encoding for the two-argument case is straightforward: each argument occupies a 32-byte slot, left-padded with zeros. + +For EVM token addresses on Mainnet, see the [token address table](/evm/developing/tokens#token-addresses) (for example, WXTZ is at `0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb`). + +```python +import smartpy as sp + +@sp.module +def main(): + # Michelson-to-EVM gateway (enshrined contract, same address on all networks). + GATEWAY = sp.address("KT18oDJJKXMKhfE1bSuAPGp92pYcwVDiqsPw") + + # Michelson type of the gateway's %call_evm entrypoint. + t_call_evm: type = sp.record( + destination = sp.string, # destination + method_signature = sp.string, # method_signature + abi_params = sp.bytes, # abi_params (no selector) + callback = sp.option[sp.bytes] # callback + ) + + class Erc20Caller(sp.Contract): + """ + Calls transfer(address,uint256) on an EVM ERC-20 contract. + + `abi_params` layout (64 bytes total, no selector): + bytes 0-11 : zero-padding for the address slot + bytes 12-31 : 20-byte EVM recipient address + bytes 32-63 : 32-byte big-endian token amount + """ + + @sp.entrypoint + def transfer_erc20( + self, + erc20: sp.string, # ERC-20 contract address, e.g. "0xc9B5…EAb" + recipient: sp.bytes, # 20-byte EVM recipient address (no 0x prefix, raw bytes) + amount: sp.bytes # 32-byte big-endian amount + ): + # ABI-encode (address, uint256): left-pad address to 32 bytes. + addr_padding = sp.bytes("0x000000000000000000000000") # 12 zero bytes + abi_parms = sp.concat([addr_padding, recipient, amount]) + + gateway = sp.contract( + t_call_evm, + GATEWAY, + "call_evm" + ).unwrap_some(error="gateway not found") + + sp.transfer( + sp.record(destination=erc20, + method_signature="transfer(address,uint256)", + abi_params=abi_parms, + callback=None), + sp.mutez(0), + gateway + ) + +@sp.add_test() +def test(): + sc = sp.test_scenario("Erc20Caller", main) + c = main.Erc20Caller() + sc += c + # Transfer 1 WXTZ (1e18 = 0xDE0B6B3A7640000) to a placeholder address. + # recipient: 20 bytes + recipient = sp.bytes("0x1234567890123456789012345678901234567890") + # amount: 1e18 as 32-byte big-endian + amount = sp.bytes( + "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + ) + c.transfer_erc20( + erc20="0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb", # WXTZ Mainnet + recipient=recipient, + amount=amount + ) +``` + +## ERC-20 wrapper + +For calling ERC-20 `transfer` and `approve` specifically, a convenience contract is available that handles ABI encoding automatically: + +``` +KT18oDJJKXMKhfE1bSuAPGp92pYcwVKvCChb +``` + +This contract provides `transfer` and `approve` entrypoints with a Michelson-friendly interface, so you do not need to construct `abi_params` manually. ## `staticcall_evm` @@ -57,20 +162,14 @@ IF_NONE { … } (* use the returned bytes *) ``` -## ERC-20 wrapper - -For calling ERC-20 token contracts specifically, a convenience contract is available at: - -``` -KT18oDJJKXMKhfE1bSuAPGp92pYcwVKvCChb -``` +## Return value -This contract provides `approve` and `transfer` methods with a Michelson-friendly interface. +For `%call_evm`, return values from the EVM callee are delivered to a **callback contract** supplied in the `callback` field. The callback receives the raw ABI-encoded return bytes. Pass `None` when no return value is needed. -## Return value +For `staticcall_evm`, the return value is delivered synchronously as `option bytes` via the Michelson `VIEW` instruction — no callback is needed. -Return values from cross-interface calls are encoded in the callee's native format: +## Failure behavior -The EVM return value (ABI-encoded bytes) is passed back to a **callback contract** specified via the `with_result` entrypoint of the gateway contract. The callback contract must implement handling for the raw bytes. +When a Michelson contract calls `%call_evm` and the EVM callee fails (revert, out of gas, or any other halt), Michelson semantics apply: **the entire operation group reverts**, including any state changes that preceded the call. There is no way to catch the failure and continue within the same operation. -On-chain libraries for encoding and decoding across runtimes are provided to simplify this. +For gas conversion between the two interfaces and the precise accounting rules, see [Resources management](/overview/resources.md#michelson-interface-calling-the-evm-interface). From 339dcbf4c34cc9ef717949ca64890a7d037f3076 Mon Sep 17 00:00:00 2001 From: Nic Volanschi Date: Thu, 13 Aug 2026 10:28:28 +0200 Subject: [PATCH 4/6] remove wrappers while preparing a better documentation --- docs/evm/get-started/network-information.mdx | 5 ----- docs/evm/nac-usage.md | 11 ----------- docs/michelson/nac-usage.md | 10 ---------- 3 files changed, 26 deletions(-) diff --git a/docs/evm/get-started/network-information.mdx b/docs/evm/get-started/network-information.mdx index 6d94dd5b..3b2924a3 100644 --- a/docs/evm/get-started/network-information.mdx +++ b/docs/evm/get-started/network-information.mdx @@ -232,11 +232,6 @@ These precompiled contracts are specific to Etherlink EVM{/* TEVM */}: - - NAC FA1.2 - - - diff --git a/docs/evm/nac-usage.md b/docs/evm/nac-usage.md index 3e8b9be3..71f02124 100644 --- a/docs/evm/nac-usage.md +++ b/docs/evm/nac-usage.md @@ -44,17 +44,6 @@ 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](https://gitlab.com/tezos/tezos/-/tree/master/etherlink/kernel_latest/solidity_examples) of the Etherlink sources. For complex types, off-line tools such as Taquito's `packData` function can compute the encoding. -## FA1.2 wrapper - -For calling FA1.2 token contracts specifically, a convenience precompile is available at: - -``` -0xff00000000000000000000000000000000ffff09 -``` - -This precompile provides `approve` and `transfer` methods that handle Michelson parameter encoding automatically, without requiring manual binary encoding. - - ## Return value In the case of a regular Michelson call (`callMichelson`), there is no return value. diff --git a/docs/michelson/nac-usage.md b/docs/michelson/nac-usage.md index af4c1dee..06e4e1cc 100644 --- a/docs/michelson/nac-usage.md +++ b/docs/michelson/nac-usage.md @@ -114,16 +114,6 @@ def test(): ) ``` -## ERC-20 wrapper - -For calling ERC-20 `transfer` and `approve` specifically, a convenience contract is available that handles ABI encoding automatically: - -``` -KT18oDJJKXMKhfE1bSuAPGp92pYcwVKvCChb -``` - -This contract provides `transfer` and `approve` entrypoints with a Michelson-friendly interface, so you do not need to construct `abi_params` manually. - ## `staticcall_evm` To call a read-only EVM function and receive the result, use the `staticcall_evm` on-chain view: From 490559ac46dad31779e082e49222b59e8ac3453c Mon Sep 17 00:00:00 2001 From: Nic Volanschi Date: Thu, 13 Aug 2026 12:02:46 +0200 Subject: [PATCH 5/6] properly document Michelson->EVM call and view + example --- docs/michelson/nac-usage.md | 111 ++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/docs/michelson/nac-usage.md b/docs/michelson/nac-usage.md index 06e4e1cc..ac2a6015 100644 --- a/docs/michelson/nac-usage.md +++ b/docs/michelson/nac-usage.md @@ -154,12 +154,115 @@ IF_NONE ## Return value -For `%call_evm`, return values from the EVM callee are delivered to a **callback contract** supplied in the `callback` field. The callback receives the raw ABI-encoded return bytes. Pass `None` when no return value is needed. +### `%call_evm` — callback -For `staticcall_evm`, the return value is delivered synchronously as `option bytes` via the Michelson `VIEW` instruction — no callback is needed. +`%call_evm` is a state-mutating call with no direct return. To receive the EVM return value, supply a `contract bytes` handle in the `callback` field. After the EVM call succeeds, the kernel emits a `TRANSFER_TOKENS` internal operation that sends the raw ABI-encoded return bytes to the callback contract at zero mutez. + +The callback contract must expose an entrypoint of type `bytes`. The entrypoint name is encoded in the `contract bytes` handle itself — you choose it when you construct the handle with `sp.contract`: + +```python +callback = sp.some( + sp.contract(sp.bytes, sp.self_address, "receive_result") + .unwrap_some(error="self-entrypoint not found") +) +``` + +If `callback` is `None`, the EVM return value is silently discarded. The call still reverts the whole operation group on EVM failure — passing `None` does not make the call "best-effort". + +#### SmartPy example — call with callback + +```python +import smartpy as sp + +@sp.module +def main(): + GATEWAY = sp.address("KT18oDJJKXMKhfE1bSuAPGp92pYcwVDiqsPw") + + t_call_evm: type = sp.record( + destination = sp.string, + method_signature = sp.string, + abi_params = sp.bytes, + callback = sp.option[sp.contract[sp.bytes]] + ) + + class CallWithResult(sp.Contract): + def __init__(self): + self.data.last_result = sp.bytes("0x") + + @sp.entrypoint + def call_evm_with_result( + self, + destination: sp.string, + method_signature: sp.string, + abi_params: sp.bytes, + ): + gateway = sp.contract( + t_call_evm, + GATEWAY, + "call_evm" + ).unwrap_some(error="gateway not found") + + callback = sp.Some( + sp.contract(sp.bytes, sp.self_address, "receive_result") + .unwrap_some(error="self-entrypoint not found") + ) + + sp.transfer( + sp.record( + destination=destination, + method_signature=method_signature, + abi_params=abi_params, + callback=callback, + ), + sp.mutez(0), + gateway, + ) + + @sp.entrypoint + def receive_result(self, result: sp.bytes): + # `result` is the raw ABI-encoded return value from the EVM call. + # Decode it according to the EVM function's return type. + self.data.last_result = result + +@sp.add_test() +def test(): + sc = sp.test_scenario("CallWithResult", main) + c = main.CallWithResult() + sc += c + + # Test receive_result directly: simulate the kernel delivering a callback. + # The result is a 32-byte ABI-encoded uint256 (value = 42). + encoded_uint256 = sp.bytes( + "0x000000000000000000000000000000000000000000000000000000000000002a" + ) + c.receive_result(encoded_uint256) + sc.verify(c.data.last_result == encoded_uint256) + + # Call call_evm_with_result: reads balanceOf(address) on a token contract. + # abi_params: 12-byte zero-pad + 20-byte address (no selector). + addr_padding = sp.bytes("0x000000000000000000000000") + addr_bytes = sp.bytes("0x1234567890123456789012345678901234567890") + c.call_evm_with_result( + destination="0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb", # WXTZ Mainnet + method_signature="balanceOf(address)", + abi_params=sp.concat([addr_padding, addr_bytes]), + ) +``` + +### `staticcall_evm` — synchronous + +`staticcall_evm` is a read-only view. The return value is delivered synchronously as `option bytes` via the Michelson `VIEW` instruction — no callback contract is needed. ## Failure behavior -When a Michelson contract calls `%call_evm` and the EVM callee fails (revert, out of gas, or any other halt), Michelson semantics apply: **the entire operation group reverts**, including any state changes that preceded the call. There is no way to catch the failure and continue within the same operation. +### `%call_evm` + +If the EVM callee fails for any reason — revert, out of gas, or any other halt — Michelson semantics apply: **the entire operation group reverts**, including all state changes that preceded the call. This holds whether or not a callback was supplied. + +If `callback` is `None` and the EVM call *succeeds*, the return value is silently dropped and execution continues normally. No error is raised. + +For the gas conversion rules and how the forwarded budget is calculated, see [Resources management](/overview/resources.md#michelson-interface-calling-the-evm-interface). + +### `staticcall_evm` -For gas conversion between the two interfaces and the precise accounting rules, see [Resources management](/overview/resources.md#michelson-interface-calling-the-evm-interface). +A view failure (EVM revert, missing view, type mismatch) surfaces as `None` from `VIEW`, which the caller handles with `IF_NONE`. Out-of-gas is the exception: it fails the operation outright rather than returning `None`, so a forwarded-gas exhaustion cannot be silently treated as a missing view. See the outcome table in the [`staticcall_evm`](#staticcall_evm) section above. From 46b7df7e62d4a2956bba0755ff5ab8f33ccf6a1e Mon Sep 17 00:00:00 2001 From: Nic Volanschi Date: Thu, 13 Aug 2026 12:08:00 +0200 Subject: [PATCH 6/6] add ABI encoding and function selector to Glossary --- docs/overview/glossary.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/overview/glossary.md b/docs/overview/glossary.md index 2ffa369a..6a882a6a 100644 --- a/docs/overview/glossary.md +++ b/docs/overview/glossary.md @@ -8,6 +8,8 @@ This glossary defines the most essential terms used in Etherlink. For a wider set of terms used in the Tezos ecosystem, see the [Glossary](https://docs.tezos.com/overview/glossary) in the Tezos documentation. These glossaries are planned to be fused in the near term. +**ABI encoding** — The binary format used to encode function arguments and return values when calling EVM smart contracts. Each argument occupies a 32-byte slot: integers and addresses are left-padded with zeros; dynamic types such as `bytes` use an offset-length-data layout. ABI-encoded data is what `%call_evm` expects in its `abi_params` field and what `staticcall_evm` and `callMichelsonView` return. + **Account** — An entity that can hold state and/or execute operations within one interface. Every account has an address and exists natively in exactly one interface. **Account state** — The data associated with an account (balance, storage, nonce, etc.), maintained by its native interface. @@ -30,6 +32,8 @@ These glossaries are planned to be fused in the near term. **Foreign interface** — From an account's perspective, any interface other than its native interface. +**Function selector** — The first 4 bytes of the Keccak256 hash of an EVM function's canonical signature (e.g. `"transfer(address,uint256)"`). EVM contracts inspect the selector at the start of a call's calldata to dispatch to the right function. The `%call_evm` gateway computes the selector automatically from the `method_signature` string; `staticcall_evm` requires the caller to prepend it manually to `abi_params`. + **Gateway contract** — A special contract in each interface that acts as the single entry point for cross-interface calls. In the EVM interface, the gateway is a precompile; in the Michelson interface, it is an enshrined KT1 contract. **Michelson** is the native smart contract language on Tezos L1.