Caveats
Calling through cross-chain proxies
-
Opcodes that differ on cross-chain proxies:
BALANCE,EXTCODESIZE,EXTCODECOPY, andEXTCODEHASHinspect the local proxy account, not the remote contract.DELEGATECALLexecutes the proxy bytecode in the caller's context; it does not execute remote contract code locally or preserve the normal manager/proxy authorization path.- Block-state opcodes (
blocknumber,blockhash,blockgaslimit,chainid,coinbase, …) reflect the chain the call is executing on, not the source chain — values will differ when the same logical action is observed on L1 vs L2.
-
Proxy self-calls: A proxy calling itself with
staticCheck()runs its internal detector instead of making a cross-chain call. -
Missing entries and destination reverts: A top-level or static lookup miss reverts at the proxy, as does a matched reverting destination; revert data alone is not a reliable way to distinguish them. A nested mutable lookup miss instead records
CALL_NOT_FOUNDand returns success with empty data. Its rejection depends on the marker surviving until hash validation; see the reverted-probe edge case below. -
Proxy ETH recovery is best-effort: ETH sent to a proxy address before deployment is swept to the recovery address in the constructor, once, ignoring failure, so a recovery address that always rejects ETH cannot block proxy creation. The sweep gets at most 100k gas, so a recipient that burns everything it receives only adds that much to the deployment cost. If the sweep fails the ETH stays in the proxy for good.
Delivery and execution semantics
L1 ordering of deferred entries is defined in the core protocol's Settlement Model.
-
Gas-budget checks are estimates: Both L1 and L2 check
_hasEnoughCallGasbefore dispatching calls with non-zerogas. A detected shortage stops the local call array and foldsCALL_INSUFFICIENT_GASinto the rolling hash; valid proofs must exclude that marker, so hash validation rejects executions that retain it. An ordinary enclosing revert can erase the marker. Static-entry execution instead reverts withInsufficientCallGas. The estimate excludes account-creation costs, return-data processing, and the rest of the entry, so it is not an unconditional gas-delivery or completion guarantee. Agasvalue of0skips the check and forwards available gas subject to EVM forwarding limits. Posters must still fund the whole batch. -
Posting does not guarantee delivery of every entry: Consumption scans forward and permanently skips non-matching entries. During the meta hook, all rollups share one cursor: a third-party operation calling
executeL2Txs(G)can consume a later G entry and skip pending entries for H, causing H's calls to fail withExecutionNotFound. If the hook catches those failures and returns successfully, the skipped immediate entries are discarded; if it reverts, the whole post rolls back. Executed entries still require valid starting roots and execution checks. Only leading immediate L2Tx entries run automatically; later ones require an explicitexecuteL2Txscall. -
Cross-rollup calls inside the meta hook only work between rollups verified in the same batch: a rollup verified by an earlier batch this block is unreachable from the hook (
ExecutionNotFound); interact with it afterpostAndVerifyBatchreturns, or verify both rollups together. -
Composer-controlled delivery: The composer can withhold a batch or, when it controls the posting contract, leave optional entries unexecuted. Including execution settings in the proof would not force it to do that work, though it could stop another submitter from changing those settings. Entries that execute must still pass the existing checks. Applications that require particular entries to run must enforce that requirement themselves.
-
Mutable retries after a revert are limited on both L1 and L2: If a call reverts, or succeeds and an enclosing application call later reverts, the table cursor and relevant hash/root updates roll back. Changing local state and repeating the same call can therefore select the old row again. Mutable lookups do not try another matching row after a hash mismatch, so a valid retry can fail. Candidate retries are not implemented; experiments are planned, and whether to implement them remains undecided. See call-pattern limits.
-
A nested retry may return an old saved answer: After an enclosing revert, a successful nested call can reuse a row whose callbacks read the new state but whose saved answer belongs to the old state. The successful nested closing hash does not include that answer, so the outer hash may still match if the wrong answer affects only state not reflected in the hashed calls/results. This is a source-level concern awaiting a dedicated reproduction and production-proof validation; retry support must not be assumed. See the example and validation checklist.
-
L2 table refresh responsibilities: A reverted entry restores the cursor, so retrying the same call hash in the same loaded table selects the same first matching row. A later row becomes reachable after a committed cursor advance or table replacement. The node must refresh tables when state or other dependencies invalidate their cached results. A transaction boundary alone does not require reloading: reuse is allowed while results and lookup contexts remain valid, subject to the manager's same-block gate.
-
Static candidates must be distinguishable: L1/L2 static lookups select the first candidate whose callback-result hash matches, supporting local writes without changing the cursor, root pins or rolling hash. Identical full execution inputs must produce identical outcomes. Ambiguity arises when a differing dependency, such as gas, is not distinguished by the lookup; builders must reject or restructure conflicting candidates. Retrying callbacks costs gas, and gas alone is not a reliable state identifier.
-
Gas-dependent results can be indistinguishable (accepted support limit): A view such as
return gasleft() < 400_000 ? 1 : 2can require different answers for identical calldata. L1 outgoing identity always uses zero gas; L2 outgoing identity does so whenUSE_GAS_LEFT == false. If the reads also share the lookup context and callback-result hash (including zero for two empty callback arrays), both candidates validate and the first wins. Such traces are outside the supported protocol behavior: builders/provers must reject or restructure them. L2 observed-gas keying can distinguish some cases, but does not guarantee unique identities or exact gas-equivalent replay. Candidate retries also spend gas even when outputs are gas-insensitive. See the gas-sensitive and retry-budget tests. -
Immediate skips are conditional: only nonempty caught failures emit
L2TxSkipped. Empty data aborts withImmediateL2TxOutOfGaswithout proving OOG. All-failed immediate runs and any later outer revert discard earlier logs.
Deployment and trust assumptions
-
Accepted proofs replace queues before deferred root checks: Every accepted batch replaces the participating rollups' execution/static queues and updates
lastVerifiedBlock. An old proof that still verifies can therefore replace useful queued work or activate the same-blocksetRootlock even if its deferred entries cannot execute against the current roots. Root advancement prevents stale state transitions; it does not prevent these posting-side effects. Queue replacement is a liveness policy, and rollup-defined verification context can enforce freshness where needed. -
Proof domains are a deployment responsibility: Each rollup is verified on its designated L1. Public inputs do not explicitly bind
block.chainid, the registry address, or a protocol version; independent deployments must use distinct verification domains. Reusing identical rollup/proof configurations across them is outside the intended model. -
L2 funding is pooled across loads: table replacement does not erase unused ETH. Partial consumption, reverts and residual inventory require an explicit node/circuit accounting policy; no per-load equality is checked. See L2 funding and inventory.
Edge cases
-
Reverts also undo missing-call markers: An ordinary enclosing revert rolls back the transient rolling hash, including
CALL_NOT_FOUND. Only the explicitContextResultpath carries that hash across its deliberate revert. A caught validation error is reflected only through the execution outcomes that remain recorded.Applications must not treat empty success from a reverted
try/catchprobe as remote approval. A nested mutable call with no matching entry returns(true, ""). If the probe reverts and reports that result in its revert data, it erases the missing-call marker. The caller can then act on the apparent success, and the final entry check may accept it despite no proven remote approval.For example, a small loan is approved during simulation, but the requested amount is increased before execution. The application probes approval again: no proven entry matches the larger amount, yet the proxy returns success with empty data. The probe then reverts, passing that apparent approval back in its revert data and erasing the missing-call marker. The application catches the revert and may pay the larger amount. This can happen on both L1 and L2. For static calls, a missing entry causes an immediate revert.