<!--
Blockchain Lab reading mirror.
Licence: CC0-1.0. This is the upstream licence, not a Blockchain Lab grant.
Canonical: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7906.md
Repository: https://github.com/ethereum/EIPs
Mirrored: 2026-10-10
The text below is the upstream file. The desk study is a different page and is not covered by this licence.
-->

---
eip: 7906
title: Transaction Assertions via State Diff Opcode
description: Opcodes that expose a transaction's state diff to on-chain assertions that can reject the transaction's outcome
author: Alex Forshtat (@forshtat), Shahaf Nacson (@shahafn), Dror Tirosh (@drortirosh), Yoav Weiss (@yoavw), Fredrik Svantes (@0xfredrik), Daniil Ankushin (@AnkushinDaniil)
discussions-to: https://ethereum-magicians.org/t/eip-restricted-behavior-transaction-type/23130
status: Draft
type: Standards Track
category: Core
created: 2025-02-21
requires: 2929, 8141
---

## Abstract

This proposal introduces three opcodes, `TXTRACE`, `TXDIFF`, and `EVENTDATACOPY`, that let a contract inspect the outcome of the transaction executing it: the balance, storage, and code changes it produced, and the events it emitted. `TXTRACE` enumerates the full transaction state diff, `TXDIFF` reads a single entry by key, and `EVENTDATACOPY` copies event data into memory.

These opcodes are valid only inside a `POST_TX` frame, a new [EIP-8141](./eip-8141.md) frame mode also defined here. `POST_TX` 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:

| Instruction     | Opcode |
|-----------------|--------|
| `TXTRACE`       | `0xb6` |
| `TXDIFF`        | `0xb7` |
| `EVENTDATACOPY` | `0xb8` |

### Constants

| Name                   | Value |
|------------------------|-------|
| `POST_TX`              | `3`   |

### The `POST_TX` Frame Mode

This EIP requires [EIP-8141](./eip-8141.md) and amends its frame transaction specification by adding a new frame `mode` value, `POST_TX`, alongside the `DEFAULT`, `VERIFY`, and `SENDER` modes already defined there.

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

