---
eip: 7906
title: Transaction Assertions via State Diff Opcode
description: An opcode that provides a mechanism to restrict the outcomes of transaction execution
author: Alex Forshtat (@forshtat), Shahaf Nacson (@shahafn), Dror Tirosh (@drortirosh), Yoav Weiss (@yoavw), Fredrik Svantes (@fredrik0x)
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 a new opcode that allows contracts to inspect the transaction outcomes on-chain. This opcode will allow contract developers to define assertions for state changes that can be enforced on-chain. These can protect Ethereum users by restricting the behavior of the smart contracts they are interacting with.

## 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

### Constants

| Name                   | Value |
|------------------------|-------|
| TXTRACE_GAS_COST       | TBD   |
| EVENTDATACOPY_GAS_COST | TBD   |
| `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:

- The static constraint `assert frame.mode < 3` on each frame is replaced by `assert frame.mode < 4`, admitting `POST_TX` as a valid mode value.
- `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. A `POST_TX` frame has no valid reason to call `APPROVE`, and its usage is forbidden.
- If a `POST_TX` frame reverts, the entire execution body of a transaction is reverted unconditionally. This overrides the atomic-batch unrolling behavior that would otherwise apply: a `POST_TX` revert always validates or reverts the whole transaction execution, up to the "validation prefix", rather than merely unwinding an atomic batch.
- A `POST_TX` revert does **not** invalidate the transaction (unlike a `VERIFY` frame revert). The transaction remains valid, is included in the block, and generates a receipt with a failed status (`status = 0`).
- 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 revert.
- 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.

It can be used to retrieve the full state diff of the current transaction up to this point.

It accepts a `(param, index)` inputs similar to the `FRAMEPARAM` opcode from [EIP-8141](./eip-8141.md).
The available parameters are listed in the table below.

| `param` | `in2`                         | Return value                                                                  |
|---------|-------------------------------|-------------------------------------------------------------------------------|
| 0x00    | must be 0                     | `balances_changed` - the total number of changed balances                     |
| 0x01    | must be 0                     | `slots_changed` - the total number of changes storage slots                   |
| 0x02    | must be 0                     | `contracts_deployed` - the total number of newly deployed contracts           |
| 0x03    | index in `balances_changed`   | `change_address` - the address of the account with balance change             |
| 0x04    | index in `balances_changed`   | `balance_before` - the balance of the address at the start of the transaction |
| 0x05    | index in `balances_changed`   | `balance_after` - the balance of the address as of this `TXTRACE` call        |
| 0x06    | index in `slots_changed`      | `change_address` - the address of the account with storage change             |
| 0x07    | index in `slots_changed`      | `slot_key` - the storage slot key that was changed                            |
| 0x08    | index in `slots_changed`      | `slot_value_before` - the value of the slot at the start of the transaction   |
| 0x09    | index in `slots_changed`      | `slot_value_after` - the value of the slot as of this `TXTRACE` call          |
| 0x0A    | index in `contracts_deployed` | `deployed_address` - the address of the newly deployed contract               |
| 0x0B    | index in `contracts_deployed` | `codehash_after` - the codehash of the newly deployed contract                |
| 0x0C    | must be 0                     | `events_count` - the total number of emitted events                           |
| 0x0D    | index in `events_count`       | `events_address` - the address of the contract that emitted the event         |
| 0x0E    | index in `events_count`       | `event_topic_count` - the number of topics of the event (0–4)                 |
| 0x0F    | index in `events_count`       | `event_topic0` - the first topic of the event; exceptional halt if no topic   |
| 0x10    | index in `events_count`       | `event_topic1` - the second topic of the event                                |
| 0x11    | index in `events_count`       | `event_topic2` - the third topic of the event                                 |
| 0x12    | index in `events_count`       | `event_topic3` - the fourth topic of the event                                |
| 0x13    | index in `events_count`       | `event_data_len` - the byte length of the event's non-indexed data            |
| 0x14    | must be 0                     | `gas_pre_charge` - the total amount deducted from the gas payer               |
| 0x15    | 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.

For transactions with blobs attached, the `gas_pre_charge` parameter includes the blob fees as `gas_pre_charge = gas_limit × gas_price + blob_count × GAS_PER_BLOB × 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.

### Transaction Diff Lookup Opcode

While `TXTRACE` enumerates the full state diff, it has no mechanism to directly query the diff for one specific account's balance, codehash, or storage slot.

We introduce an additional `TXDIFF` opcode for this purpose, complementing `TXTRACE`'s enumeration model with direct, keyed access.

#### Params

| `param` | `in2`   | `in3`                           | Return value                        |
|---------|---------|---------------------------------|-------------------------------------|
| 0x00    | address | `slot_key` value                | `slot_value_before`                 |
| 0x01    | address | `slot_key` value                | `slot_value_after`                  |
| 0x02    | address | must be 0                       | `balance_before`                    |
| 0x03    | address | must be 0                       | `balance_after`                     |
| 0x04    | address | must be 0                       | `codehash_before`                   |
| 0x05    | address | must be 0                       | `codehash_after`                    |
| 0x06    | address | must be 0                       | `address_slots_count`               |
| 0x07    | address | index in `address_slots_count`  | `TXTRACE` index for `slots_changed` |
| 0x08    | address | must be 0                       | `address_events_count`              |
| 0x09    | address | index in `address_events_count` | `TXTRACE` index for `events_count`  |
| 0x0A    | address | must be 0                       | `account_change_flags`              |

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.

#### Per-Address Remapping

Params queryalbe 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` recevied an invalid local index, i.e. 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 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:

- Storage slot params (`0x00`, `0x01`): `COLD_SLOAD_COST` (2100) if the `(address, slot)` pair is not in the accessed storage list; `WARM_STORAGE_READ_COST` (100) otherwise.
- Balance and codehash params (`0x02`–`0x05`): `COLD_ACCOUNT_ACCESS_COST` (2600) if the address is not in the accessed addresses set; `WARM_STORAGE_READ_COST` (100) otherwise.
- Per-address view and flags params (`0x06`–`0x0A`): a flat cost of `TXTRACE_GAS_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 respective access list after the call. Params `0x06`–`0x0A` do not interact with the [EIP-2929](./eip-2929.md) access lists.

`codehash_before` is equal to the empty-code hash for undeployed contracts.

### 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.

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

#### `EVENTDATACOPY` opcode

This opcode copies event 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 semantics match `CALLDATACOPY`, copying `length` bytes from the event's non-indexed data, starting at the given byte `dataOffset`, into a memory region starting at `memOffset`.

- If `event_index >= events_count`, an exceptional halt occurs.
- If `dataOffset + length` exceeds the event's data length, an exceptional halt occurs.

## Rationale

### Selection Parameter Design

The `TXTRACE` opcode follows the same `(param, index)` two-argument pattern used by `FRAMEPARAM` in [EIP-8141](./eip-8141.md). 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.

### 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`.

### 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 generates a standard `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 entire transaction.

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` revert unconditionally invalidates the whole transaction, 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 invalidate the transaction 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.

### Gas Pre-Charge Parameter

The gas pre-charge (`gas_limit × gas_price`) is deducted at transaction start 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 invalidates the entire transaction, including any gas payment already approved by an earlier frame, 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.

### 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). Because the wallet controls the validation prefix (e.g., ensuring the `deploy` frame points to a trusted factory), it is inherently safe to commit it to state. Malicious dApps cannot inject arbitrary state changes into the validation prefix.

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.

## Copyright

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