EIP.tools

EIP.tools

⚠️ DraftStandards Track: Core

EIP-7906: Transaction Assertions via State Diff Opcode

Opcodes that expose a transaction's state diff to on-chain assertions that can reject the transaction's outcome

Authors
Created2025-02-21
Discussion Linkhttps://ethereum-magicians.org/t/eip-restricted-behavior-transaction-type/23130
Requires
Referenced by

Markdown

https://raw.githubusercontent.com/ethereum/EIPs/ma...

EIP-GPT summary

Contents
AbstractMotivationSpecificationConstantsThe ASSERT Frame ModeTransaction Trace OpcodeState Difference SemanticsEvent Data Copy OpcodeStackBehaviorGas CostBlock-Level Access ListReserved InputsUndefined Params and Out-of-Range IndicesResults OrderingRationaleSelection Parameter DesignEnumerationCode and Nonce ChangesGas CostReceipt Representation and Anti-DoSThe ASSERT Frame Mode RequirementPer-contract UsageIndividual Topic AccessEVENTDATACOPY as a Companion OpcodeEvents as a Channel from Earlier FramesGas Pre-Charge ParameterDeterministic EnumerationBackwards CompatibilitySecurity ConsiderationsInsufficiently Restrictive AssertionsEnforcing ASSERT Frame InclusionAssertion Inputs Controlled by the TransactionChanges outside of Transaction Assertion's controlAssertion Gas ExhaustionThe ASSERT Frame Mode Not Reverting Validation PrefixCopyright

Abstract

This proposal introduces two opcodes, TXTRACE and EVENTDATACOPY, that let a contract inspect the outcome of the transaction executing it: the balance, storage, code, and nonce changes it produced, and the events it emitted. TXTRACE enumerates the full transaction state diff, and EVENTDATACOPY copies event data into memory.

These opcodes are valid only inside an ASSERT frame, a new EIP-8141 frame mode also defined here. ASSERT frames execute read-only as a trailing suffix of the transaction's frames, so they observe the transaction's final outcome, and their revert rolls back the execution body while leaving the transaction valid and the gas paid.

Together these allow a wallet or dApp to attach an on-chain assertion to a transaction and have the transaction's effects discarded if the assertion fails, giving users a way to bound what a transaction can do to them and reducing the risk of blind signing.

Motivation

The total value of crypto assets that have been stolen to date exceeds the yearly GDP of a medium-sized nation. This level of loss and waste is indefensible and has a long list of negative consequences for everyone around the world.

The ability of an average user or a Wallet application to find, collect, review, and analyze the EVM code the transaction will execute is very limited.

This leaves the users with no mechanism to enforce any restrictions on what the transaction actually does once it is signed. This leads users to perform de-facto blind signing every time they interact with Ethereum, exposing themselves to significant risks.

By providing the Wallets and dApps with the ability to observe and restrict the possible outcomes of a transaction, we create a tool that users can apply to reduce their risk levels.

Specification

This EIP defines the following instructions:

InstructionOpcode
TXTRACE0xb6
EVENTDATACOPY0xb7

Constants

NameValue
ASSERT3

The ASSERT Frame Mode

This EIP requires EIP-8141 and amends its frame transaction specification by adding a new frame mode value, ASSERT, alongside the DEFAULT, VERIFY, and SENDER modes already defined there.

