marc:
We have to list the assets, approvals, and config slots we care about, and the Guard verifies those. The problem is that the attacks tend to be the ones nobody thought to put on the list.
If I understood well the EIP, is that it gives the assertion that could reason about everything that actually changed in the transaction, not just a predefined set. “nothing happened that I didn’t expect”.
Hello @marc , happy to see you here and thank you for your feedback!
Indeed, one of the main points of EIP-7906 is solving this exact threat and finally giving Ethereum users the bulletproof guarantee that their transactions have absolutely zero unintended consequences!
It would also let us cover state we can’t reach from inside the account at all, like a value in another contract that isn’t exposed through a view function.
Exactly, and it even goes one step further than that. Whenever the contract you are interacting with is a proxy, if the attacker was be able to swap the implementation of the contract, then the same view functions the old checkAfterExecution relies on would be lying to the Guard about the contract’s real state.
Exposing the raw storage slots access, while sometimes tricky, removes these limitations and risks entirely.
That would be extremely useful, thank you! We are currently in the process of creating a testnet with EIP-7906 enabled, and I will be happy to get in touch regarding the testing process.
In the meantime, you can track the implementation progress in the Ethrex client here:
eip-8141-1 ← eip-7906
opened 03:00PM - 18 Jun 26 UTC
## Motivation
EIP-7906 (Transaction Assertions) lets a frame transaction append… assertion frames that inspect the whole transaction's effects after the body has executed and revert the entire transaction if an invariant is violated (e.g. "this tx made no hidden ERC-20 approval"). It builds on EIP-8141 frame transactions (tx type `0x06`) and activates under `Fork::Hegota`.
The spec recently landed two merged updates that change the mechanism this branch already partially implemented:
- **ethereum/EIPs #11829** — POST_TX frame mode: assertions run as a trailing-suffix frame executed read-only after the tx body, rather than via the older trace-export approach.
- **ethereum/EIPs #11830** — TXDIFF opcode: a keyed before/after state-diff lookup replacing the previous bulk trace-copy model.
This PR updates the existing EIP-7906 implementation (which shipped the older `TXTRACE`/`EVENTDATACOPY` trace model) to the merged spec: it introduces the `POST_TX` frame mode, gates the introspection opcodes to that frame, and adds the new `TXDIFF` opcode. The spec was read against `eips.ethereum.org` master on 2026-06-24, including both merged PRs.
## Description
### POST_TX frame mode (`FrameMode::PostTx = 3`)
A new frame mode for trailing assertion frames:
- Runs read-only: dispatched as a STATICCALL with `caller = ENTRY_POINT`, `static = true`.
- Executes after the transaction body (it is the trailing suffix of the frame list).
- A reverted POST_TX frame invalidates the **whole transaction body**, overriding atomic-batch unrolling — the same tx-level invalidation as a reverted `VERIFY` frame. This sets `tx_invalid` at both the atomic-batch-revert and non-batch-revert sites in `vm.rs`.
- Rejected in the validation-prefix simulation: POST_TX is part of the body, never the prefix.
The three frame-mode dispatch sites are wired: execution dispatch, default-code path, and validation-prefix rejection.
### Frame-mode namespace (Hegota)
| Mode | Value | EIP |
|------|-------|-----|
| DEFAULT | 0 | 8141 |
| VERIFY | 1 | 8141 |
| SENDER | 2 | 8141 |
| **POST_TX** | **3** | **7906** |
| DEP_VERIFY | 4 | 8288 (deferred) |
`validate_static_constraints` admits modes `0..=3`; `frame.mode >= 4` is rejected (reserved). When EIP-8288 resumes, this relaxes to `>= 5`.
### Trailing-suffix structural rule
Static check: once any frame is POST_TX, every later frame must be POST_TX (`"POST_TX frames must form a contiguous trailing suffix"`).
### Introspection opcodes (gated to POST_TX frames only)
`require_post_tx_frame(vm)` gates all three opcodes: they exceptional-halt (`InvalidOpcode`) unless the enclosing tx frame's mode is POST_TX. Gating holds for nested calls in the POST_TX subtree (`current_frame_index` tracks the enclosing tx frame).
| Opcode | Byte | Detail |
|--------|------|--------|
| `TXTRACE` | `0xB5` | Stack `[in2, param]` (in2 on top). Reads the tx-scoped diff: balance/slot/code-deployment change counts + indexed fields, ordered event fields, gas pre-charge (`0x14`) and gas payer (`0x15`). Gas 100 (provisional, spec example value). |
| `EVENTDATACOPY` | `0xB6` | Stack `[event_index, memOff, dataOff, length]`. Copies an emitted event's data into memory (past-the-end reads halt; no zero-fill). Gas mirrors CALLDATACOPY. |
| `TXDIFF` | `0xB7` | New keyed before/after lookup; stack `[param, address, in3]` (param on top). `param` selects `slot_before`/`slot_after`/`balance_before`/`balance_after`/`codehash_before`/`codehash_after`. `in3` is the slot key for slot params and must be zero for scalar params. Gas 2100 (provisional). |
### TXDIFF keyed before/after lookup
`TXDIFF` reads the transaction's prestate from `initial_accounts_state` ("before") and the live post-body value from `current_accounts_state` ("after"). A key the transaction never modified reads the live value both ways; an undeployed account's `codehash_before` is the empty-Keccak hash. Reads load the account/slot into the diff caches but never trigger EIP-2929 warm/cold accounting. Because POST_TX is the trailing suffix, the live `current_accounts_state` already reflects the executed body, so before vs after is the whole-tx diff.
## How to Test
Integration suite (POST_TX-frame assert-or-revert):
```
cargo test -p ethrex-levm-test eip7906
```
In-crate pure-function unit tests for the diff views:
```
cargo test -p ethrex-levm pure_fn_tests
```
The integration suite covers: TXTRACE inside POST_TX + whole-body revert + gating (DEFAULT frame and normal-tx halt), TXTRACE storage-change observation / gas payer + pre-charge / undefined-param + scalar-in2 halts, EVENTDATACOPY copy + gating, TXDIFF slot/balance/codehash before/after + unmodified-reads-live + scalar-in3 halt + gating, and fork gating (opcodes invalid before Hegota).
The pure-function tests cover the diff views (`balance_changes`, `slot_changes`, `deployed_contracts`): counts, before/after values, address/slot sorting, net-zero/restored/preexisting exclusions, and EIP-7702 delegation exclusion.
## Divergences from the draft spec
1. **TXDIFF gas `2100` is provisional.** #11830 marks the gas cost TBD; this branch prices a keyed before/after lookup as a cold SLOAD (`SLOAD_COLD_DYNAMIC = 2100`) rather than the flat warm TXTRACE cost, since the lookup may touch a cold account/slot. **TXTRACE gas `100` is likewise the EIP's own provisional example value.** Both finalize once #11830 freezes.
2. **TXDIFF "after" reads live state via the diff caches**, not a separately materialized post-tx snapshot. Equivalent to the spec intent; flagged for cross-client confirmation of edge cases.
3. **Spec opcode bytes here, renumbered on integration.** This branch uses the spec's bytes `TXTRACE = 0xB5` / `EVENTDATACOPY = 0xB6` / `TXDIFF = 0xB7`. On the Hegotá integration branch these are renumbered to `0xB6`/`0xB7`/`0xB8` to avoid the `0xB5` collision with EIP-8272's `RECENTROOTREFLOAD`.
## Flagged for devnet validation
- **Whole-body-revert receipt semantics.** The spec states a failed POST_TX assertion "unconditionally invalidates the entire transaction, including any gas payment already approved", while also describing the validation prefix as not reverted in a mempool-compatible way. ethrex's consensus path reverts the whole tx via the tx-level backup (the tx is excluded) and sets `tx_invalid`. The precise *included-but-reverted vs excluded* receipt semantics and the prefix-payment interaction are flagged for devnet + cross-client interop confirmation (see the `NOTE` in `vm.rs` at the non-batch-revert site).
- **Provisional TXDIFF gas** — finalize once #11830 freezes the value.
- **Frame-tx gas accounting observation.** In the frame-tx execution path, a body-frame SSTORE was observed to consume substantially more than the bare opcode cost in the integration harness; POST_TX assertion frames should be budgeted generously until frame-tx gas accounting is confirmed on the devnet. Tracked separately from EIP-7906 (frame-tx gas accounting, not assertion logic).
- Cross-client interop on the POST_TX trailing-suffix structural rule and the gating of the three opcodes outside POST_TX.
## Related
- Spec: ethereum/EIPs `EIPS/eip-7906.md` (Draft, Standards Track: Core; requires EIP-8141), including merged PRs ethereum/EIPs #11829 (POST_TX frame mode) and ethereum/EIPs #11830 (TXDIFF opcode).
- Builds on EIP-8141 frame transactions (tx type `0x06`).
- Docs: `docs/eip-7906.md`.
## Checklist
- [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. _(N/A — execution-layer logic; no `Store` schema change.)_
Hello @zergity , nice to meet you!
I appreciate your feedback, however EIP-7906 has been written with an assumption that not all security-critical state changes on Ethereum are 100% guaranteed to be wrapped with an informative event emission.
This is especially important in the context of potential DELEGATECALL operations performed by Smart Contract Accounts, as these calls would be able to modify the storage slots directly, circumventing any event-based guards.
One more thing, as a result of a number of discussions of the transaction assertions approach, I have made a number of significant changes to the EIP worth discussing:
Strictly require a new EIP-8141 Frame Transaction mode to access Transaction Assertions opcodes.
This is done to prevent the ability of executable EVM code from “introspecting” the state of the transaction execution in-flight, that could create a convoluted mess of interdependencies.
Introduce TXDIFF opcode that allows querying the state changes for a specific (address, slot) tuple without iteration over all state changes.
Enables more efficient assertions that only care about a specific state changes, as opposed to the expected use-case for TXTRACE opcode.
On the side of Clear Signing, I have been working to introduce a format describing the storage layout and its human-readable meaning to the signers (hardware wallets, etc.)
You can read more about it in the following links:
This ERC is an attempt to standardize the description of any EVM contract’s storage layout metadata using a JSON format, to be used with transaction simulation and transaction assertion features by the hardware wallets that implement Clear Signing (ERC-7730 ).
Knowing the storage layout of the contract being interacted with allows the hardware wallet to trace a transaction locally and display information like:
"This transaction sets USDC::balanceOf[vitalik.eth] := 1000"
With smart contract acc…
master ← forshtat:ideas-erc7730-storage-layout
opened 12:42PM - 01 Jun 26 UTC
In order to display the Transaction Outcome Simulation results (and Transaction … Assertions) in the trusted (hardware) wallet UI, the wallet needs to trust the simulation provider to supply the correct simulation results data.
In my opinion, it is not optimal to also trust the simulation provider with parsing the contracts' storage layout and interpreting the simulated state changes to the user.
I see the annotated storage layout as a part of the contract's static data similar to the calldata inputs and belongs in ERC-7730 registry.
This PR is still WIP.
All feedback is greatly appreciated!