EIP.tools

EIP.tools

āš ļø DraftStandards Track: Core

EIP-8386: Transaction Events View

Exposes the current transaction's emitted events through the EIP-8130 Transaction Context precompile, enabling in-phase outcome assertions

Authors
Created2026-08-21
Discussion Linkhttps://ethereum-magicians.org/t/eip-8386-transaction-events-view/29472
Requires

Markdown

https://raw.githubusercontent.com/ethereum/EIPs/re...
Pull Request#12218PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationConstantsEvents ViewsUsage: Outcome AssertionsShield Construction RulesRequired AssertionsRationaleBackwards CompatibilitySecurity ConsiderationsCopyright

Abstract

This proposal extends the EIP-8130 Transaction Context precompile with read-only views over the events emitted so far in the current transaction. Combined with EIP-8130's existing call-phase atomicity, this enables outcome assertions: an ordinary call placed at the end of a protected phase reads the transaction's events (and any trusted contract state), and reverts if the outcome violates the signer's expectations, rolling back exactly that phase. No opcodes, transaction fields, statuses, or revert semantics are added; clients expose a list they already maintain for the receipt.

Motivation

Simulation can be misleading: previews omit runtime effects such as MEV, oracle movement, reentrancy side effects, and fee variance, and users are routinely induced to sign transactions that appear safe but execute differently on-chain. Wallets need a way to enforce, atomically and on-chain, that the outcome the user approved is the outcome that happens.

For known, positive conditions ("I received at least X", "I still own NFT N"), EIP-8130 already suffices with no protocol change: the wallet appends a check call to the protected phase, the check reads the asset contracts' interfaces, and a revert rolls the phase back atomically. The gap is closed-world conditions — "no approval I did not intend, to any spender", "no operator was set on my NFTs", "nothing touched my account's authority" — which cannot be checked through interfaces, because the checker does not know which spenders, operators, or contracts to query. The attacks live in these unknown-unknowns.

Events close the gap for the assets worth protecting. Honest, standards-conforming contracts — the tokens and NFTs whose loss the user fears — emit a standardized record of every consequential action: Transfer and Approval (ERC-20), Transfer, Approval, and ApprovalForAll (ERC-721/ERC-1155), and EIP-8130 itself protocol-injects ActorAuthorized, ActorRevoked, AccountCreated, and DelegationApplied receipt logs, so changes to the account's own authority are event-visible by specification. A complete scan of the transaction's events is therefore a complete scan of what the honest contracts did — exactly the closed-world check interfaces cannot express. Events are, however, unreadable from the EVM today: no opcode or call exposes emitted logs to on-chain code. This proposal exposes them where EIP-8130 already exposes transaction context.

Adoption is immediate: because every EOA is auto-delegated to the default account, batching, phase atomicity, and (with this proposal) event-checked assertions are available to every account with nothing to activate, no delegation step, and no wallet-communication standard to roll out.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Constants

NameValueComment
EVENT_VIEW_GAS_COSTTBDBase cost of each events view
ASSERTION_AUTHTBDAddress of the assertion wrapper authenticator (see Required Assertions)

Events Views

The Transaction Context precompile at TX_CONTEXT_ADDRESS gains the following read-only views over the events emitted by the current transaction up to the point of the call:

ViewReturns
eventCount()uint256 — events emitted so far
eventAt(uint256 i)(address emitter, bytes32[] topics, bytes data)
eventsOf(address a)uint256 — events emitted by a so far
eventIndexOf(address a, uint256 j)uint256 — global index of a's j-th event
  • Events are enumerated in emission order, matching their log index within the transaction.
  • The view reflects the transaction's live log journal: events emitted within a call subtree or phase that has reverted are not present, exactly as they are absent from the receipt. An assertion therefore observes precisely the events that will appear in the receipt if execution up to that point commits.
  • eventAt(i) with i >= eventCount() reverts; eventIndexOf(a, j) with j >= eventsOf(a) reverts.
  • The per-address views are answered from a per-emitter index so their cost is independent of unrelated events; assertion authors SHOULD use them so an adversary cannot inflate assertion gas by padding the transaction with cheap logs from other contracts.