The following rules apply to EIP-8141 frame transactions wherever this EIP is active:

  • ASSERT is added to the set of mode values accepted by EIP-8141's static constraint on frame.mode.
  • ASSERT frames must form a contiguous trailing suffix of tx.frames: once any frame has mode ASSERT, every subsequent frame in the transaction must also have mode ASSERT. A frame transaction violating this is invalid.
  • As with DEFAULT and VERIFY frames, the caller of an ASSERT frame is ENTRY_POINT.
  • An ASSERT frame is executed as a STATICCALL, disallowing all state manipulation. Unlike VERIFY frames, ASSERT frames are not granted the APPROVE exception defined in EIP-8141: calling APPROVE anywhere in an ASSERT frame's call subtree is an ordinary STATICCALL violation and causes an exceptional halt of that call frame, handled like any other ASSERT failure per the rule below.
  • Approval-scope flags are valid on an ASSERT frame but have no effect, since APPROVE cannot succeed in it. The atomic batch flag is not valid on an ASSERT frame, as the EIP-8141 flag table allows it only with DEFAULT and SENDER, nor on a frame directly before an ASSERT frame. No atomic batch contains an ASSERT frame, so unrolling a failed batch never skips an assertion.
  • If an ASSERT frame reverts or halts exceptionally, the entire execution body of the transaction is reverted unconditionally. The state and the approval context are restored to their condition immediately after the frame whose execution set payer, the last frame of the validation prefix. This overrides the atomic-batch unrolling behavior that would otherwise apply: an ASSERT failure always validates or reverts the whole execution body rather than merely unwinding an atomic batch. If no frame has set payer when an ASSERT frame fails, there is nothing to restore and the transaction is invalid under EIP-8141.
  • An ASSERT failure ends frame execution. The remaining ASSERT frames are not executed: their receipts have status 2 and no gas used, as for the skipped frames of a failed EIP-8141 atomic batch, and their gas is refunded at the end of the transaction.
  • Neither an ASSERT revert nor an exceptional halt invalidates the transaction (unlike a VERIFY frame revert). The transaction remains valid and is included in the block. The failing ASSERT frame's receipt has status 0. The receipts of the reverted execution-body frames keep their status and execution gas, but their logs are discarded and the state gas recorded in them is removed, as for an unrolled EIP-8141 atomic batch.
  • All state changes made within the validation prefix (such as gas payment via APPROVE and account creation in a deploy frame) are permanently committed to the state, and the payer is fully charged for the gas consumed up to the point of the failure.
  • Under EIP-8141's default code, an ASSERT frame is handled the same way as SENDER or DEFAULT.

Transaction Trace Opcode

We introduce a new TXTRACE opcode (0xb6).

TXTRACE reads the state diff of the current transaction: the net difference between the transaction prestate and the state as of this call, spanning balance changes, storage changes, code changes, nonce changes, and emitted events. Since ASSERT frames cannot write state and form a trailing suffix of the transaction, that diff is the transaction's final outcome. The exact semantics are given under State Difference Semantics.

A single call returns one 256-bit value. Two stack inputs select it: param chooses which piece of the diff to read, and in2 is an index into the table that param selects, or 0 for the params that take no index. An assertion reads the diff by calling TXTRACE repeatedly: a count param first (0x00–0x03 or 0x11), then one call per field of each entry. This is the same selector-and-index idea as the FRAMEPARAM opcode from EIP-8141, but the operand order differs: TXTRACE takes param from the top of the stack, while FRAMEPARAM takes its index from the top.

StackValue
top - 0param
top - 1in2

The selected value is pushed onto the stack.

Each TXTRACE call costs the active fork's WARM_STORAGE_READ_COST, as defined in EIP-2929 and subject to the inherited gas schedule described under Gas Cost, for every param.

The available parameters are listed in the table below.

