Skip to content

Commit 6f9a290

Browse files
Fix Across Predict withdraw gas payment edges (#8762)
## Summary This is PR 4 of 4 in the core stack for Predict withdraws over Across. - Fixes TransactionController gas-fee-token publishing when native token usage is explicitly excluded. - Handles Across gas-station edge cases for post-quote Predict withdraws, including source-token gas pricing fallback when the gas station cannot price the token directly. - Caps prefunded post-quote Predict withdraw gas estimates at the configured Across fallback when batch simulation inflates the estimate. - Keeps transaction-pay-controller coverage at 100% and exercises the transaction-controller gas-fee-token path. ## Stack 1. #8759: plumbing to identify Predict Across withdraws 2. #8760: quote support 3. #8761: submit support 4. This PR: gas payment edge cases ## Validation - `yarn workspace @metamask/transaction-pay-controller run jest --no-coverage src/strategy/across/across-quotes.test.ts src/strategy/across/across-submit.test.ts src/strategy/across/AcrossStrategy.test.ts` - `yarn workspace @metamask/transaction-controller run jest --no-coverage src/utils/gas-fee-tokens.test.ts` - `yarn changelog:validate` - `yarn workspace @metamask/transaction-pay-controller run test` - `yarn workspace @metamask/transaction-controller run test` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes transaction publishing and Across quote fee estimation logic around gas-fee tokens and EIP-7702, which can affect whether transactions are priced and signed correctly in edge cases. > > **Overview** > Fixes gas-fee-token handling when publishing transactions that explicitly set `excludeNativeTokenForFee`, ensuring the pre-publish check skips native-balance logic and forces `isExternalSign`/nonce clearing when a gas fee token is selected. > > Improves Across post-quote Predict-withdraw fee calculation by (1) capping inflated prefunded EIP-7702 batch gas estimates to the configured Across fallback, (2) falling back to fiat-derived source-token gas pricing when the gas station can’t price the source token, and (3) preferring source-token gas pricing for post-quote Predict withdraws (while keeping native-gas pricing when the account can’t use 7702). Tests were expanded to cover these new paths and edge cases. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2d30cef. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent b3dd204 commit 6f9a290

7 files changed

Lines changed: 685 additions & 31 deletions

File tree

packages/transaction-controller/CHANGELOG.md

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

1212
- Bump `@metamask/core-backend` from `^6.2.2` to `^6.3.0` ([#8813](https://github.com/MetaMask/core/pull/8813))
1313

14+
### Fixed
15+
16+
- Respect `excludeNativeTokenForFee` when publishing transactions with a selected gas fee token ([#8762](https://github.com/MetaMask/core/pull/8762))
17+
1418
## [65.4.0]
1519

1620
### Added

packages/transaction-controller/src/utils/gas-fee-tokens.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,36 @@ describe('Gas Fee Tokens Utils', () => {
480480
expect(request.transaction.txParams.nonce).toBeUndefined();
481481
});
482482

483+
it('sets external sign when native token is excluded for fees', async () => {
484+
request.transaction.excludeNativeTokenForFee = true;
485+
request.transaction.isGasFeeTokenIgnoredIfBalance = false;
486+
request.transaction.selectedGasFeeToken = TOKEN_ADDRESS_1_MOCK;
487+
request.transaction.gasFeeTokens = [];
488+
request.transaction.isExternalSign = false;
489+
request.transaction.txParams.nonce = '0x1';
490+
491+
jest.mocked(request.fetchGasFeeTokens).mockResolvedValueOnce([
492+
{
493+
tokenAddress: TOKEN_ADDRESS_1_MOCK,
494+
} as GasFeeToken,
495+
]);
496+
497+
await checkGasFeeTokenBeforePublish(request);
498+
499+
jest
500+
.mocked(request.updateTransaction)
501+
.mock.calls[0][1](request.transaction);
502+
503+
expect(isNativeBalanceSufficientForGasMock).not.toHaveBeenCalled();
504+
expect(request.fetchGasFeeTokens).toHaveBeenCalledWith(
505+
expect.objectContaining({
506+
isExternalSign: true,
507+
}),
508+
);
509+
expect(request.transaction.isExternalSign).toBe(true);
510+
expect(request.transaction.txParams.nonce).toBeUndefined();
511+
});
512+
483513
it('removes selected gas fee token if native balance sufficient', async () => {
484514
request.transaction.isGasFeeTokenIgnoredIfBalance = true;
485515
request.transaction.selectedGasFeeToken = TOKEN_ADDRESS_1_MOCK;
@@ -503,7 +533,7 @@ describe('Gas Fee Tokens Utils', () => {
503533
expect(request.updateTransaction).not.toHaveBeenCalled();
504534
});
505535

506-
it('does nothing if not ignoring gas fee token when native balance sufficient', async () => {
536+
it('does nothing if not ignoring gas fee token and native token is allowed for fees', async () => {
507537
request.transaction.selectedGasFeeToken = TOKEN_ADDRESS_1_MOCK;
508538
request.transaction.isGasFeeTokenIgnoredIfBalance = false;
509539

packages/transaction-controller/src/utils/gas-fee-tokens.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,31 +146,40 @@ export async function checkGasFeeTokenBeforePublish({
146146
fn: (tx: TransactionMeta) => void,
147147
) => void;
148148
}): Promise<void> {
149-
const { isGasFeeTokenIgnoredIfBalance, selectedGasFeeToken } = transaction;
149+
const {
150+
excludeNativeTokenForFee,
151+
isGasFeeTokenIgnoredIfBalance,
152+
selectedGasFeeToken,
153+
} = transaction;
150154

151-
if (!selectedGasFeeToken || !isGasFeeTokenIgnoredIfBalance) {
155+
if (
156+
!selectedGasFeeToken ||
157+
(!isGasFeeTokenIgnoredIfBalance && !excludeNativeTokenForFee)
158+
) {
152159
return;
153160
}
154161

155162
log('Checking gas fee token before publish', { selectedGasFeeToken });
156163

157-
const hasNativeBalance = await isNativeBalanceSufficientForGas(
158-
transaction,
159-
messenger,
160-
networkClientId,
161-
);
162-
163-
if (hasNativeBalance) {
164-
log(
165-
'Ignoring gas fee token before publish due to sufficient native balance',
164+
if (!excludeNativeTokenForFee) {
165+
const hasNativeBalance = await isNativeBalanceSufficientForGas(
166+
transaction,
167+
messenger,
168+
networkClientId,
166169
);
167170

168-
updateTransaction(transaction.id, (tx) => {
169-
tx.isExternalSign = false;
170-
tx.selectedGasFeeToken = undefined;
171-
});
171+
if (hasNativeBalance) {
172+
log(
173+
'Ignoring gas fee token before publish due to sufficient native balance',
174+
);
172175

173-
return;
176+
updateTransaction(transaction.id, (tx) => {
177+
tx.isExternalSign = false;
178+
tx.selectedGasFeeToken = undefined;
179+
});
180+
181+
return;
182+
}
174183
}
175184

176185
const gasFeeTokens = await fetchGasFeeTokens({

packages/transaction-pay-controller/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Handle gas-station and prefunded gas-estimate edge cases for Across Predict withdraw quotes ([#8762](https://github.com/MetaMask/core/pull/8762))
13+
1014
## [22.5.0]
1115

1216
### Added

0 commit comments

Comments
 (0)