- `POST_TX` is added to the set of mode values accepted by EIP-8141's static constraint on `frame.mode`.
- `POST_TX` frames must form a contiguous trailing suffix of `tx.frames`: once any frame has mode `POST_TX`, every subsequent frame in the transaction must also have mode `POST_TX`. A frame transaction violating this is invalid.
- As with `DEFAULT` and `VERIFY` frames, the `caller` of a `POST_TX` frame is `ENTRY_POINT`.
- A `POST_TX` frame is executed as a `STATICCALL`, disallowing all state manipulation. Unlike `VERIFY` frames, `POST_TX` frames are not granted the `APPROVE` exception defined in [EIP-8141](./eip-8141.md#approve-instruction-0xaa): calling `APPROVE` anywhere in a `POST_TX` frame's call subtree is an ordinary `STATICCALL` violation and causes an exceptional halt of that call frame, handled like any other `POST_TX` failure per the rule below.
- Approval-scope flags are valid on a `POST_TX` frame but have no effect, since `APPROVE` cannot succeed in it. The atomic batch flag is not valid on a `POST_TX` frame, as the [EIP-8141](./eip-8141.md) flag table allows it only with `DEFAULT` and `SENDER`, nor on a frame directly before a `POST_TX` frame. No atomic batch contains a `POST_TX` frame, so unrolling a failed batch never skips an assertion.
- If a `POST_TX` frame reverts or halts exceptionally, the entire execution body of a transaction is reverted unconditionally. This overrides the atomic-batch unrolling behavior that would otherwise apply: a `POST_TX` failure always validates or reverts the whole transaction execution, up to the "validation prefix", rather than merely unwinding an atomic batch.
- A `POST_TX` failure ends frame execution. The remaining `POST_TX` frames are not executed: their receipts have `status` `2` and no gas used, as for the skipped frames of a failed [EIP-8141](./eip-8141.md) atomic batch, and their gas is refunded at the end of the transaction.
- Neither a `POST_TX` 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 `POST_TX` 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](./eip-8141.md) 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](./eip-8141.md#default-code), a `POST_TX` 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, deployed contracts, and emitted events. Since `POST_TX` 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](#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`, `0x01`, `0x02`, or `0x0C`), then one call per field of each entry. This is the same selector-and-index idea as the `FRAMEPARAM` opcode from [EIP-8141](./eip-8141.md), but the operand order differs: `TXTRACE` takes `param` from the top of the stack, while `FRAMEPARAM` takes its index from the top.

| Stack     | Value   |
|-----------|---------|
| `top - 0` | `param` |
| `top - 1` | `in2`   |

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](./eip-2929.md) and subject to the inherited gas schedule described under [Gas Cost](#gas-cost), for every `param`.

The available parameters are listed in the table below.

| `param` | Name                         | `in2`                         | Return value                                                                      |
|---------|------------------------------|-------------------------------|-----------------------------------------------------------------------------------|
| 0x00    | `TXTRACE_BALANCES_CHANGED`   | must be 0                     | `balances_changed` - the number of addresses with a net balance change            |
| 0x01    | `TXTRACE_SLOTS_CHANGED`      | must be 0                     | `slots_changed` - the number of `(address, slot)` pairs with a net storage change |
| 0x02    | `TXTRACE_CONTRACTS_DEPLOYED` | must be 0                     | `contracts_deployed` - the number of addresses with newly deployed contract code  |
| 0x03    | `TXTRACE_BALANCE_ADDRESS`    | index in `balances_changed`   | `change_address` - the address of the account with balance change                 |
| 0x04    | `TXTRACE_BALANCE_BEFORE`     | index in `balances_changed`   | `balance_before` - the balance of the address at the start of the transaction     |
| 0x05    | `TXTRACE_BALANCE_AFTER`      | index in `balances_changed`   | `balance_after` - the balance of the address as of this `TXTRACE` call            |
| 0x06    | `TXTRACE_SLOT_ADDRESS`       | index in `slots_changed`      | `change_address` - the address of the account with storage change                 |
| 0x07    | `TXTRACE_SLOT_KEY`           | index in `slots_changed`      | `slot_key` - the storage slot key that was changed                                |
| 0x08    | `TXTRACE_SLOT_BEFORE`        | index in `slots_changed`      | `slot_value_before` - the value of the slot at the start of the transaction       |
| 0x09    | `TXTRACE_SLOT_AFTER`         | index in `slots_changed`      | `slot_value_after` - the value of the slot as of this `TXTRACE` call              |
| 0x0A    | `TXTRACE_DEPLOYED_ADDRESS`   | index in `contracts_deployed` | `deployed_address` - the address of the newly deployed contract                   |
| 0x0B    | `TXTRACE_DEPLOYED_CODEHASH`  | index in `contracts_deployed` | `codehash_after` - the codehash of the newly deployed contract                    |
| 0x0C    | `TXTRACE_EVENTS_COUNT`       | must be 0                     | `events_count` - the total number of emitted events                               |
| 0x0D    | `TXTRACE_EVENT_ADDRESS`      | index in `events_count`       | `events_address` - the address of the contract that emitted the event             |
| 0x0E    | `TXTRACE_EVENT_TOPIC_COUNT`  | index in `events_count`       | `event_topic_count` - the number of topics of the event (0–4)                     |
| 0x0F    | `TXTRACE_EVENT_TOPIC0`       | index in `events_count`       | `event_topic0` - the first topic of the event; exceptional halt if no topic       |
| 0x10    | `TXTRACE_EVENT_TOPIC1`       | index in `events_count`       | `event_topic1` - the second topic of the event; exceptional halt if no such topic |
| 0x11    | `TXTRACE_EVENT_TOPIC2`       | index in `events_count`       | `event_topic2` - the third topic of the event; exceptional halt if no such topic  |
| 0x12    | `TXTRACE_EVENT_TOPIC3`       | index in `events_count`       | `event_topic3` - the fourth topic of the event; exceptional halt if no such topic |
| 0x13    | `TXTRACE_EVENT_DATA_LEN`     | index in `events_count`       | `event_data_len` - the byte length of the event's non-indexed data                |
| 0x14    | `TXTRACE_GAS_PRE_CHARGE`     | must be 0                     | `gas_pre_charge` - the total amount deducted from the gas payer                   |
| 0x15    | `TXTRACE_GAS_PAYER`          | must be 0                     | `gas_payer_address` - the address charged the gas pre-charge                      |


`TXTRACE`, `EVENTDATACOPY`, and `TXDIFF` are valid only for execution inside a `POST_TX` mode frame, as defined above. Executing any of these opcodes in any other context — including legacy transactions, [EIP-1559](./eip-1559.md) 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](./eip-8141.md): the amount collected from the payer when payment is approved, equal to `TXPARAM` param `0x06`. That is `gas_pre_charge = max_gas × max_fee_per_gas + blob_gas × blob_base_fee`, where `max_gas` is EIP-8141's maximum gas, including the calldata floor where it applies. For transactions with blobs attached, it therefore 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 `0x15`) and subtract `gas_pre_charge` (param `0x14`) 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` and the per-address view, and they do not set the storage bit of `account_change_flags`. Reporting them would require iterating the account's prestate storage, which is unbounded. A keyed `TXDIFF` lookup of such a slot still reports the wipe: `slot_value_before` (param `0x00`) returns the slot's transaction prestate value, and `slot_value_after` (param `0x01`) returns its current value, `0`.

An address appears in `contracts_deployed` when its code hash changed during the transaction from the empty-code hash to a non-empty code hash that is not an [EIP-7702](./eip-7702.md) delegation designator. An account that did not exist at transaction start counts as having the empty-code hash, so a contract created at a fresh address is enumerated. An account that a `CREATE`/`CREATE2` leaves with empty code is not enumerated, as it has no code change.

An account that [EIP-6780](./eip-6780.md) 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 `contracts_deployed`.

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

### Transaction Diff Lookup Opcode

`TXTRACE` enumerates the state diff but cannot be asked about one specific entry, so reading a single known value means searching the enumeration. We introduce an additional `TXDIFF` opcode (`0xb7`) for this purpose.

A single call returns one 256-bit value. Three stack inputs select it: `param` chooses what to read, `in2` is the address the query is keyed on, and `in3` is a storage slot key, a per-address index, or `0`, depending on `param`. Params `0x00` to `0x05` read one account's balance, codehash, or storage slot directly. Params `0x06` to `0x0A` expose per-address views over the diff, mapping into `TXTRACE`'s tables, and a bitmask summarizing an account's changes. For the address-keyed params `0x00` to `0x0A`, only the low 20 bytes of `in2` are used as the address, as with `BALANCE`, and the high 12 bytes are ignored. For the per-topic params, all 32 bytes of `in2` are the topic value.

| Stack     | Value   |
|-----------|---------|
| `top - 0` | `param` |
| `top - 1` | `in2`   |
| `top - 2` | `in3`   |

The selected value is pushed onto the stack.

#### Params

| `param` | Name                          | `in2`       | `in3`                           | Return value                        |
|---------|-------------------------------|-------------|---------------------------------|-------------------------------------|
| 0x00    | `TXDIFF_SLOT_BEFORE`          | address     | `slot_key` value                | `slot_value_before`                 |
| 0x01    | `TXDIFF_SLOT_AFTER`           | address     | `slot_key` value                | `slot_value_after`                  |
| 0x02    | `TXDIFF_BALANCE_BEFORE`       | address     | must be 0                       | `balance_before`                    |
| 0x03    | `TXDIFF_BALANCE_AFTER`        | address     | must be 0                       | `balance_after`                     |
| 0x04    | `TXDIFF_CODEHASH_BEFORE`      | address     | must be 0                       | `codehash_before`                   |
| 0x05    | `TXDIFF_CODEHASH_AFTER`       | address     | must be 0                       | `codehash_after`                    |
| 0x06    | `TXDIFF_ADDRESS_SLOTS_COUNT`  | address     | must be 0                       | `address_slots_count`               |
| 0x07    | `TXDIFF_ADDRESS_SLOT_INDEX`   | address     | index in `address_slots_count`  | `TXTRACE` index for `slots_changed` |
| 0x08    | `TXDIFF_ADDRESS_EVENTS_COUNT` | address     | must be 0                       | `address_events_count`              |
| 0x09    | `TXDIFF_ADDRESS_EVENT_INDEX`  | address     | index in `address_events_count` | `TXTRACE` index for `events_count`  |
| 0x0A    | `TXDIFF_ACCOUNT_CHANGE_FLAGS` | address     | must be 0                       | `account_change_flags`              |
| 0x0B    | `TXDIFF_TOPIC_EVENTS_COUNT`   | topic value | must be 0                       | `topic_events_count`                |
| 0x0C    | `TXDIFF_TOPIC_EVENT_INDEX`    | topic value | index in `topic_events_count`   | `TXTRACE` index for `events_count`  |

