Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `userFeeLevel` for such transactions is set to `medium` instead of `dappSuggested`, so the values are kept updated while the transaction is unapproved.
- The original dapp-suggested values remain available via `dappSuggestedGasFees` on the transaction metadata.
- Disabled by default.
- Ignore underpriced saved (advanced) gas fee preferences in favour of the wallet's suggested estimates ([#9705](https://github.com/MetaMask/core/pull/9705))
- If the new `replaceUnderpricedSavedGasFees` feature flag is enabled for the chain, saved custom fees with a `maxBaseFee` below the current low estimate are ignored and the suggested medium values are used instead, as they are unlikely to result in inclusion in a block before fee values change.
- Level-based saved preferences track current estimates and are never ignored.
- The `userFeeLevel` for such transactions is set to `medium` instead of `custom`, so the values are kept updated while the transaction is unapproved.
- Disabled by default.

## [69.3.0]

Expand Down
45 changes: 45 additions & 0 deletions packages/transaction-controller/src/utils/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,27 @@ export type TransactionControllerFeatureFlags = {
*/
default?: boolean;
};

/**
* Replacement of underpriced saved (advanced) gas fee preferences.
* If enabled, saved custom fees with a `maxBaseFee` below the current low
* estimate are ignored in favour of the wallet's suggested fees, as they
* are unlikely to be included in a block before fee values change.
* Level-based saved preferences track current estimates and are never
* ignored.
*/
replaceUnderpricedSavedGasFees?: {
/** Enablement on a per-chain basis. */
perChainConfig?: {
[chainId: Hex]: boolean;
};

/**
* Default enablement.
* This value is used when no specific value is found for a chain ID.
*/
default?: boolean;
};
};
};

Expand Down Expand Up @@ -508,6 +529,30 @@ export function getReplaceUnderpricedDappGasFeesEnabled(
);
}

/**
* Retrieves whether underpriced saved (advanced) gas fee preferences should be
* ignored in favour of the wallet's suggested fees.
*
* @param chainId - The chain ID.
* @param messenger - The controller messenger instance.
* @returns Whether the replacement is enabled.
*/
export function getReplaceUnderpricedSavedGasFeesEnabled(
chainId: Hex,
messenger: TransactionControllerMessenger,
): boolean {
const featureFlags = getFeatureFlags(messenger);

const replaceUnderpricedSavedGasFeesFlags =
featureFlags?.[FeatureFlag.Transactions]?.replaceUnderpricedSavedGasFees;

return (
replaceUnderpricedSavedGasFeesFlags?.perChainConfig?.[chainId] ??
replaceUnderpricedSavedGasFeesFlags?.default ??
false
);
}

