Skip to content

feat(simulation): update how we select revert reason - #975

Open
crystalt wants to merge 1 commit into
crystal/trace-call-create-outcomesfrom
crystal/frame-aware-revert-reason
Open

feat(simulation): update how we select revert reason#975
crystalt wants to merge 1 commit into
crystal/trace-call-create-outcomesfrom
crystal/frame-aware-revert-reason

Conversation

@crystalt

@crystalt crystalt commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

What

The response-level revertReason now follows the terminally failing frame path instead of "first revert seen bottom-up":

  • A reverted frame's own non-empty payload wins.
  • An empty revert inherits from its last child, only if that child also failed — a caught sibling revert followed by successful work is discarded.
  • Safe4337Module's bare ExecutionFailed() on an executeUserOp frame is replaced by the revert its Safe call caught (gated on the exact selector and exact payload). This RPC calls the smart account directly, so EntryPoint wrappers like FailedOp never appear and get no special case.

Every frame still carries its own local revertReason in the trace; this only changes which one the response surfaces.

How

Selection is a pure recursive function (terminal_revert_payload) over the completed trace — the inspector records raw revert payloads and does no candidate bookkeeping during execution.

take_trace_entries became the borrowing trace_entries, so reason selection and trace extraction are order-independent (no drain-ordering footgun).

Testing

  • Unit tests drive the selection function with hand-built trace fixtures (inheritance, superseding siblings, halts, Safe recovery, selector and exact-payload gating).
  • In-memory integration tests cover the record→select pipeline end to end, including the Safe scenario with real calldata.
  • Both Safe selectors verified against cast sig.

Note

Medium Risk
Changes only how simulation RPC explains failures (revert selection semantics), but wrong heuristics could mislead wallets on Safe/UserOp errors; scope is localized to the simulate inspector and response assembly.

Overview
Simulation responses now pick revertReason from a post-execution walk of the completed call trace instead of recording the first revert payload seen in call_end / create_end hooks.

The inspector stores raw revert_output per frame and exposes terminal_revert_reason() via terminal_revert_payload / frame_revert_payload: a reverted frame’s own payload wins; empty reverts inherit from the last direct child only if that child also failed (later successful siblings discard earlier caught reverts). Safe4337Module bare ExecutionFailed() on executeUserOp is replaced by the revert hidden under a successful “catching” subtree when selector and payload match exactly (EXECUTE_USER_OP_SELECTOR, EXECUTION_FAILED_SELECTOR in simulate_consts.rs). Handler order is halt reason → terminal trace selection → outer execution revert decode.

take_trace_entriestrace_entries (borrow, no drain) so trace export and terminal reason selection are order-independent. Per-frame trace revertReason is still decoded from each frame’s payload at read time.

Tests cover hand-built trace trees, CREATE empty-revert inheritance, caught-child vs terminal parent, and Safe-style bytecode integration.

Reviewed by Cursor Bugbot for commit 3dad3b2. Bugbot is set up for automated code reviews on this repo. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant