Skip to content

feat(transaction-controller): extract revert reason for failed transactions#8589

Draft
matthewwalsh0 wants to merge 1 commit intomainfrom
feat/extract-revert-reason
Draft

feat(transaction-controller): extract revert reason for failed transactions#8589
matthewwalsh0 wants to merge 1 commit intomainfrom
feat/extract-revert-reason

Conversation

@matthewwalsh0
Copy link
Copy Markdown
Member

@matthewwalsh0 matthewwalsh0 commented Apr 27, 2026

Explanation

When a transaction fails — either on-chain after broadcast, or up-front during gas estimation — MetaMask currently records a generic "Transaction failed on-chain" with no information about why it failed. The actual revert reason — `ERC20: transfer amount exceeds balance`, `Panic: arithmetic overflow`, etc. — is recoverable from any standard JSON-RPC endpoint.

This PR extracts the human-readable revert reason at both points and surfaces it on `TransactionMeta`:

  • `revertReason?: string` — set by `PendingTransactionTracker` when a receipt comes back with `status: 0x0`. Replays the call via `eth_estimateGas` and decodes the returned revert payload.
  • `gasRevertReason?: string` — set during transaction creation when the controller's `eth_estimateGas` call reverts (i.e. the tx is predicted to fail). Allows confirmation UI to warn the user before they confirm.

The on-chain failure path also appends the decoded reason to the existing error message: `"Transaction failed on-chain: ERC20: transfer amount exceeds balance"`.

Decoding

The shared `decodeRevertReasonFromError` util handles:

  • `Error(string)` (selector `0x08c379a0`) → the embedded message
  • `Panic(uint256)` (selector `0x4e487b71`) → e.g. `Panic: arithmetic overflow or underflow (0x11)`
  • Custom error selectors → `reverted with custom error 0xdeadbeef` (raw selector fallback; we don't have ABIs for arbitrary contracts)
  • Empty reverts → falls back to the provider error message (`execution reverted: ...` style)

Always wrapped in try/catch — never throws, safe to run on every failure path.

Why `eth_estimateGas` (not `eth_call`)

The on-chain replay path uses `eth_estimateGas` rather than `eth_call` to work around a bug in `@metamask/eth-json-rpc-middleware`'s `RetryOnEmptyMiddleware` (fixed separately in #8597). That middleware's `isExecutionRevertedError` check rejects EIP-1474 / Infura-style revert errors (`code: 3`, suffixed message), causing reverted `eth_call`s to be retried 10 times and the original error data to be discarded. `eth_estimateGas` returns the same revert payload but isn't routed through that middleware.

When #8597 lands and is consumed, we could revisit and switch back to `eth_call` if there's value in the block-tag precision. For now `eth_estimateGas` produces the same reasons in practice.

References

Changelog

```

Added

  • Extract a human-readable revert reason for transactions that fail on-chain
    • When a transaction's receipt has a failure status (`0x0`), the controller now replays the call via `eth_estimateGas` and decodes the returned revert data
    • Supports `Error(string)`, `Panic(uint256)` (with named codes), custom error selectors, and empty reverts; falls back to the provider error message when no revert data is surfaced
    • Adds `revertReason?: string` field to `TransactionMeta` for consumers that prefer not to parse error strings
    • Appends the decoded reason to the existing `Transaction failed on-chain` error message
  • Capture revert reason from gas estimation when a transaction is predicted to fail
    • When `eth_estimateGas` reverts during transaction creation, the controller now decodes the same revert data and stores it on `TransactionMeta.gasRevertReason`
    • Allows confirmation UI to surface why a transaction is predicted to fail, before the user submits it
    • Set independently from `revertReason` (which is only populated by on-chain failure detection)
      ```

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've highlighted breaking changes using the `BREAKING` category above as appropriate — no breaking changes; both fields are purely additive
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes — no breaking changes

…ctions

When a transaction fails on-chain (status 0x0) or is predicted to fail
during gas estimation, decode and surface the revert reason instead of
just "Transaction failed on-chain".

- Add extractRevertReason util that decodes Error(string), Panic(uint256)
  with named codes, custom error selectors, and empty reverts; falls back
  to the provider error message when no revert data is surfaced
- Add revertReason?: string on TransactionMeta, populated when an
  on-chain receipt has a failure status (replays via eth_estimateGas)
- Add gasRevertReason?: string on TransactionMeta, populated when
  eth_estimateGas reverts during transaction creation, allowing UI to
  warn the user before they confirm a tx that's predicted to fail
- Append the on-chain reason to the existing 'Transaction failed
  on-chain' error message via a new OnChainFailureError subclass

eth_estimateGas is used (instead of eth_call) for the on-chain replay
path as a workaround for a bug in eth-json-rpc-middleware's
RetryOnEmptyMiddleware: its isExecutionRevertedError check fails to
recognise EIP-1474 / Infura-style revert errors (code 3, suffixed
message), causing it to retry reverted calls 10 times and discard the
original error data. eth_estimateGas returns the same revert payload
but bypasses that middleware. A separate upstream PR fixes the
underlying bug.
@matthewwalsh0 matthewwalsh0 force-pushed the feat/extract-revert-reason branch from 988450c to fcfa427 Compare April 29, 2026 15:43
@matthewwalsh0 matthewwalsh0 changed the title feat(transaction-controller): extract revert reason for on-chain failures feat(transaction-controller): extract revert reason for failed transactions Apr 29, 2026
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