/**
* Retrieves the relevant feature flags from the remote feature flag controller.
*
Expand Down
100 changes: 99 additions & 1 deletion packages/transaction-controller/src/utils/gas-fees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
TransactionType,
UserFeeLevel,
} from '../types.js';
import { getReplaceUnderpricedDappGasFeesEnabled } from './feature-flags.js';
import {
getReplaceUnderpricedDappGasFeesEnabled,
getReplaceUnderpricedSavedGasFeesEnabled,
} from './feature-flags.js';
import type { UpdateGasFeesRequest } from './gas-fees.js';
import { gweiDecimalToWeiDecimal, updateGasFees } from './gas-fees.js';
import { rpcRequest } from './provider.js';
Expand Down Expand Up @@ -107,6 +110,9 @@ describe('gas-fees', () => {
const getReplaceUnderpricedDappGasFeesEnabledMock = jest.mocked(
getReplaceUnderpricedDappGasFeesEnabled,
);
const getReplaceUnderpricedSavedGasFeesEnabledMock = jest.mocked(
getReplaceUnderpricedSavedGasFeesEnabled,
);
let gasFeeFlowMock: jest.Mocked<GasFeeFlow>;

/**
Expand Down Expand Up @@ -863,6 +869,98 @@ describe('gas-fees', () => {
);
});
});

describe('replaces underpriced saved gas fees', () => {
beforeEach(() => {
getReplaceUnderpricedSavedGasFeesEnabledMock.mockReturnValue(true);
mockGasFeeFlowMockResponse(FLOW_RESPONSE_FEE_MARKET_MOCK);
});

it('with suggested medium values if saved maxBaseFee below low estimate', async () => {
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
maxBaseFee: String(GAS_LOW_MOCK - 1),
priorityFee: '1',
});

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
GAS_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.txParams.maxPriorityFeePerGas).toBe(
GAS_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
UserFeeLevel.MEDIUM,
);
});

it('unless saved maxBaseFee is at or above low estimate', async () => {
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
maxBaseFee: String(GAS_LOW_MOCK),
priorityFee: '1',
});

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
GAS_LOW_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
UserFeeLevel.CUSTOM,
);
});

it('unless saved preference is level-based', async () => {
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
level: GasFeeEstimateLevel.Low,
});

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
GAS_LOW_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
GasFeeEstimateLevel.Low,
);
});

it('unless feature flag is disabled', async () => {
getReplaceUnderpricedSavedGasFeesEnabledMock.mockReturnValue(false);
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
maxBaseFee: String(GAS_LOW_MOCK - 1),
priorityFee: '1',
});

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
UNDERPRICED_GAS_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
UserFeeLevel.CUSTOM,
);
});

it('unless estimates are unavailable', async () => {
gasFeeFlowMock.getGasFees.mockRejectedValue(new Error('TestError'));
rpcRequestMock.mockResolvedValue(GAS_HEX_WEI_MOCK);
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
maxBaseFee: String(GAS_LOW_MOCK - 1),
priorityFee: '1',
});

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
UNDERPRICED_GAS_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
UserFeeLevel.CUSTOM,
);
});
});
});
});

Expand Down
65 changes: 63 additions & 2 deletions packages/transaction-controller/src/utils/gas-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import {
TransactionType,
UserFeeLevel,
} from '../types.js';
import { getReplaceUnderpricedDappGasFeesEnabled } from './feature-flags.js';
import {
getReplaceUnderpricedDappGasFeesEnabled,
getReplaceUnderpricedSavedGasFeesEnabled,
} from './feature-flags.js';
import { getGasFeeFlow } from './gas-flow.js';
import { rpcRequest } from './provider.js';
import { SWAP_TRANSACTION_TYPES } from './swaps.js';
Expand Down Expand Up @@ -85,7 +88,7 @@ export async function updateGasFees(
txMeta.type as TransactionType,
);

const savedGasFees =
let savedGasFees =
shouldIgnoreSavedGasFees || hasInitialGasFeeParams(initialParams)
? undefined
: request.getSavedGasFees(txMeta);
Expand All @@ -97,6 +100,17 @@ export async function updateGasFees(

log('Suggested gas fees', suggestedGasFees);

if (
shouldIgnoreUnderpricedSavedGasFees(savedGasFees, suggestedGasFees, request)
) {
log(
'Ignoring saved custom gas fees below low estimate',
savedGasFees?.maxBaseFee,
suggestedGasFees.low?.maxFeePerGas,
);
savedGasFees = undefined;
}

const getGasFeeRequest: GetGasFeeRequest = {
...request,
initialParams,
Expand Down Expand Up @@ -157,6 +171,53 @@ export function gweiDecimalToWeiDecimal(gweiDecimal: string | number): string {
return weiValue.toString().split('.')[0];
}

/**
* Determine whether saved (advanced) gas fee preferences should be ignored in
* favour of the suggested gas fees, as the saved custom `maxBaseFee` is below
* the current low estimate and hence unlikely to result in timely inclusion in
* a block. Level-based saved preferences track current estimates and are never
* ignored.
*
* @param savedGasFees - The saved gas fee preferences.
* @param suggestedGasFees - The suggested gas fees.
* @param request - The request object.
* @returns Whether the saved gas fee preferences should be ignored.
*/
function shouldIgnoreUnderpricedSavedGasFees(
savedGasFees: SavedGasFees | undefined,
suggestedGasFees: SuggestedGasFees,
request: UpdateGasFeesRequest,
): boolean {
const { eip1559, messenger, txMeta } = request;

if (
!eip1559 ||
!savedGasFees?.maxBaseFee ||
!getReplaceUnderpricedSavedGasFeesEnabled(txMeta.chainId, messenger)
) {
return false;
}

const lowMaxFeePerGas = suggestedGasFees.low?.maxFeePerGas;

const hasReplacement =
Boolean(suggestedGasFees.maxFeePerGas) &&
Boolean(suggestedGasFees.maxPriorityFeePerGas);

if (!lowMaxFeePerGas || !hasReplacement) {
return false;
}

try {
return (
BigInt(gweiDecimalToWeiHex(savedGasFees.maxBaseFee)) <
BigInt(lowMaxFeePerGas)
);
} catch {
return false;
}
}

/**
* Determine whether dapp-suggested gas fees should be ignored in favour of the
* suggested gas fees, as the dapp-suggested `maxFeePerGas` is below the current
Expand Down
Loading