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
| Name | Value | Comment |
|---|---|---|
EVENT_VIEW_GAS_COST | TBD | Base cost of each events view |
ASSERTION_AUTH | TBD | Address 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:
| View | Returns |
|---|---|
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)withi >= eventCount()reverts;eventIndexOf(a, j)withj >= 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 inparams, 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_dataauthenticate(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
Copyright and related rights waived via CC0.
