EIP-7906 - Transaction Assertions via State Diff Opcode

Created 2025-02-21
Status Draft
Category Core
Type Standards Track
Authors

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

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

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 normally a tx.origin but may be dependent on a future transaction type details. For EIP-8141 transactions, the target of the APPROVE_PAYMENT frame may replace the transaction sender as the gas payer.

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.

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.

Rationale

Selection Parameter Design

The TXTRACE opcode follows the same (param, index) two-argument pattern used by FRAMEPARAM in EIP-8141. This keeps the interface consistent and avoids introducing a separate opcode for every piece of trace information.

Enumeration instead of lookup

The TXTRACE opcode exposes transaction outcomes through index-based access over the full set of observable state changes, but it does not provide a mechanism to look up a balance or storage change for a specific address or slot key.

Doing so would require introducing a separate "state diff lookup opcode", which would spare contracts from performing a linear search over enumerated results — a common operation in post-transaction assertions.

A lookup opcode is deliberately excluded from this proposal for the following reasons.

Redundant querying mechanism

While the TXTRACE opcode is sufficient for contracts to gain access to any storage change within a transaction, a lookup mechanism has no way to enumerate all changes and would still rely on TXTRACE to list all observed state diffs when expressing any "negative conditions" (i.e. "only these changes happened"). Relying solely on TXTRACE for both lookup and enumeration is feasible and avoids introducing a second, partially overlapping querying mechanism.

Implementation overhead

A direct-access storage opcode would require EVM implementations to expose a lookup interface from an (address, slot) key tuple to the storage diff entry. A topic-based lookup opcode would require clients to maintain a per-topic index over the transaction log and expose it within the EVM. These introduce extra complexity to client implementations of this proposal and represent non-trivial operations.

TXTRACE's enumeration model is simpler; it relies only on the ordered state-diff data that every EVM client already tracks during execution.

High-level language syntax support

For the overwhelming majority of assertion use cases, a compiler can provide a high-level abstraction that internally searches the enumerated TXTRACE results with minimal overhead.

Even in the worst case, the gas cost of a compiler-generated linear search over TXTRACE results is negligible relative to the gas cost of any state-changing operation.

Typical transaction assertion costs are negligible

Balance and storage results are returned in sorted order, so a single entry lookup by address or slot key can use binary search. Events are in emission order and require a linear scan.

At the 16,777,216 gas transaction limit specified by EIP-7825, the entry counts and corresponding scan costs, assuming TXTRACE_GAS_COST = 100 gas comparable to warm memory access, are the following:

Category Typical transaction entries Max possible entries (estimated) Max possible lookup cost for single entry Typical tx lookup cost for single entry Max cost of a full iteration of state diff by assertion script Typical tx cost
Balance changes ~15 1,380 log₂(1,380) × 100 = 1,100 log₂(15) × 100 = 400 1,380 × 100 = 138,000 15 × 100 = 1,500
Storage slots ~100 3,200 log₂(3,200) × 100 = 1,200 log₂(100) × 100 = 700 3,200 × 100 = 320,000 100 × 100 = 10,000
Events ~40 42,600 42,600 × 100 = 4,260,000 40 × 100 = 4,000 42,600 × 100 = 4,260,000 40 × 100 = 4,000
Total 15,500

Most assertion scripts are expected to enumerate the full set of allowed state changes and will not require a binary search. Binary search is relevant only for sparse, targeted checks against a small subset of entries.

The values given for a "typical transaction" attempt to reflect state changes we might expect from some complex DeFi transaction.

Use with Frame Transactions

When used within an EIP-8141 frame transaction, placing the assertion logic in the last frame ensures the diff is final and the assertion can reason about the full transaction outcome.

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.

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 transactions, so the same subtraction isolates pure ETH transfers regardless of transaction type. gas_payer_address completes the picture: in EIP-8141 transactions the 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 uniformly across all transaction types.

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.

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.

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.

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.

Copyright

Copyright and related rights waived via CC0.