paramNamein2Return value
0x00TXTRACE_BALANCES_CHANGEDmust be 0balances_changed - the number of addresses with a net balance change
0x01TXTRACE_SLOTS_CHANGEDmust be 0slots_changed - the number of (address, slot) pairs with a net storage change
0x02TXTRACE_CODES_CHANGEDmust be 0codes_changed - the number of addresses with a net code hash change
0x03TXTRACE_NONCES_CHANGEDmust be 0nonces_changed - the number of addresses with a net nonce change
0x04TXTRACE_BALANCE_ADDRESSindex in balances_changedchange_address - the address of the account with balance change
0x05TXTRACE_BALANCE_BEFOREindex in balances_changedbalance_before - the balance of the address at the start of the transaction
0x06TXTRACE_BALANCE_AFTERindex in balances_changedbalance_after - the balance of the address as of this TXTRACE call
0x07TXTRACE_SLOT_ADDRESSindex in slots_changedchange_address - the address of the account with storage change
0x08TXTRACE_SLOT_BEFOREindex in slots_changedslot_value_before - the value of the slot at the start of the transaction
0x09TXTRACE_SLOT_AFTERindex in slots_changedslot_value_after - the value of the slot as of this TXTRACE call
0x0ATXTRACE_SLOT_KEYindex in slots_changedslot_key - the storage slot key that was changed
0x0BTXTRACE_CODE_ADDRESSindex in codes_changedchange_address - the address of the account with code change
0x0CTXTRACE_CODE_BEFOREindex in codes_changedcodehash_before - the code hash of the address at the start of the transaction
0x0DTXTRACE_CODE_AFTERindex in codes_changedcodehash_after - the code hash of the address as of this TXTRACE call
0x0ETXTRACE_NONCE_ADDRESSindex in nonces_changedchange_address - the address of the account with nonce change
0x0FTXTRACE_NONCE_BEFOREindex in nonces_changednonce_before - the nonce of the address at the start of the transaction
0x10TXTRACE_NONCE_AFTERindex in nonces_changednonce_after - the nonce of the address as of this TXTRACE call
0x11TXTRACE_EVENTS_COUNTmust be 0events_count - the total number of emitted events
0x12TXTRACE_EVENT_ADDRESSindex in events_countevents_address - the address of the contract that emitted the event
0x13TXTRACE_EVENT_TOPIC_COUNTindex in events_countevent_topic_count - the number of topics of the event (0–4)
0x14TXTRACE_EVENT_TOPIC0index in events_countevent_topic0 - the first topic of the event; exceptional halt if no topic
0x15TXTRACE_EVENT_TOPIC1index in events_countevent_topic1 - the second topic of the event; exceptional halt if no such topic
0x16TXTRACE_EVENT_TOPIC2index in events_countevent_topic2 - the third topic of the event; exceptional halt if no such topic
0x17TXTRACE_EVENT_TOPIC3index in events_countevent_topic3 - the fourth topic of the event; exceptional halt if no such topic
0x18TXTRACE_EVENT_DATA_LENindex in events_countevent_data_len - the byte length of the event's non-indexed data
0x19TXTRACE_GAS_PRE_CHARGEmust be 0gas_pre_charge - the total amount deducted from the gas payer
0x1ATXTRACE_GAS_PAYERmust be 0gas_payer_address - the address charged the gas pre-charge

Each state-change table lists its fields in the same order: change_address, the before value, and the after value, followed by any additional key. A table's before and after params are therefore always one and two above its address param.

TXTRACE and EVENTDATACOPY are valid only for execution inside an ASSERT mode frame, as defined above. Executing either of these opcodes in any other context — including legacy transactions, EIP-1559 transactions, or any other EIP-8141 frame mode — results in an exceptional halt.

The gas_pre_charge parameter is the transaction's max_cost as defined in EIP-8141: the amount collected from the payer when payment is approved. It depends on the including block's base fees and can be less than the transaction's max_fee. For transactions with blobs attached, it includes the blob fees, priced at the blob base fee.

The gas_payer_address is the target of whichever frame called APPROVE(APPROVE_PAYMENT) or APPROVE(APPROVE_EXECUTION_AND_PAYMENT), i.e. the EIP-8141 payer, which may or may not be the transaction sender.

State Difference Semantics

The before values reflect the transaction prestate values recorded before the start of entire transaction's execution, before any state writes made in relation to this transaction. The after values reflect the current state as of the TXTRACE opcode call. Intermediary writes between transaction start and the TXTRACE call are not observable separately.

An address will appear in balances_changed when its balance at the time of the TXTRACE call differs from its balance at transaction start. This includes the gas fee pre-charge applied to the gas payer address. Callers computing the net ETH transferred to or from an address can look up the gas payer via gas_payer_address (param 0x1A) and subtract gas_pre_charge (param 0x19) from that address's balance delta.

A (address, slot) pair will appear in slots_changed when its value at the time of the TXTRACE call differs from its value at transaction start. Multiple writes to the same slot during the transaction collapse into this single entry.

A storage wipe can clear slots the transaction never wrote, for example a CREATE onto an address that holds only storage if the active fork's creation rules permit it, or the removal of an empty account that holds storage. Those slots are not reported: they are absent from slots_changed. Reporting them would require iterating the account's prestate storage, which is unbounded.

An address appears in codes_changed when its code hash at the time of the TXTRACE call differs from its code hash at transaction start. As with EXTCODEHASH, the code hash is computed over the code as stored: an EIP-7702 delegation indicator is hashed itself and is not followed to the code it delegates to. An account that does not exist counts as having the empty-code hash, where EXTCODEHASH would return 0. The table therefore includes contracts created by the transaction, whose codehash_before is the empty-code hash, and delegation indicators the transaction installed, changed, or cleared. An account that a CREATE/CREATE2 leaves with empty code is not enumerated, as it has no code change.

