Skip to content

fix(transaction-controller): scope saved gas exclusions to swaps and bridges - #9682

Merged
pedronfigueiredo merged 4 commits into
mainfrom
pnf/saved-gas-fees-internal
Jul 29, 2026
Merged

fix(transaction-controller): scope saved gas exclusions to swaps and bridges#9682
pedronfigueiredo merged 4 commits into
mainfrom
pnf/saved-gas-fees-internal

Conversation

@pedronfigueiredo

@pedronfigueiredo pedronfigueiredo commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Saved gas preferences were excluded from every transaction with isInternal: true. This also excluded wallet-initiated transfers, which are marked internal by the extension API even though they should use saved gas settings.

This changes the exclusion to a dedicated transaction-type list containing swaps and bridge transactions. Wallet transfers can now apply saved gas preferences while swaps and bridges remain protected from underpriced saved fees.

Related PRs

Testing

  • yarn jest --config packages/transaction-controller/jest.config.js --runInBand packages/transaction-controller/src/utils/gas-fees.test.ts --coverage=false
  • Added coverage for internal wallet transactions and swap/bridge exclusions.

Changelog

Added an Unreleased changelog entry for @metamask/transaction-controller.


Note

Medium Risk
Changes which transactions receive saved gas fees at submit time; wallet sends gain user prefs while swap/bridge behavior stays guarded, but any misclassified type could get the wrong fee path.

Overview
Saved gas preferences were skipped for every transaction with isInternal: true, which incorrectly blocked wallet-initiated transfers (often marked internal) from using the user’s advanced gas settings.

updateGasFees now ignores saved gas only for swap and bridge transaction types (SWAP_TRANSACTION_TYPES, bridge, bridgeApproval) instead of all internal transactions. Internal simpleSend transfers can apply saved fees again; aggregator/relay-driven swaps and bridges still avoid user-saved fees that could underprice them.

Tests cover internal wallet sends applying saved gas and parameterized cases for swap/bridge types still ignoring them.

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

@pedronfigueiredo
pedronfigueiredo marked this pull request as ready for review July 28, 2026 16:09
@pedronfigueiredo
pedronfigueiredo requested review from a team as code owners July 28, 2026 16:09
@matthewwalsh0
matthewwalsh0 requested a review from jpuri July 29, 2026 11:01
@pedronfigueiredo
pedronfigueiredo added this pull request to the merge queue Jul 29, 2026
Merged via the queue into main with commit 8d97ab0 Jul 29, 2026
136 checks passed
@pedronfigueiredo
pedronfigueiredo deleted the pnf/saved-gas-fees-internal branch July 29, 2026 12:07
@cursor cursor Bot mentioned this pull request Jul 29, 2026
4 tasks
pull Bot pushed a commit to dmrazzy/core that referenced this pull request Aug 4, 2026
…sted estimates (MetaMask#9704)

## Explanation

Underpriced gas fees — a `maxFeePerGas` below (or barely above) the
current base fee — produce transactions that are very unlikely to be
included before fee conditions change further. They typically end up
stuck as pending until eventually marked as failed (e.g. `Transaction
not found on network after timeout`), with no actionable feedback to the
user. Telemetry shows this is a meaningful contributor to
dropped/timed-out transactions.

Two fee sources can produce such values, and nothing in the client
validates either against current network conditions. This PR adds a
feature-flag-gated staleness gate for both, at transaction creation time
— the last point where fees can still be changed before signing.

### 1. Dapp-suggested fees (`replaceUnderpricedDappGasFees` flag)

Currently:

- `updateGasFees` uses `initialParams.maxFeePerGas` as-is.
- Gas estimation deliberately strips fee fields before calling the RPC
node, so the node's own fee-cap validation never runs.
- `GasFeePoller` only refreshes fees for preset levels, so
`dappSuggested` fees stay frozen while the confirmation is open.

A dapp that computes fees from stale data can therefore produce a signed
transaction that cannot mine. With the flag enabled:

- A new `shouldIgnoreDappGasFees` check fires when the dapp-suggested
`maxFeePerGas` (or a legacy `gasPrice` used as `maxFeePerGas` on
EIP-1559 networks) is below the current **low** estimate. The low
estimate is the floor rather than the raw base fee, since fees between
the base fee and the low estimate have near-zero effective priority fee
and no headroom for base fee movement, so they share the same failure
mode.
- When it fires, the transaction falls through to the existing suggested
(medium) fee logic for both `maxFeePerGas` and `maxPriorityFeePerGas`.
- `userFeeLevel` is set to `medium` instead of `dappSuggested`, which
also opts the transaction into `GasFeePoller` refresh while unapproved.
- The original values remain available via `dappSuggestedGasFees` on the
transaction metadata, so UIs can still display the site suggestion and
users can revert via the edit modal.

Note: for transactions with complete dapp-suggested fees,
`getSuggestedGasFees` previously skipped fetching estimates entirely.
When the flag is enabled, estimates are now fetched for these
transactions so the comparison can run — one additional gas fee estimate
call per dapp transaction on enabled chains.

### 2. Saved (advanced) gas fee preferences
(`replaceUnderpricedSavedGasFees` flag)

A saved *custom* preference is a static `maxBaseFee` captured at some
point in the past; once the base fee rises past it, every transaction
priced from it strands. Nothing else guards this source: the dapp gate
deliberately excludes `savedGasFees`, and `hasInitialGasFeeParams` only
protects transactions carrying explicit fee params — saved fees apply
precisely when they don't. This is increasingly relevant as clients
expand saved-preference usage (per-account custom gas settings persisted
from the confirmation flow, and MetaMask#9682 extending saved fees to
wallet-initiated transfers).

With the flag enabled:

- After suggested fees are fetched, if the saved preference has a custom
`maxBaseFee` below the current low estimate, the saved preference is
ignored for this transaction and the suggested (medium) values are used
instead.
- **Level-based saved preferences (`low`/`medium`/`high`) are never
touched** — they track current estimates by construction.
- `userFeeLevel` becomes `medium` instead of `custom`, with the same
`GasFeePoller` refresh benefit.
- No fetch-path changes needed: transactions eligible for saved fees
carry no fee params, so estimates are already fetched.

### Guards / fail-open behavior (both gates)

Fees are kept unchanged when: the respective feature flag is not enabled
for the chain (both default to disabled; per-chain rollout via
`perChainConfig` with `default` fallback, mirroring `timeoutAttempts`);
the transaction is internal (dapp gate; swaps/bridges are priced by the
aggregator/relay); the fee is at or above the low estimate; or estimates
are unavailable / not fee-market type.

### The product decision (saved-fees gate)

Overriding a value the *user* saved is a stronger intervention than
overriding a dapp's computed value. Three defensible behaviors when the
saved-fees gate fires: (1) silently use suggested fees (this PR's
behavior), (2) use suggested fees + surface an alert ("your saved gas
setting is below current network fees"), (3) keep the saved value + warn
only. The flag ships disabled, so this can be settled before enablement.

An argument for intervening (1 or 2) rather than deferring entirely to
the saved value: **saved preferences already do not grant full
autonomy.** The controller already ignores them for swaps and bridges
(`SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES`, MetaMask#9401/MetaMask#9682) — explicitly
because a saved fee could underprice aggregator-priced transactions —
and already yields them to explicit fee params
(`hasInitialGasFeeParams`, MetaMask#8993). The established principle is that
saved preferences apply *where they can work* and yield *where they
would predictably break the transaction*. A custom `maxBaseFee` below
the current low estimate is the clearest instance of the latter — this
extends existing precedent rather than setting new one.

## References

- Prior art for saved-fee exclusions: MetaMask#9401, MetaMask#9682, MetaMask#8993
- Known edge case (fee bump vs. dapp max-spend arithmetic) and proposed
balance-clamp refinement: see PR comment below — it applies to both
gates.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes pre-sign gas selection for dapp and saved-fee paths when flags
are enabled; behavior is gated and fail-open, but wrong thresholds or
rollout could surprise users or sites.
> 
> **Overview**
> Adds **opt-in, per-chain** gates in `updateGasFees` so transactions
are less likely to be priced below current network conditions before
approval.
> 
> When **`replaceUnderpricedDappGasFees`** is enabled, dapp-suggested
EIP-1559 fees whose effective `maxFeePerGas` (including legacy
`gasPrice` used as max fee) is **below the wallet’s low estimate** are
replaced with **suggested medium** `maxFeePerGas` /
`maxPriorityFeePerGas`. **`userFeeLevel`** becomes **`medium`** instead
of **`dappSuggested`**, so fees can keep updating while unapproved;
originals stay on **`dappSuggestedGasFees`**. Complete dapp fee params
no longer skip estimate fetch when this flag is on (needed for the
comparison; fail-open if estimates are missing).
> 
> When **`replaceUnderpricedSavedGasFees`** is enabled, saved **custom**
`maxBaseFee` below the low estimate is **dropped** for that tx so medium
suggestions apply; **level-based** saved prefs are unchanged.
**`userFeeLevel`** becomes **`medium`** instead of **`custom`**.
> 
> New remote feature-flag helpers and types mirror existing per-chain
`default` / `perChainConfig` patterns; both flags default to
**disabled**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
95e622d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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