If the queried key `address`/`(address, slot)` was never modified during the transaction, `TXDIFF` (params `0x00`–`0x05`) returns the current live value for both the `before` and `after` variant of that param. A slot cleared by a storage wipe counts as modified for this rule, so its `before` value is its transaction prestate value, as described in [State Difference Semantics](#state-difference-semantics).

An account that does not exist reads as an empty account: balance `0` and the empty-code hash. This applies to `codehash_before` for an account that did not exist at transaction start, and to `codehash_after` for an account that does not exist at the time of the call. For a non-existent account, `TXDIFF` params `0x04` and `0x05` therefore return the empty-code hash where `EXTCODEHASH` would return `0`.

#### Per-Address Remapping

Params queryable with `TXDIFF` `0x06`, `0x07`, `0x08`, and `0x09` expose per-address filtered views over the storage slots and events exposed via enumeration by the `TXTRACE` opcode.

The count params (`0x06`, `0x08`) return the size of this view, returning `0` for an address with no entries.

The index params (`0x07`, `0x09`) map a per-address, local index, passed as `in3`, to the entry's global index in the corresponding table. The returned global index can be used directly with the per-entry `TXTRACE` params and with `EVENTDATACOPY`.

If `TXDIFF` received an invalid local index, i.e. value greater than or equal to the view's count, an exceptional halt occurs.

#### Per-Topic Views

Params `0x0B` and `0x0C` expose a per-topic-value filtered view over the events exposed via enumeration by the `TXTRACE` opcode, keyed by an indexed topic *value* rather than by the emitting address.

The count param (`0x0B`) returns the number of entries in `events_count` that carry `in2` as an indexed topic in any of the positions `topic1`, `topic2`, or `topic3`, returning `0` when no event carries the value. The signature topic `topic0` is excluded, as it identifies the event type rather than a participant.

The index param (`0x0C`) maps a per-value, local index, passed as `in3`, to the entry's global index in the `events_count` table, in ascending emission order. The returned global index can be used directly with the per-event `TXTRACE` params and with `EVENTDATACOPY`.

If `TXDIFF` received an invalid local index, i.e. a value greater than or equal to the view's count, an exceptional halt occurs.

#### Account Change Flags

Param `0x0A` returns a bitmask summarizing all net changes to the account's state. The bits follow the field order of the account tuple `(nonce, balance, storage_root, code_hash)`:

| Binary | Set when                                                                                    |
|--------|---------------------------------------------------------------------------------------------|
| 0b0001 | the account nonce differs from its transaction prestate value                               |
| 0b0010 | `balance_after != balance_before`                                                           |
| 0b0100 | any storage slot of the account differs from its prestate value (`address_slots_count > 0`) |
| 0b1000 | `codehash_after != codehash_before`                                                         |

All higher bits are set to zero.

A set bit reflects a net difference between the transaction prestate and the state as of the opcode call.
Values that were modified and later restored within the transaction do not set a bit.

`account_change_flags == 0` if and only if the account's internal state, including its entire storage, is identical to the transaction prestate. The one exception is a storage wipe of slots the transaction never wrote, as described in [State Difference Semantics](#state-difference-semantics).

The actual nonce value is not observable through `TXTRACE` or `TXDIFF`.

#### Gas Cost

`TXDIFF` params that may fall back to reading live state use the [EIP-2929](./eip-2929.md) access lists to determine their cost. All access costs in this EIP use the active fork's gas schedule, including any repricing of the referenced parameters. Where [EIP-8038](./eip-8038.md) is active, `COLD_ACCOUNT_ACCESS_COST`, `COLD_SLOAD_COST`, and `WARM_STORAGE_READ_COST` are named `COLD_ACCOUNT_ACCESS`, `COLD_STORAGE_ACCESS`, and `WARM_ACCESS`, respectively, and have values 3000, 2100, and 100. This also applies to `TXTRACE`'s warm-read cost.

- Storage slot params (`0x00`, `0x01`): `COLD_SLOAD_COST` if the `(address, slot)` pair is not in the accessed storage list; `WARM_STORAGE_READ_COST` otherwise. Only the `(address, slot)` pair is priced: the address itself is not charged `COLD_ACCOUNT_ACCESS_COST` and is not added to the accessed addresses set, so a later `BALANCE` or `CALL` of a cold address still pays the cold account cost.
- Balance and codehash params (`0x02`–`0x05`): `COLD_ACCOUNT_ACCESS_COST` if the address is not in the accessed addresses set; `WARM_STORAGE_READ_COST` otherwise.
- Per-address and per-topic view and flags params (`0x06`–`0x0C`): `WARM_STORAGE_READ_COST`. These params are answered entirely from the transaction-local state diff and never read the live state.

For params `0x00`–`0x05`, the accessed slot or address is added to the [EIP-2929](./eip-2929.md) access list after the call, and — where [EIP-7928](./eip-7928.md) is active — recorded in the block-level access list, like any other state-reading opcode. For params `0x00` and `0x01` that is the `(address, slot)` pair only; for params `0x02`–`0x05` it is the address. The full gas cost is charged before state is read: if the charge fails, or the opcode halts on a non-zero reserved input or outside a `POST_TX` frame, the opcode adds no access to the block-level access list. `TXTRACE` and `EVENTDATACOPY` read only already-recorded diff and log data and therefore add no new accesses. Params `0x06`–`0x0C` do not interact with the [EIP-2929](./eip-2929.md) access lists.

#### Block-Level Access List

Where [EIP-7928](./eip-7928.md) is active, a `TXDIFF` read of params `0x00`–`0x05` is recorded in the block-level access list as follows. A slot read (`0x00`, `0x01`) adds the slot to the account's `storage_reads`, and adds the account to the list with empty change lists if it is absent, without adding the address to the accessed addresses set. A balance or codehash read (`0x02`–`0x05`) adds the address with empty change lists if absent. As in EIP-7928, a slot appears in `storage_reads` only if it appears in no `storage_changes` entry of the same account anywhere in the block.

If a `POST_TX` 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 `POST_TX` frames themselves also remain. State changes from the validation prefix are recorded at the transaction's `block_access_index`.

### 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 (`0xb8`) 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

| Stack      | Value           |
|------------|-----------------|
| `top - 0`  | `event_index`   |
| `top - 1`  | `memOffset`     |
| `top - 2`  | `dataOffset`    |
| `top - 3`  | `length`        |

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.

### Reserved Inputs

Any `in2`/`in3` operand marked *must be 0* in the parameter tables 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 tables above, that is any value above `0x15` for `TXTRACE` or above `0x0C` for `TXDIFF`, 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 (`0x03`–`0x0B` and `0x0D`–`0x13`), an `in2` greater than or equal to that table's count (`balances_changed`, `slots_changed`, `contracts_deployed`, or `events_count`) causes an exceptional halt.

### Results Ordering

Balance and storage slot 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.

Newly deployed contracts are enumerated in ascending order sorted by the deployed address as a numerical `uint160` value.

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

The per-address views of `TXDIFF` (params `0x07` and `0x09`) and the per-topic view (param `0x0C`) list their entries in ascending order of the global index each entry maps to. A per-address slot view is therefore in slot key order, and a per-address or per-topic event view is in emission order.

## Rationale

### Selection Parameter Design

The `TXTRACE` opcode follows the same selector-and-index idea used by `FRAMEPARAM` in [EIP-8141](./eip-8141.md), a `param` selecting the field and a second operand selecting the entry. Unlike `FRAMEPARAM`, it takes `param` from the top of the stack, as `TXDIFF` does, so both diff opcodes read their selector first. This keeps the interface consistent and avoids introducing a separate opcode for every piece of trace information.

### Enumeration and Lookup

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

The `TXDIFF` opcode complements it with direct, keyed access to one specific balance, codehash, or storage slot.

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.

### Gas Cost

This EIP adds no gas constants. `TXTRACE` and the `TXDIFF` view and flags params read 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 `TXDIFF` already charges `WARM_STORAGE_READ_COST` to read its `before` or `after` value. `TXTRACE` returns those same values by index and has the same price, so the cost of a changed value does not depend on which opcode reads it. The other `TXTRACE` params, such as counts, addresses, and event fields, could cost less, as `FRAMEPARAM` does in [EIP-8141](./eip-8141.md). 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 a `POST_TX` frame first reads them, and reuse it while the diff does not change. `POST_TX` 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 `POST_TX` frame never needs the index.

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

### Direct Lookup via `TXDIFF`

`TXTRACE`'s enumeration model has no way to directly check one specific value, such as "the value of `usdc.balances[vitalik.eth]` before this transaction".
An assertion contract would have to implement its own search over the sorted enumeration output to look up such a value.
`TXDIFF` addresses this gap directly.

A point lookup over storage needs both an `address` and a `slot` to be unambiguous, not fitting the `TXTRACE`'s existing 2-argument `(param, in2)` shape.

Balance and codehash are keyed only by `address`.

### Per-Address Views

Assertion contracts frequently need per-contract answers: "did this contract's storage change at all", "did this contract emit any event", "check every slot this contract changed".

For events, no efficient workaround exists: events are enumerated in emission order, so finding one contract's events requires a linear scan over every event in the transaction. The number of unrelated events is attacker-controlled — a malicious dApp can pad a transaction with cheap logs, inflating the assertion's gas cost until it exceeds its stipend.
The per-address views make the cost proportional only to the activity of the contracts the assertion actually inspects.

Returning global indices keeps the parameter space small, as one conversion call plugs into all existing `TXTRACE` params and `EVENTDATACOPY`.

### Per-Topic Views

The per-address event view answers "which events did this contract emit", keyed by the emitting address. An assertion protecting a user asks the dual question: "which events name this user as a participant" — for example, every `Transfer` whose `from` is the user, or every `Approval` whose `owner` is the user — regardless of which contract emitted them.

Under dynamic routing the set of emitting contracts is not known at signing time, so the emitting address is not a usable key: the assertion cannot enumerate the tokens a route will touch. The participant, by contrast, is a fixed value the wallet places in the assertion at signing time. Keying the event view on the indexed topic value makes such an assertion's cost proportional to the events that name the participant, rather than to the transaction's total event count, which is subject to the same padding argument that motivates the per-address views.

Keying on the raw 32-byte topic value, rather than on an address, keeps the view semantics-free and lets it serve any indexed identity an assertion cares about, including token ids and other opaque handles carried in indexed topics.

### Account Change Flags as a Shield Assertion

A common assertion is expected to be a "shield": asserting that a given account was **not** affected by the transaction. Without the flags param this requires three separate lookups — balance, codehash, and storage count — and still leaves the nonce unobserved. `account_change_flags` collapses the entire check into a single opcode call: `flags == 0` guarantees the account's state, including its full storage, is identical to the transaction prestate.

Events are deliberately excluded from the bitmask: an emitted event is not a change to the account's state. A "the contract stayed silent" check is available separately as `address_events_count == 0`.

### Receipt Representation and Anti-DoS

If a `POST_TX` 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 `POST_TX` 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. A `POST_TX` 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 `POST_TX` Frame Mode Requirement

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

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

Because `POST_TX` 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 a `POST_TX` 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 `POST_TX` 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 a `POST_TX` 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](./eip-20.md), [ERC-721](./eip-721.md), [ERC-1155](./eip-1155.md) 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](./eip-8141.md) discards transient storage between frames and gives a later frame no access to an earlier frame's return data, so a `POST_TX` 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](./eip-4844.md) transactions, so the same subtraction isolates pure ETH transfers. `gas_payer_address` completes the picture: the [EIP-8141](./eip-8141.md) 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`, `EVENTDATACOPY`, and `TXDIFF` 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](./eip-8141.md) (`requires: 8141`): `TXTRACE`, `EVENTDATACOPY`, and `TXDIFF` can only execute inside an EIP-8141 `POST_TX` frame, as defined under [The `POST_TX` Frame Mode](#the-post_tx-frame-mode). Legacy transactions, [EIP-1559](./eip-1559.md) transactions, and any other EIP-8141 frame mode cannot use any 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 `POST_TX` 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 `POST_TX` Frame Inclusion

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

