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.
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.
| Name | Value |
|---|---|
| TXTRACE_GAS_COST | TBD |
| EVENTDATACOPY_GAS_COST | TBD |
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.
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.
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 opcodeThis 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 | Value |
|---|---|
top - 0 |
event_index |
top - 1 |
memOffset |
top - 2 |
dataOffset |
top - 3 |
length |
No stack output value is produced.
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.
event_index >= events_count, an exceptional halt occurs.dataOffset + length exceeds the event's data length, an exceptional halt occurs.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.
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.
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.
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.
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 OpcodeEvent 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.
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.
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.
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.
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 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 and related rights waived via CC0.