Execution Behavior
This page describes how a frame transaction is processed from validation through execution to finalization.
Stateful Validation
Before any frames execute:
assert tx.nonce == state[tx.sender].nonceThe nonce check happens against the current state, just like legacy transactions. However, the nonce is not incremented here — that happens inside APPROVE.
The Frame Execution Loop
After validation, the execution engine initializes two transaction-scoped variables:
payer_approved = false
sender_approved = falseThen it iterates through each frame in order:
For each frame:
Determine the call target
- If
targetis null, usetx.sender
- If
Set the caller based on mode
DEFAULTorVERIFY: caller isENTRY_POINT(0xaa)SENDER: caller istx.sender— butsender_approvedmust already betrue, otherwise the entire transaction is invalid
Handle codeless accounts
- If
frame.targethas no code, execute the default code logic
- If
Execute the frame
VERIFYmode: executed as aSTATICCALL(no state changes)DEFAULT/SENDERmode: regular call execution- The
ORIGINopcode returns the frame'scallerthroughout all call depths
Handle the result
- If the frame reverts, its state changes are discarded and execution proceeds to the next frame
- If the frame is
VERIFYmode and did not successfully callAPPROVE, the entire transaction is invalid
After all frames:
- Verify that
payer_approved == true. If not, the entire transaction is invalid. - Refund unused gas to the gas payer.
Transaction-Scoped State
Approval Flags
The sender_approved and payer_approved flags are transaction-scoped and follow a strict one-way progression:
false → true (via APPROVE)Once set, they cannot be unset or re-approved. The ordering constraint is enforced: sender must approve before payer can approve.
Cross-Frame Interactions
Two important cross-frame behaviors:
Warm/cold state journal is shared — If frame 0 touches a storage slot (making it "warm"), frame 1 sees it as warm. This affects gas costs via EIP-2929 rules.
Transient storage is discarded between frames —
TSTORE/TLOAD(EIP-1153) state does not persist across frame boundaries. Each frame starts with a fresh transient storage context.
ORIGIN Behavior
For frame transactions, the ORIGIN opcode returns the frame's caller — not the traditional tx.origin:
- In
DEFAULTandVERIFYframes:ORIGINreturnsENTRY_POINT(0xaa) - In
SENDERframes:ORIGINreturnstx.sender
This is consistent with the precedent set by EIP-7702 which already modified ORIGIN semantics. Contracts that rely on ORIGIN == CALLER for security checks (a long-discouraged pattern) may behave differently.
Validity Summary
A frame transaction is invalid (rejected entirely, as if it never existed) if any of the following are true:
| Condition | When checked |
|---|---|
| Static constraints fail (nonce size, frame count, etc.) | Pre-execution |
tx.nonce != state[tx.sender].nonce | Pre-execution |
A SENDER frame executes while sender_approved == false | During execution |
A VERIFY frame completes without calling APPROVE | After frame execution |
payer_approved == false after all frames | Post-execution |