From a3382a2cc3e54b0201e1c7530fec01272ef13adb Mon Sep 17 00:00:00 2001 From: Sam Walker Date: Tue, 7 Jul 2026 00:07:25 -0400 Subject: [PATCH 1/3] fix: ignore saved advanced gas fees for bridge transactions Bridge and bridge approval transactions applied the user's saved advanced gas fees, which swaps already deliberately ignore. A user with a low saved max base fee (e.g. 0.05 gwei on mainnet) would therefore submit underpriced bridge transactions that fail or get stuck as pending. Introduce a dedicated SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES list (swap + bridge types) to decide whether saved gas fees apply, keeping the "ignore saved gas fees" concern decoupled from SWAP_TRANSACTION_TYPES, which also gates swap-specific behavior. --- packages/transaction-controller/CHANGELOG.md | 5 +++ .../src/utils/gas-fees.test.ts | 45 +++++++++++++++++++ .../src/utils/gas-fees.ts | 19 ++++++-- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 17809b45e58..44e577f1241 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/messenger` from `^1.2.0` to `^2.0.0` ([#9392](https://github.com/MetaMask/core/pull/9392)) +### Fixed + +- Ignore user-saved (advanced) gas fees for bridge transactions ([#0000](https://github.com/MetaMask/core/pull/0000)) + - `bridge` and `bridgeApproval` transactions now ignore saved advanced gas fees, matching the existing behavior for swaps. Previously a user's saved gas fees (e.g. a low max base fee) could underprice a bridge transaction, causing it to fail or get stuck as pending. + ## [68.2.2] ### Changed diff --git a/packages/transaction-controller/src/utils/gas-fees.test.ts b/packages/transaction-controller/src/utils/gas-fees.test.ts index b72854dc086..14af0deb7b8 100644 --- a/packages/transaction-controller/src/utils/gas-fees.test.ts +++ b/packages/transaction-controller/src/utils/gas-fees.test.ts @@ -130,6 +130,51 @@ describe('gas-fees', () => { ); }); + describe('saved (advanced) gas fees', () => { + const SAVED_GAS_FEES_MOCK = { maxBaseFee: '123', priorityFee: '456' }; + + it('are applied for non-swap, non-bridge transactions', async () => { + updateGasFeeRequest.txMeta.type = TransactionType.simpleSend; + updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce( + SAVED_GAS_FEES_MOCK, + ); + + await updateGasFees(updateGasFeeRequest); + + expect(updateGasFeeRequest.getSavedGasFees).toHaveBeenCalledTimes(1); + expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe( + '0x1ca35f0e00', // 123 gwei + ); + expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe( + UserFeeLevel.CUSTOM, + ); + }); + + it.each([ + TransactionType.swap, + TransactionType.swapAndSend, + TransactionType.swapApproval, + TransactionType.bridge, + TransactionType.bridgeApproval, + ])('are ignored for %s transactions', async (type) => { + mockGasFeeFlowMockResponse(FLOW_RESPONSE_FEE_MARKET_MOCK); + updateGasFeeRequest.txMeta.type = type; + updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce( + SAVED_GAS_FEES_MOCK, + ); + + await updateGasFees(updateGasFeeRequest); + + expect(updateGasFeeRequest.getSavedGasFees).not.toHaveBeenCalled(); + expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe( + GAS_HEX_WEI_MOCK, + ); + expect(updateGasFeeRequest.txMeta.userFeeLevel).not.toBe( + UserFeeLevel.CUSTOM, + ); + }); + }); + it('deletes gasPrice property if maxPriorityFeePerGas set', async () => { updateGasFeeRequest.txMeta.txParams.maxPriorityFeePerGas = GAS_HEX_MOCK; updateGasFeeRequest.txMeta.txParams.gasPrice = GAS_HEX_MOCK; diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 3598fac8099..ad01175bd2b 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -12,10 +12,9 @@ import type { SavedGasFees, TransactionParams, TransactionMeta, - TransactionType, GasFeeFlow, } from '../types'; -import { GasFeeEstimateType, UserFeeLevel } from '../types'; +import { GasFeeEstimateType, TransactionType, UserFeeLevel } from '../types'; import { getGasFeeFlow } from './gas-flow'; import { rpcRequest } from './provider'; import { SWAP_TRANSACTION_TYPES } from './swaps'; @@ -45,6 +44,18 @@ type SuggestedGasFees = { const log = createModuleLogger(projectLogger, 'gas-fees'); +/** + * Transaction types whose gas fees are managed by the Swaps or Bridge feature. + * Any user-saved (advanced) gas fees are ignored for these types, since the + * fees are dictated by the swap/bridge aggregator or relay; applying saved gas + * fees could underprice the transaction and cause it to fail or get stuck. + */ +const SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES: TransactionType[] = [ + ...SWAP_TRANSACTION_TYPES, + TransactionType.bridge, + TransactionType.bridgeApproval, +]; + /** * Update the gas fee properties of the provided transaction meta. * @@ -56,10 +67,10 @@ export async function updateGasFees( const { txMeta } = request; const initialParams = { ...txMeta.txParams }; - const isSwap = SWAP_TRANSACTION_TYPES.includes( + const ignoreSavedGasFees = SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES.includes( txMeta.type as TransactionType, ); - const savedGasFees = isSwap + const savedGasFees = ignoreSavedGasFees ? undefined : request.getSavedGasFees(txMeta.chainId); From 21a2f82f3df750a4d2be76e1d279441f530a2080 Mon Sep 17 00:00:00 2001 From: Sam Walker Date: Tue, 7 Jul 2026 00:07:57 -0400 Subject: [PATCH 2/3] docs: reference PR number in changelog entry --- packages/transaction-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 44e577f1241..d9c1fa371f0 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Ignore user-saved (advanced) gas fees for bridge transactions ([#0000](https://github.com/MetaMask/core/pull/0000)) +- Ignore user-saved (advanced) gas fees for bridge transactions ([#9401](https://github.com/MetaMask/core/pull/9401)) - `bridge` and `bridgeApproval` transactions now ignore saved advanced gas fees, matching the existing behavior for swaps. Previously a user's saved gas fees (e.g. a low max base fee) could underprice a bridge transaction, causing it to fail or get stuck as pending. ## [68.2.2] From eb622ebb6dff9c2b39e0da3354fe5195e42386c7 Mon Sep 17 00:00:00 2001 From: Sam Walker Date: Tue, 7 Jul 2026 12:32:27 -0400 Subject: [PATCH 3/3] refactor: exclude saved gas fees by isInternal instead of tx type Per review feedback, skip user-saved (advanced) gas fees for all internal transactions rather than maintaining a list of swap/bridge types. Saved gas fees are only intended for dApp transactions; internal transactions (swaps, bridges, etc.) have their fees dictated by the aggregator or relay. --- packages/transaction-controller/CHANGELOG.md | 4 ++-- .../src/utils/gas-fees.test.ts | 14 ++++------- .../src/utils/gas-fees.ts | 24 +++++-------------- 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index d9c1fa371f0..dba4816ccdb 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Ignore user-saved (advanced) gas fees for bridge transactions ([#9401](https://github.com/MetaMask/core/pull/9401)) - - `bridge` and `bridgeApproval` transactions now ignore saved advanced gas fees, matching the existing behavior for swaps. Previously a user's saved gas fees (e.g. a low max base fee) could underprice a bridge transaction, causing it to fail or get stuck as pending. +- Only apply user-saved (advanced) gas fees to dApp transactions ([#9401](https://github.com/MetaMask/core/pull/9401)) + - Saved advanced gas fees are now ignored for internal transactions (`isInternal`), such as swaps and bridges, whose gas fees are dictated by the aggregator or relay. Previously they were only ignored for swaps, so a user's saved gas fees (e.g. a low max base fee) could underprice a bridge transaction and cause it to fail or get stuck as pending. ## [68.2.2] diff --git a/packages/transaction-controller/src/utils/gas-fees.test.ts b/packages/transaction-controller/src/utils/gas-fees.test.ts index 14af0deb7b8..e90dc0a1f66 100644 --- a/packages/transaction-controller/src/utils/gas-fees.test.ts +++ b/packages/transaction-controller/src/utils/gas-fees.test.ts @@ -133,8 +133,8 @@ describe('gas-fees', () => { describe('saved (advanced) gas fees', () => { const SAVED_GAS_FEES_MOCK = { maxBaseFee: '123', priorityFee: '456' }; - it('are applied for non-swap, non-bridge transactions', async () => { - updateGasFeeRequest.txMeta.type = TransactionType.simpleSend; + it('are applied for dApp (non-internal) transactions', async () => { + updateGasFeeRequest.txMeta.isInternal = false; updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce( SAVED_GAS_FEES_MOCK, ); @@ -150,15 +150,9 @@ describe('gas-fees', () => { ); }); - it.each([ - TransactionType.swap, - TransactionType.swapAndSend, - TransactionType.swapApproval, - TransactionType.bridge, - TransactionType.bridgeApproval, - ])('are ignored for %s transactions', async (type) => { + it('are ignored for internal transactions (e.g. swaps and bridges)', async () => { mockGasFeeFlowMockResponse(FLOW_RESPONSE_FEE_MARKET_MOCK); - updateGasFeeRequest.txMeta.type = type; + updateGasFeeRequest.txMeta.isInternal = true; updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce( SAVED_GAS_FEES_MOCK, ); diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index ad01175bd2b..7c3755f7035 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -14,10 +14,9 @@ import type { TransactionMeta, GasFeeFlow, } from '../types'; -import { GasFeeEstimateType, TransactionType, UserFeeLevel } from '../types'; +import { GasFeeEstimateType, UserFeeLevel } from '../types'; import { getGasFeeFlow } from './gas-flow'; import { rpcRequest } from './provider'; -import { SWAP_TRANSACTION_TYPES } from './swaps'; export type UpdateGasFeesRequest = { eip1559: boolean; @@ -44,18 +43,6 @@ type SuggestedGasFees = { const log = createModuleLogger(projectLogger, 'gas-fees'); -/** - * Transaction types whose gas fees are managed by the Swaps or Bridge feature. - * Any user-saved (advanced) gas fees are ignored for these types, since the - * fees are dictated by the swap/bridge aggregator or relay; applying saved gas - * fees could underprice the transaction and cause it to fail or get stuck. - */ -const SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES: TransactionType[] = [ - ...SWAP_TRANSACTION_TYPES, - TransactionType.bridge, - TransactionType.bridgeApproval, -]; - /** * Update the gas fee properties of the provided transaction meta. * @@ -67,10 +54,11 @@ export async function updateGasFees( const { txMeta } = request; const initialParams = { ...txMeta.txParams }; - const ignoreSavedGasFees = SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES.includes( - txMeta.type as TransactionType, - ); - const savedGasFees = ignoreSavedGasFees + // User-saved (advanced) gas fees only apply to dApp transactions. Internal + // transactions (e.g. swaps and bridges) have their fees dictated by the + // aggregator or relay, so applying saved gas fees could underprice them and + // cause them to fail or get stuck. + const savedGasFees = txMeta.isInternal ? undefined : request.getSavedGasFees(txMeta.chainId);