Availability. The views are callable during the execution of calls (any call's subtree) in an EIP-8130 transaction. Called during validation, authenticator execution, or account_changes processing, the precompile reverts: authenticators remain functions of declared context only. On non-8130 chains the address has no code and STATICCALL returns empty, per EIP-8130's portability table.

Gas. Each view costs EVENT_VIEW_GAS_COST plus 3 gas per 32 returned bytes, matching the precompile's existing pricing model. No state is read; the views are answered from the client's in-memory log journal.

Protocol-injected logs. EIP-8130's protocol-injected receipt logs (ActorAuthorized, ActorRevoked, AccountCreated, DelegationApplied) are visible through these views from the point they are injected, so an assertion can verify that a transaction performed no account-authority changes.

Usage: Outcome Assertions

No new transaction semantics are required; assertions use existing phase atomicity. The following example is informative:

calls = [
  [ sponsor_compensation ],                          // phase 0: commits regardless
  [ action_a, action_b, shield.check(params) ]       // phase 1: protected scope
]

shield.check is the final call of the protected phase. It scans the transaction's events via the views — e.g. revert on any Approval where owner == account and the spender is not in params; revert on any ApprovalForAll(account, …, true); revert on any ActorAuthorized/DelegationApplied for the account; verify expected Transfers — and MAY additionally read trusted contract state directly (e.g. balanceOf, SELFBALANCE against a signed expected minimum). On revert, standard phase atomicity rolls back the protected phase (actions included); earlier phases, account_changes, the nonce, and the gas charge commit; the transaction is included with the shield's revert reason as the failed phase's return data, and the payer is charged for gas used.

Shield Construction Rules

These rules bind wallets that construct shielded transactions. They are the signer's responsibility because the signer signs calls; the protocol does not enforce them except through ASSERTION_AUTH where an actor is configured with it.

  • The shield MUST be the final call of the final phase containing untrusted actions; the events view reflects emissions up to the point of the call, so anything after the shield is outside its protection.
  • Untrusted dApp actions MUST NOT be placed in phases before the shield's phase (those commit on shield failure) — earlier phases are for trusted setup and compensation only.
  • The shield MUST bind every event check to its expected emitter, and MUST NOT derive a positive conclusion (for example "the payment happened") from topic patterns alone.
  • A shield revert does not undo account_changes, so wallets MUST review account-change entries directly rather than relying on a shield to catch them.
  • The shield target SHOULD be an immutable contract; an upgradeable shield's checks can change after review.
  • Native ETH deltas emit no events; a shield bounds them by checking SELFBALANCE (or the balance of watched accounts) against expected values supplied in params, which the wallet computes at signing time.
  • Multiple independent shields compose as additional calls at the end of the protected phase.

Required Assertions

This EIP defines a wrapper authenticator at ASSERTION_AUTH, deployed at a deterministic CREATE2 address across chains like the EIP-8130 canonical authenticators. The wrapper is part of this EIP's normative surface; configuring an actor with it is optional. It makes a shield's presence a validity condition per actor:

sender_auth = ASSERTION_AUTH ‖ shield_target (20) ‖ inner_authenticator (20) ‖ inner_data

authenticate(hash, data) reads getTransactionCalls() (available at validation), reverts unless the final call of the final phase targets shield_target, then delegates to the inner authenticator and returns its actorId. A transaction from such an actor without its shield in terminal position is invalid at validation, not merely reverted. The wrapper's dependency set is the transaction context plus the inner authenticator's, so it remains on the fixed-cost validation path when the inner authenticator is canonical. This makes a session key expressible as "only valid with my shield attached", as configuration rather than per-transaction construction.

Rationale

Events rather than a state diff. A per-slot state-diff surface (prestate tracking, collapsed writes, keyed lookups) imposes real client cost and is aimed at adversarial contracts that misreport through their interfaces. The assets whose loss users fear are standards-conforming contracts whose consequential actions are all evented, and EIP-8130 events the account-authority layer by specification. The log list already exists in every client for receipt construction, journaled per subtree; these views expose it. The closed-world capability — the one thing interface checks cannot express — is retained in full.

In-phase checks rather than a deferred assertion stage. EIP-8130's phases are already an atomicity boundary, and calls is signed, so nothing can be appended after the shield: terminal position is under the signer's control. A deferred, protocol-run assertion stage was considered and rejected as unnecessary machinery once the introspection surface is events; the sole cost is that shield position is a construction rule rather than a protocol guarantee, mitigated by ASSERTION_AUTH, which turns terminal position into a validity condition checked from declared context.

Live journal semantics. Reporting only surviving events (reverted subtrees excluded) makes the assertion's view coincide with the receipt's, which is the object of the user's expectations, and falls out of the client's existing journaling for free.

Mid-execution observability. The views are open to any called contract during calls. This admits the self-check pattern (a contract verifying its own emissions before returning) and keeps the gate rule trivial. The trade-off — a contract conditioning behavior on unrelated earlier events in the transaction — is acknowledged under Security Considerations.

Sponsorship-compatible rollback. Because a shield failure is a phase failure, the sponsor's compensation in an earlier phase commits while the sponsor is paid for gas; the payer signs over calls and can verify its compensation sits outside the protected phase. An always-failing shield cannot be used to reclaim compensation from a sponsor.

Backwards Compatibility

No changes to opcodes, transaction types, the EIP-8130 wire format, signature payloads, statuses, or revert semantics. Transactions that never call the views behave exactly as before. Contracts cannot currently read logs, so no existing code changes meaning.

Security Considerations

Insufficiently restrictive assertions. A shield that checks too little is worse than none, because it misleads the user into confidence. Wallet guidance should treat incomplete assertions as no assertion. A sound default shield checks, at minimum: unexpected Approval/ApprovalForAll where the account is the owner, unexpected Transfer where the account is the source, any account-authority log (ActorAuthorized, ActorRevoked, DelegationApplied) not signed for, and native balance bounds.

Event forgery. Any contract can emit a log with any topics; the only authentic field is the emitter address. This is why the construction rules require every event check to be bound to its expected emitter (the per-address views make this the natural and cheapest pattern) and forbid positive conclusions from topic patterns alone. Unbound negative checks ("no Approval anywhere naming me as owner") can be spoofed only into a revert, which is the protective direction, but degrade into a griefing surface for the transaction; emitter-bound checks avoid both.

Non-conforming contracts. A contract that moves value without emitting standard events defeats event-based checks for that contract. This is out of scope by construction: the shield protects the user's standards-conforming assets and account authority; interactions denominated entirely in a non-conforming contract's own state carry that contract's risk regardless. Native ETH movements emit no events today and are bounded via balance checks (see Usage); where EIP-7708 (ETH transfers emit logs) is active, ETH movements become event-visible and the balance-check workaround is unnecessary.

Shield position. The shield protects only the phase it is in and observes only emissions before its call. The construction rules therefore require it to be the final call of the protected phase with no untrusted actions in earlier phases. ASSERTION_AUTH enforces terminal position as a validity condition where configured.

Pre-execution prefix. account_changes run before calls. Their protocol-injected logs are visible to the shield, but a shield revert does not undo them, which is why the construction rules require wallets to review account-change entries directly.

Log-padding griefing. An adversary can pad a transaction's global event list with cheap logs. Assertion cost is kept independent of unrelated events by the per-address views; shield authors should enumerate per watched address rather than scanning the global list.

Conditioning on transaction contents. Any called contract can observe earlier events in the transaction, enabling order-dependent behavior across otherwise-unrelated calls. Consumers should not treat the events view as a substitute for their own state reads in ordinary application logic.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth