Skip to content

Commit 9b2a8b1

Browse files
authored
fix: ignore saved advanced gas fees for bridge transactions (#9401)
## Explanation Bridge transactions apply the user's **saved advanced gas fees**, whereas swaps deliberately ignore them. In `updateGasFees`, saved gas fees are skipped only for `SWAP_TRANSACTION_TYPES` (`swap`, `swapAndSend`, `swapApproval`). `bridge` / `bridgeApproval` are not in that list, so a user's saved advanced gas fees are applied to bridge transactions. For a user who has saved a low max base fee (observed in production: `maxBaseFee: "0.05"` gwei, `priorityFee: "0"` for mainnet), every bridge is submitted **underpriced** — below the current base fee — causing it to fail (`GAS_TOO_LOW` on the relay) or get stuck as pending. This is exactly the failure swaps were already protected from. ### Fix Ignore saved gas fees for bridge transactions as well, via a dedicated list: ```ts const SAVED_GAS_FEES_IGNORED_TRANSACTION_TYPES: TransactionType[] = [ ...SWAP_TRANSACTION_TYPES, TransactionType.bridge, TransactionType.bridgeApproval, ]; ``` I intentionally did **not** add the bridge types to `SWAP_TRANSACTION_TYPES` itself, because that constant also gates swap-specific behavior in `updateSwapsTransaction` (e.g. the `simulationFails` cancel-and-throw, and `transactionNewSwap*` events). Keeping a separate, purpose-named list decouples "ignore saved gas fees" from "is a swap". ## References - Root-caused from production state logs where a bridge smart transaction was submitted with `maxFeePerGas: 0x2faf080` (0.05 gwei) / `maxPriorityFeePerGas: 0x0`, sourced from `advancedGasFee["0x1"] = { maxBaseFee: "0.05", priorityFee: "0" }`. ## Changelog ### `@metamask/transaction-controller` - **FIXED**: `bridge` and `bridgeApproval` transactions now ignore user-saved (advanced) gas fees, matching swaps. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes gas pricing for all internal transactions (bridges, swaps, etc.); behavior is intentional but affects submission paths where wrong fees caused production failures. > > **Overview** > **`updateGasFees`** no longer applies user-saved (advanced) gas fees when **`txMeta.isInternal`** is true. Saved fees are only loaded for non-internal (dApp) transactions. > > The previous logic skipped saved fees only for **`SWAP_TRANSACTION_TYPES`**, so bridge and other internal flows could inherit a low saved max base fee and submit underpriced txs. The swap-type check and **`SWAP_TRANSACTION_TYPES`** import were removed in favor of **`isInternal`**. > > Tests cover applying saved fees for non-internal txs and ignoring them (without calling **`getSavedGasFees`**) for internal txs. The changelog documents the fix. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit eb622eb. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 545f66c commit 9b2a8b1

3 files changed

Lines changed: 49 additions & 6 deletions

File tree

packages/transaction-controller/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Bump `@metamask/messenger` from `^1.2.0` to `^2.0.0` ([#9392](https://github.com/MetaMask/core/pull/9392))
1313

14+
### Fixed
15+
16+
- Only apply user-saved (advanced) gas fees to dApp transactions ([#9401](https://github.com/MetaMask/core/pull/9401))
17+
- 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.
18+
1419
## [68.2.2]
1520

1621
### Changed

packages/transaction-controller/src/utils/gas-fees.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,45 @@ describe('gas-fees', () => {
130130
);
131131
});
132132

133+
describe('saved (advanced) gas fees', () => {
134+
const SAVED_GAS_FEES_MOCK = { maxBaseFee: '123', priorityFee: '456' };
135+
136+
it('are applied for dApp (non-internal) transactions', async () => {
137+
updateGasFeeRequest.txMeta.isInternal = false;
138+
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce(
139+
SAVED_GAS_FEES_MOCK,
140+
);
141+
142+
await updateGasFees(updateGasFeeRequest);
143+
144+
expect(updateGasFeeRequest.getSavedGasFees).toHaveBeenCalledTimes(1);
145+
expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
146+
'0x1ca35f0e00', // 123 gwei
147+
);
148+
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
149+
UserFeeLevel.CUSTOM,
150+
);
151+
});
152+
153+
it('are ignored for internal transactions (e.g. swaps and bridges)', async () => {
154+
mockGasFeeFlowMockResponse(FLOW_RESPONSE_FEE_MARKET_MOCK);
155+
updateGasFeeRequest.txMeta.isInternal = true;
156+
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce(
157+
SAVED_GAS_FEES_MOCK,
158+
);
159+
160+
await updateGasFees(updateGasFeeRequest);
161+
162+
expect(updateGasFeeRequest.getSavedGasFees).not.toHaveBeenCalled();
163+
expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
164+
GAS_HEX_WEI_MOCK,
165+
);
166+
expect(updateGasFeeRequest.txMeta.userFeeLevel).not.toBe(
167+
UserFeeLevel.CUSTOM,
168+
);
169+
});
170+
});
171+
133172
it('deletes gasPrice property if maxPriorityFeePerGas set', async () => {
134173
updateGasFeeRequest.txMeta.txParams.maxPriorityFeePerGas = GAS_HEX_MOCK;
135174
updateGasFeeRequest.txMeta.txParams.gasPrice = GAS_HEX_MOCK;

packages/transaction-controller/src/utils/gas-fees.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ import type {
1212
SavedGasFees,
1313
TransactionParams,
1414
TransactionMeta,
15-
TransactionType,
1615
GasFeeFlow,
1716
} from '../types';
1817
import { GasFeeEstimateType, UserFeeLevel } from '../types';
1918
import { getGasFeeFlow } from './gas-flow';
2019
import { rpcRequest } from './provider';
21-
import { SWAP_TRANSACTION_TYPES } from './swaps';
2220

2321
export type UpdateGasFeesRequest = {
2422
eip1559: boolean;
@@ -56,10 +54,11 @@ export async function updateGasFees(
5654
const { txMeta } = request;
5755
const initialParams = { ...txMeta.txParams };
5856

59-
const isSwap = SWAP_TRANSACTION_TYPES.includes(
60-
txMeta.type as TransactionType,
61-
);
62-
const savedGasFees = isSwap
57+
// User-saved (advanced) gas fees only apply to dApp transactions. Internal
58+
// transactions (e.g. swaps and bridges) have their fees dictated by the
59+
// aggregator or relay, so applying saved gas fees could underprice them and
60+
// cause them to fail or get stuck.
61+
const savedGasFees = txMeta.isInternal
6362
? undefined
6463
: request.getSavedGasFees(txMeta.chainId);
6564

0 commit comments

Comments
 (0)