An address appears in nonces_changed when its nonce at the time of the TXTRACE call differs from its nonce at transaction start. An account that does not exist counts as having nonce 0. The table includes tx.sender whenever APPROVE increments its account nonce in the validation prefix, and also includes any account that executed CREATE or CREATE2 and any contract the transaction created.

An account that EIP-6780 deletes at the end of the transaction appears as the execution body left it, since the deletion happens after the last frame. A contract created and destroyed in the same transaction therefore still appears in codes_changed and nonces_changed.

Logs that the protocol emits on the transaction's behalf, such as EIP-7708 transfer logs, count as events like any other log, so the event tables also expose ETH transfers.

Event Data Copy Opcode

TXTRACE reports an event's topics and the byte length of its non-indexed data, but not the data itself, which is variable-length and so cannot be returned on the stack. We introduce an EVENTDATACOPY opcode (0xb7) to copy that data into memory.

The gas cost matches CALLDATACOPY, i.e. the operation has a fixed cost of 3 and a variable cost that accounts for the memory expansion and copying.

Stack

StackValue
top - 0event_index
top - 1memOffset
top - 2dataOffset
top - 3length

No stack output value is produced.

Behavior

The operation copies length bytes from the event's non-indexed data, starting at the given byte dataOffset, into a memory region starting at memOffset. Copying and memory expansion follow CALLDATACOPY, with the following source-range checks:

  • If event_index >= events_count, an exceptional halt occurs.
  • If dataOffset + length exceeds the event's data length, an exceptional halt occurs. This applies even when length is 0, unlike CALLDATACOPY, which never halts on its source range.

The addition dataOffset + length is evaluated without 256-bit wraparound.

Gas Cost

All access costs in this EIP use the active fork's gas schedule, including any repricing of the referenced parameters. Where EIP-8038 is active, WARM_STORAGE_READ_COST is named WARM_ACCESS and has value 100.

TXTRACE and EVENTDATACOPY read only already-recorded diff and log data. They do not read live state, do not interact with the EIP-2929 access lists, and add no accesses to the block-level access list.

Block-Level Access List

Where EIP-7928 is active and an ASSERT failure reverts the execution body, the block-level access list treats the reverted frames as reverted calls: their state changes are discarded, but every address and storage slot they accessed remains in the list. A slot the body wrote and that the rollback restored has no net change, so it is recorded in storage_reads unless the validation prefix or another transaction in the block changed it. Accesses made by the ASSERT frames themselves also remain. State changes from the validation prefix are recorded at the transaction's block_access_index.

Reserved Inputs

Any in2 operand marked must be 0 in the parameter table above MUST be zero; supplying a non-zero value causes an exceptional halt.

Undefined Params and Out-of-Range Indices

A param value not listed in the parameter table above, that is any value above 0x1A for TXTRACE, causes an exceptional halt. Selectors, indices, and reserved-zero inputs are checked as full 256-bit values, without truncation.

For every TXTRACE param whose in2 is an index into a table (0x04–0x10 and 0x12–0x18), an in2 greater than or equal to that table's count (balances_changed, slots_changed, codes_changed, nonces_changed, or events_count) causes an exceptional halt.

Results Ordering

Balance, storage slot, code, and nonce changes returned by the TXTRACE opcode are enumerated in ascending order sorted by the affected address as a numerical uint160 value.

Storage changes within a single address are sorted by the storage slot key as a numerical uint256 value.

Events are enumerated in the order they were emitted during transaction execution, matching their global log index within the transaction.

Rationale

Selection Parameter Design

The TXTRACE opcode follows the same selector-and-index idea used by FRAMEPARAM in EIP-8141, a param selecting the field and a second operand selecting the entry. This avoids introducing a separate opcode for every piece of trace information. Unlike FRAMEPARAM, TXTRACE takes param from the top of the stack.

The count params of all state-change tables come first, and every state-change table uses the same field order. An assertion can therefore read any of these tables with the same code, given only the table's count and address params.

Enumeration

The TXTRACE opcode exposes transaction outcomes through index-based access over the full set of observable state changes.

Typical transaction assertion costs are negligible compared to the gas cost of the storage modification itself.

Events are in emission order and require a linear scan.

Most assertion scripts are expected to enumerate the full set of allowed state changes and will not require a binary search.

Code and Nonce Changes