Because a transaction can contain multiple `POST_TX` 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 `POST_TX` 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 POST_TX` frame target during validation guarantees the transaction will be evaluated against the intended immutable policy.

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

```solidity
function requirePostTx() 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; // POST_TX frames are a contiguous suffix; no more left to check
        }
        if (f.target == this.postTxEnforcer) {
            // (Optionally check f.data matches the expected POST_TX method selector)
            found = true;
            break;
        }
    }
    require(found, "Missing required POST_TX frame for this account");
}
```

### Assertion Inputs Controlled by the Transaction

The execution body runs before any `POST_TX` 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 `POST_TX` 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 a `POST_TX` 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 `POST_TX` 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](./eip-8141.md) expiry verifier frame limits that time, not the change.
- Revert target state is not the prestate: the verification 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 verification 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.

Assertions concerned with specific contracts should use the per-address `TXDIFF` views instead of enumerating the global tables, making their gas cost independent of unrelated — and potentially attacker-controlled — entries.

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 `POST_TX` Frame Mode Not Reverting Validation Prefix

It is crucial that the transaction relying on a `POST_TX` 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 `POST_TX` 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 a `POST_TX` 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](./eip-8141.md#structural-rules)), not a protocol invariant — nothing prevents it from producing other side effects, which `TXTRACE` will surface but which `POST_TX` cannot undo. A wallet must never use the `deploy` frame for anything beyond deploying `tx.sender`; they cannot rely on `POST_TX` to catch or reverse it.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
