Skip to content

fix: query layer 1 gas fee oracles via direct eth_call instead of ethers Web3Provider#9505

Open
cryptotavares wants to merge 5 commits into
mainfrom
fix/oracle-l1-gas-fee-timer-free-dispatch
Open

fix: query layer 1 gas fee oracles via direct eth_call instead of ethers Web3Provider#9505
cryptotavares wants to merge 5 commits into
mainfrom
fix/oracle-l1-gas-fee-timer-free-dispatch

Conversation

@cryptotavares

@cryptotavares cryptotavares commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Explanation

Current state: OracleLayer1GasFeeFlow and MantleLayer1GasFeeFlow call OP-stack gas price oracle contracts using an ethers Contract backed by Web3Provider. Every read through this path (contract.getL1FeeBaseProvider.call) first awaits getNetwork()detectNetwork(), which internally depends on zero-delay setTimeout calls: the constructor's network-detection setTimeout(…, 0), an await timer(0) inside _uncachedDetectNetwork, and the detect-network cache-clear setTimeout(…, 0).

Why it needs to change: On React Native iOS, the JS timer pump is driven by a single CADisplayLink registered in RCTDisplayLink. When the display link stops receiving ticks from the render server (backboardd), all setTimeout callbacks silently stop firing — while the app remains visually foreground-active. This happens reproducibly when a WKWebView (in-app browser) becomes the active surface. Bridgeless RN has no zero-delay fast path, so even setTimeout(…, 0) waits for a display frame that never comes. As a result, ethers' getNetwork()/detectNetwork() timers never fire, the oracle eth_call is never dispatched, updateTransactionLayer1GasFee blocks indefinitely inside addTransaction, no approval is ever created, and the dapp confirmation modal never appears. This was reported as a Sev1 in MetaMask Mobile (MetaMask/metamask-mobile#32863) and reproduced on both iOS Simulator and physical iPhone.

The fix: Replace the Contract/Web3Provider dispatch path with:

  1. Interface.encodeFunctionData (from @ethersproject/abi, already an indirect dependency) to ABI-encode the calldata for getL1Fee, getOperatorFee, and tokenRatio.
  2. The controller's existing rpcRequest utility, which calls provider.request directly — a plain EIP-1193 call with no timer dependency.

Layer1GasFeeFlowRequest gains a messenger field so the flows can reach rpcRequest. The fix covers both the base oracle flow (OracleLayer1GasFeeFlow: getL1Fee + getOperatorFee) and MantleLayer1GasFeeFlow (tokenRatio in transformOracleFee). The fix was validated end-to-end on a live mobile repro: after the patch, the confirmation modal appears correctly while the RN timer pump is frozen.

No new npm dependencies are introduced — @ethersproject/abi was already transitively present; @ethersproject/contracts and @ethersproject/providers are no longer imported.

References

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 communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Touches the addTransaction L1 fee path and introduces a breaking shape change on Layer1GasFeeFlowRequest for any custom flows, though behavior is covered by updated tests and targets a known mobile deadlock.

Overview
Layer 1 gas fee flows no longer use ethers Contract + Web3Provider to read OP-stack and Mantle oracles. They ABI-encode getL1Fee, getOperatorFee, and tokenRatio with @ethersproject/abi and call the existing rpcRequest helper (eth_call on provider.request), avoiding ethers’ setTimeout-based network detection that can hang indefinitely on React Native iOS and block addTransaction / dapp confirmations.

Layer1GasFeeFlowRequest now carries messenger instead of provider; updateTransactionLayer1GasFee passes the messenger through and no longer resolves a provider up front. Oracle responses treat empty 0x / non-string results as errors for L1 fee and Mantle tokenRatio (operator fee still defaults to zero on failure).

@ethersproject/providers is dropped from dependencies; tests are updated to mock rpcRequest and assert eth_call payloads.

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

…ers Web3Provider

Previously OracleLayer1GasFeeFlow and MantleLayer1GasFeeFlow called OP-stack
oracle contracts via an ethers Contract backed by Web3Provider.
Web3Provider batches its JSON-RPC dispatch using setTimeout, which never
fires on React Native iOS when the timer pump is starved due to a display-link
freeze. This blocks addTransaction indefinitely, preventing the dapp
confirmation modal from ever appearing (MetaMask/metamask-mobile#32863, Sev1).

Replace the Contract/Web3Provider path with Interface.encodeFunctionData
(from @ethersproject/abi) to encode calldata, then dispatch via the
controller's existing rpcRequest utility (a plain provider.request eth_call).
This path is timer-free and works correctly even while the RN timer pump is
frozen.

Layer1GasFeeFlowRequest gains a messenger field so flows can call rpcRequest
directly. The fix is applied to both the base oracle flow (getL1Fee,
getOperatorFee) and MantleLayer1GasFeeFlow (tokenRatio). Validated end-to-end
on a live MetaMask Mobile repro: the confirmation modal now surfaces while the
RN timer pump is frozen.
@cryptotavares cryptotavares requested review from a team as code owners July 14, 2026 16:14
@matthewwalsh0 matthewwalsh0 self-requested a review July 15, 2026 08:19

### Changed

- Add `messenger` property to `Layer1GasFeeFlowRequest` ([#9505](https://github.com/MetaMask/core/pull/9505))

@matthewwalsh0 matthewwalsh0 Jul 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, is this unnecessary in changelog since Layer1GasFeeFlowRequest isn't exposed by the package?

messenger: TransactionControllerMessenger;

/** RPC Provider instance. */
provider: Provider;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this at the same time since it's no longer used and encourages use of rpcRequest instead?

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.

2 participants