codes_changed reports every code hash change rather than only new deployments, because an account's code can change without a deployment: EIP-7702 delegation indicators can be installed, changed, or cleared during execution by instructions such as EIP-7819's SETDELEGATE, which EIP-8141 already accounts for in its deploy frame. An assertion that protects an account has to be able to see that the account's delegation changed. A new deployment remains identifiable by a codehash_before equal to the empty-code hash.

nonces_changed completes the account fields: together with balances_changed, slots_changed, and codes_changed, it lets an assertion compare every field of an account against the transaction prestate, apart from the storage wipes described in State Difference Semantics. Clients already track per-transaction code and nonce changes for the EIP-7928 block-level access list.

Gas Cost

This EIP adds no gas constants. TXTRACE reads data that the client already holds for the current transaction: its state diff, its logs, and its gas payment. Every slot or account in the diff was written by the transaction, so it is already warm, and reading its current value with SLOAD or BALANCE costs WARM_STORAGE_READ_COST. TXTRACE charges the same for its before and after values. The other TXTRACE params, such as counts, addresses, and event fields, could cost less, as FRAMEPARAM does in EIP-8141. They use the same price so that TXTRACE has one cost for every param.

A client can build an index over the diff and the logs when an ASSERT frame first reads them, and reuse it while the diff does not change. ASSERT frames cannot write state or emit logs, so the diff changes only if one of them fails and the execution body is reverted. The work to build the index grows with the accounts, storage slots, and logs that the transaction touched, and the transaction has already paid for each of them: at least a cold access for a slot or for an account that did not start warm, and the LOG cost or the value transfer cost for an event. A transaction with no ASSERT frame never needs the index.

Once the index is built, each TXTRACE call does a constant amount of work, so the gas an assertion needs grows linearly with the number of entries it reads.

Receipt Representation and Anti-DoS

If an ASSERT revert were to completely exclude the transaction from the block and roll back the gas payment, it would introduce a severe Denial-of-Service vector. Attackers could consume up to the block gas limit and then revert in the ASSERT frame for free.

Keeping the transaction valid and committing the validation prefix is strictly required to ensure block builders are compensated for the execution work performed. An ASSERT revert operates as an application-level execution revert, not a protocol-level invalidation, and thus gives the failing frame a status = 0 receipt while keeping the gas payment intact.

The ASSERT Frame Mode Requirement

The TXTRACE and EVENTDATACOPY opcodes provide significant introspection capabilities that may break code encapsulation.

By only allowing their execution inside an ASSERT frame we ensure this capability may only be used to determine the outcome validity and decide whether to revert the execution body.

Because ASSERT frames are required to be a trailing suffix of tx.frames, the diff TXTRACE observes is always the final outcome of the transaction, and because an ASSERT failure unconditionally reverts the whole execution body, a failed assertion can never be partially bypassed by atomic-batch semantics or by frames that ran before it.

Allowing multiple ASSERT frames allows independent assertion providers to compose without a need for an active collaboration.

Each transaction assertion module can run its own assertion logic in its own frame and independently revert the execution body if its check fails.

Per-contract Usage

Individual contracts can use the TXTRACE opcode to inspect the state changes made internally, using a pattern similar to "reentrancy guard" modifier for their external functions. This applies to any contract called from within an ASSERT frame's call subtree.

Individual Topic Access

EVM events carry 0–4 topics, each a 32-byte word. Topic 0 is conventionally the event signature hash; topics 1–3 carry indexed parameters. Assertion contracts that verify which specific token was transferred, which address was approved, or which identifier was involved need to inspect these indexed values directly.

Accessing a topic slot at or beyond event_topic_count causes an exceptional halt, consistent with out-of-bounds behavior for all other indexed params.

EVENTDATACOPY as a Companion Opcode

Event non-indexed data is variable-length and cannot be returned as a single 32-byte stack word. A memory-copy opcode with the same semantics as CALLDATACOPY is the idiomatic EVM approach for variable-length data access.

Events as a Channel from Earlier Frames

While store and balance changes represent the actual source of truth for closed-ended assertions, events are the most accessible way to express an assertion restricting certain standardized actions. Their signatures and indexed topics are parts of the public standard and external interface from day one — see ERC-20, ERC-721, ERC-1155 and others. Events are routinely decoded by blockchain tooling like wallets, explorers, and indexers. An event-based assertion needs only the public signature: "this transaction emits no Transfer(address,address,uint256) with my account as from" - to achieve a useful safety check.

Additionally, EIP-8141 discards transient storage between frames and gives a later frame no access to an earlier frame's return data, so an ASSERT frame cannot receive a value measured before the transaction body through the frame mechanism. Events provide a channel to deliver pre-state snapshots to the assertions frame without writing anything to the persistent state.

Gas Pre-Charge Parameter

The gas pre-charge (EIP-8141's max_cost) is collected from the payer when payment is approved and appears in the gas payer's balance_after, making it hard to isolate actual ETH transfers. The pre-charge is also provisional: a refund for unused gas is issued after execution, so the bundled figure is not the final cost.

Exposing gas_pre_charge directly lets callers subtract it with a single opcode call. It covers all gas-related deductions including the blob fee for EIP-4844 transactions, so the same subtraction isolates pure ETH transfers. gas_payer_address completes the picture: the EIP-8141 gas payer may be a separate paymaster rather than the sender, and no existing opcode exposes that address. Together the two parameters let assertion contracts identify the right balances_changed entry and apply the subtraction correctly.

Deterministic Enumeration

State changes use address-sorted order because the state diff model collapses all intermediate writes into a single entry per (address, slot). Sequence of execution does not define a deterministic order for the collapsed state diff, as the same slot may be written multiple times across interleaved reentrant calls, yet produce exactly one entry. Sorting by address and slot key ensures a canonical, deterministic enumeration independent of execution flow.

Events can use emission order because each event is a distinct, non-collapsed entity with a canonical position corresponding to its log index. Assertion contracts that verify cross-contract event sequencing require this ordering.

Backwards Compatibility

TXTRACE and EVENTDATACOPY occupy previously unused opcode slots. No changes are made to existing opcodes, transaction types, or precompiles, so existing contracts and tooling are unaffected.

This proposal has a hard dependency on EIP-8141 (requires: 8141): TXTRACE and EVENTDATACOPY can only execute inside an EIP-8141 ASSERT frame, as defined under The ASSERT Frame Mode. Legacy transactions, EIP-1559 transactions, and any other EIP-8141 frame mode cannot use either of these opcodes.

Security Considerations

Insufficiently Restrictive Assertions

The main risk is a false sense of security: an assertion contract that checks too little may mislead users into believing a transaction is safe when it is not.

Wallets and dApps that build on TXTRACE must ensure their assertion logic covers all relevant state changes for the protected operation. It is critical that the ecosystem treats incomplete assertions as no better than no assertion at all.

The ASSERT frame mode requirement strengthens, but does not replace, this guarantee: it ensures a triggered assertion cleanly and unconditionally reverts the entire execution body, while the transaction stays valid and keeps the gas payment approved in the validation prefix, and that this cannot be partially bypassed via atomic-batch flags or frames ordered after the assertion. It does not, by itself, make any individual assertion more restrictive or correct.

Enforcing ASSERT Frame Inclusion

While ASSERT frames dictate how assertions are executed, there is no explicit protocol-level requirement that a transaction must include an ASSERT frame. Smart accounts aiming to use ASSERT frames must explicitly configure their VERIFY frame's validation logic to strictly require the presence of their specific ASSERT frame in every transaction they approve without any mechanist that could allow circumventing this requirement.

Because a transaction can contain multiple ASSERT frames, and they are guaranteed to form a contiguous suffix at the end of the transaction, the smart account can iterate backwards to locate its required assertion frame.

It is critical that the expected target of all ASSERT frames is an immutable, non-upgradeable contract. If the assertion contract were upgradeable, a malicious transaction could potentially upgrade it during execution to bypass the security checks. Only as long as the VERIFY frame executes before any state changes are made, checking the ASSERT frame target during validation guarantees the transaction will be evaluated against the intended immutable policy.

This can be enforced as follows in Solidity (pseudocode):

function requireAssertFrame() internal view {
    bool found = false;
    for (uint256 i = tx.frames.length; i > 0; i--) {
        Frame memory f = tx.frames[i - 1];
        if (f.mode != 3) {
            break; // ASSERT frames are a contiguous suffix; no more left to check
        }
        if (f.target == this.assertEnforcer) {
            // (Optionally check f.data matches the expected ASSERT method selector)
            found = true;
            break;
        }
    }
    require(found, "Missing required ASSERT frame for this account");
}

Assertion Inputs Controlled by the Transaction

The execution body runs before any ASSERT frame. Any state the body can write is therefore chosen by whoever built the execution frames at the moment the assertion reads it. An assertion that takes its reference values from such state can be steered to accept a harmful outcome, even though it reads the real post-execution state.

This applies to two sources of reference values:

  • Calls from the ASSERT frame's call subtree to other contracts, such as a price oracle, a token's balanceOf, a registry, or a proxy. The execution body can move an oracle price, edit a registry entry, or upgrade a proxy before the assertion runs. A called contract can also detect that its caller is inside an ASSERT frame, for example by executing TXTRACE in a sub-call that halts in any other context, and answer the assertion differently from other callers.
  • after values of accounts and slots that the transaction was able to modify, when they are used as a threshold or a price rather than as the outcome being checked. They report what the execution body left behind and carry no more trust than the body itself.

Assertions should take the reference values they compare the outcome against, such as minimum amounts received, permitted spenders, and prices, from data fixed at signing time, for example the ASSERT frame's data committed by the canonical signature hash, or from before values, which the transaction cannot modify. before values reflect the transaction prestate, so earlier transactions in the same block can still move them; a price read from the prestate keeps the usual oracle manipulation risk.

Changes outside of Transaction Assertion's control

An assertion constrains one transaction on one chain: the transaction that carries it. The following limits hold for any assertion, however complete.

  • Earlier authority: an existing allowance, operator approval, or signed off-chain permit stays usable in the spender's own transactions, which do not carry this assertion.
  • Later effects: a queued withdrawal or a bridge deposit completes in another transaction or on another chain, where this assertion does not run.
  • Time before inclusion: the prestate is the state at inclusion, not the state the signer saw. An EIP-8141 expiry verifier frame limits that time, not the change.
  • Revert target state is not the prestate: the validation prefix of a transaction modifies the chain state as well as the execution body. Assertions revert the execution body. Changes that take place in the validation prefix are observed but cannot be undone.
  • Events: a contract's event shows only that its code emitted it. A contract can change state without an event, or emit an event without the change. Storage and balance changes remain the source of truth.

Assertion Gas Exhaustion

Assertion contracts that enumerate TXTRACE results may run out of gas. As stated previously, a transaction can produce up to ~42,600 events in a transaction in the current Ethereum configuration. Asserting over them will require a significant amount of gas in the worst-case.

Assertion contracts should defend against assertion gas related issues by reading the total entry counts and ensuring these are below a safe limit. The framework layer calling the assertion must forward a gas stipend proportional to the entry counts it expects to process.

The execution body controls part of what the tables contain: a malicious dApp can pad the transaction with cheap logs, inflating the cost of an assertion until it exceeds its stipend. The balance, storage, code, and nonce tables are sorted, so an assertion can locate the entries of one address or slot by binary search in a logarithmic number of calls. Events are in emission order, so an assertion over events must scan every event, including padding.

An assertion that runs out of gas before completing its enumeration loop has not verified the full outcome. Any framework built on TXTRACE must ensure that assertion OOG is treated as an explicit assertion revert.

The ASSERT Frame Mode Not Reverting Validation Prefix

It is crucial that the transaction relying on an ASSERT frame to ensure the outcome validity does not contain untrusted execution in its validation prefix.

As it is not feasible to revert the entire transaction including the validation prefix without un-paying the block builder (which introduces a massive DoS vector), the validation prefix is NOT reverted by reverting ASSERT frames.

In a correctly constructed EIP-8141 transaction, the wallet software constructs the validation prefix, while untrusted dApp actions are placed strictly in the execution phase (SENDER frames). This is inherently safe as far as the wallet's own validation-prefix logic — including any deploy frame — is itself trustworthy. A SENDER frame placed before the frame that approves payment is part of that validation prefix, so a failed assertion does not revert it. Actions an assertion protects belong after the payment approval.

Therefore, committing the validation prefix on an ASSERT revert is not a security flaw for the user (provided they use standard wallet software), but it is a strict necessity for protecting the network from DoS attacks.

The deploy frame's restriction to installing tx.sender's code is a mempool-propagation rule (Structural Rules), not a protocol invariant — nothing prevents it from producing other side effects, which TXTRACE will surface but which an ASSERT frame cannot undo. A wallet must never use the deploy frame for anything beyond deploying tx.sender; they cannot rely on an ASSERT frame to catch or reverse it.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth