Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Snapshot `txParamsOriginal` in `updateEditableParams` when `containerTypes` are first applied ([#8546](https://github.com/MetaMask/core/pull/8546))
- Add `requiresAuthorizationList` to `TransactionController:estimateGasBatch` results when EIP-7702 batch gas estimation requires a first-time account upgrade ([#8577](https://github.com/MetaMask/core/pull/8577))

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ import {
getTransactionHistoryLimit,
} from './utils/feature-flags';
import { updateFirstTimeInteraction } from './utils/first-time-interaction';
import type { EstimateGasBatchResult } from './utils/gas';
import {
addGasBuffer,
estimateGas,
Expand Down Expand Up @@ -1717,7 +1718,7 @@ export class TransactionController extends BaseController<
chainId: Hex;
from: Hex;
transactions: BatchTransactionParams[];
}): Promise<{ totalGasLimit: number; gasLimits: number[] }> {
}): Promise<EstimateGasBatchResult> {
return estimateGasBatch({
from,
getSimulationConfig: this.#getSimulationConfig,
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-controller/src/utils/gas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,7 @@ describe('gas', () => {
expect(result).toStrictEqual({
totalGasLimit: GAS_MOCK,
gasLimits: [GAS_MOCK],
requiresAuthorizationList: true,
});

expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith(
Expand Down
14 changes: 12 additions & 2 deletions packages/transaction-controller/src/utils/gas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export type UpdateGasRequest = {
txMeta: TransactionMeta;
};

export type EstimateGasBatchResult = {
gasLimits: number[];
requiresAuthorizationList?: true;
totalGasLimit: number;
};

export const log = createModuleLogger(projectLogger, 'gas');

export const FIXED_GAS = '0x5208';
Expand Down Expand Up @@ -202,7 +208,7 @@ export async function estimateGasBatch({
messenger: TransactionControllerMessenger;
networkClientId: NetworkClientId;
transactions: BatchTransactionParams[];
}): Promise<{ totalGasLimit: number; gasLimits: number[] }> {
}): Promise<EstimateGasBatchResult> {
const chainId = getChainId({ messenger, networkClientId });

const is7702Result = await isAtomicBatchSupported({
Expand Down Expand Up @@ -245,7 +251,11 @@ export async function estimateGasBatch({

log('Estimated EIP-7702 gas limit', totalGasLimit);

return { totalGasLimit, gasLimits: [totalGasLimit] };
return {
gasLimits: [totalGasLimit],
...(isUpgradeRequired ? { requiresAuthorizationList: true } : {}),
totalGasLimit,
};
}

const allTransactionsHaveGas = transactions.every(
Expand Down
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Fall back from Across to later pay strategies when Across quotes would require a first-time EIP-7702 authorization list ([#8577](https://github.com/MetaMask/core/pull/8577))

## [19.3.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,96 @@ describe('AcrossStrategy', () => {
).toBe(false);
});

it('returns false when the transaction has an authorization list', () => {
const strategy = new AcrossStrategy();
expect(
strategy.supports({
...baseRequest,
transaction: {
...TRANSACTION_META_MOCK,
txParams: {
...TRANSACTION_META_MOCK.txParams,
authorizationList: [{ address: '0xabc' as Hex }],
},
} as TransactionMeta,
}),
).toBe(false);
});

it('does not support authorization lists during request support checks', () => {
const strategy = new AcrossStrategy();
const result = strategy.supports({
...baseRequest,
transaction: {
...TRANSACTION_META_MOCK,
txParams: {
...TRANSACTION_META_MOCK.txParams,
authorizationList: [{ address: '0xabc' as Hex }],
},
} as TransactionMeta,
});

expect(result).toBe(false);
});

it('does not support quotes that require first-time 7702 upgrades', () => {
const strategy = new AcrossStrategy();
const quote = {
original: {
metamask: {
gasLimits: [],
is7702: true,
requiresAuthorizationList: true,
},
},
} as TransactionPayQuote<AcrossQuote>;

const result = strategy.checkQuoteSupport({
messenger,
quotes: [quote],
transaction: TRANSACTION_META_MOCK,
});

expect(result).toBe(false);
});

it('supports 7702 quotes that do not require an authorization list', () => {
const strategy = new AcrossStrategy();
const quote = {
original: {
metamask: {
gasLimits: [],
is7702: true,
},
},
} as TransactionPayQuote<AcrossQuote>;

expect(
strategy.checkQuoteSupport({
messenger,
quotes: [quote],
transaction: TRANSACTION_META_MOCK,
}),
).toBe(true);
});

it('returns false for unsupported destination actions', () => {
const strategy = new AcrossStrategy();
expect(
strategy.supports({
...baseRequest,
transaction: {
...TRANSACTION_META_MOCK,
txParams: {
...TRANSACTION_META_MOCK.txParams,
data: '0x12345678' as Hex,
to: '0xdef' as Hex,
},
} as TransactionMeta,
}),
).toBe(false);
});

it('returns true when all requests are cross-chain', () => {
const strategy = new AcrossStrategy();
expect(strategy.supports(baseRequest)).toBe(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { TransactionType } from '@metamask/transaction-controller';

import type {
PayStrategy,
PayStrategyCheckQuoteSupportRequest,
PayStrategyExecuteRequest,
PayStrategyGetQuotesRequest,
TransactionPayQuote,
} from '../../types';
import { getPayStrategiesConfig } from '../../utils/feature-flags';
import { getAcrossDestination } from './across-actions';
import { getAcrossQuotes } from './across-quotes';
import { submitAcrossQuotes } from './across-submit';
import { isSupportedAcrossPerpsDepositRequest } from './perps';
Expand All @@ -28,18 +30,54 @@ export class AcrossStrategy implements PayStrategy<AcrossQuote> {
}

if (request.transaction?.type === TransactionType.perpsDeposit) {
return actionableRequests.every((singleRequest) =>
const supportsPerpsDeposit = actionableRequests.every((singleRequest) =>
isSupportedAcrossPerpsDepositRequest(
singleRequest,
request.transaction?.type,
),
);

if (!supportsPerpsDeposit) {
return false;
}
} else {
// Across doesn't support same-chain swaps (e.g. mUSD conversions).
const hasSameChainRequest = actionableRequests.some(
(singleRequest) =>
singleRequest.sourceChainId === singleRequest.targetChainId,
);

if (hasSameChainRequest) {
return false;
}
}

// Across cannot submit EIP-7702 authorization lists. This pre-quote check
// catches transactions where the authorization list is already present.
// First-time 7702 upgrades discovered during gas planning are handled in
// `checkQuoteSupport` below.
if (request.transaction.txParams?.authorizationList?.length) {
return false;
}

// Across doesn't support same-chain swaps (e.g. mUSD conversions).
return actionableRequests.every(
(singleRequest) =>
singleRequest.sourceChainId !== singleRequest.targetChainId,
return actionableRequests.every((singleRequest) => {
try {
getAcrossDestination(request.transaction, singleRequest);
return true;
} catch {
return false;
}
});
}

checkQuoteSupport(
request: PayStrategyCheckQuoteSupportRequest<AcrossQuote>,
): boolean {
// Gas planning can discover that TransactionController would add an
// authorization list for a first-time 7702 upgrade. `is7702` alone is not a
// blocker because it also covers already-upgraded accounts.
return !request.quotes.some(
(quote) => quote.original.metamask.requiresAuthorizationList,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,7 @@ describe('Across Quotes', () => {
estimateGasBatchMock.mockResolvedValue({
totalGasLimit: 51000,
gasLimits: [51000],
requiresAuthorizationList: true,
});

successfulFetchMock.mockResolvedValue({
Expand Down Expand Up @@ -1232,6 +1233,7 @@ describe('Across Quotes', () => {
},
]);
expect(result[0].original.metamask.is7702).toBe(true);
expect(result[0].original.metamask.requiresAuthorizationList).toBe(true);
expect(calculateGasCostMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
Expand All @@ -1253,6 +1255,43 @@ describe('Across Quotes', () => {
);
});

it('omits the authorization-list flag when a combined batch does not require one', async () => {
estimateGasBatchMock.mockResolvedValue({
totalGasLimit: 51000,
gasLimits: [51000],
});

successfulFetchMock.mockResolvedValue({
json: async () => ({
...QUOTE_MOCK,
approvalTxns: [
{
chainId: 1,
data: '0xaaaa' as Hex,
to: '0xapprove1' as Hex,
value: '0x1' as Hex,
},
],
}),
} as Response);

const result = await getAcrossQuotes({
messenger,
requests: [QUOTE_REQUEST_MOCK],
transaction: TRANSACTION_META_MOCK,
});

expect(result[0].original.metamask).toStrictEqual({
gasLimits: [
{
estimate: 51000,
max: 51000,
},
],
is7702: true,
});
});

it('throws when the shared gas estimator marks a quote as 7702 without a combined gas limit', async () => {
const estimateQuoteGasLimitsSpy = jest.spyOn(
quoteGasUtils,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,8 @@ async function normalizeQuote(
const dustUsd = calculateDustUsd(quote, request, targetFiatRate);
const dust = getFiatValueFromUsd(dustUsd, usdToFiatRate);

const { gasLimits, is7702, sourceNetwork } = await calculateSourceNetworkCost(
quote,
messenger,
request,
);
const { gasLimits, is7702, requiresAuthorizationList, sourceNetwork } =
await calculateSourceNetworkCost(quote, messenger, request);

const targetNetwork = getFiatValueFromUsd(new BigNumber(0), usdToFiatRate);

Expand Down Expand Up @@ -244,6 +241,7 @@ async function normalizeQuote(
const metamask = {
gasLimits,
is7702,
...(requiresAuthorizationList ? { requiresAuthorizationList } : {}),
};

return {
Expand Down Expand Up @@ -390,6 +388,7 @@ async function calculateSourceNetworkCost(
sourceNetwork: TransactionPayQuote<AcrossQuote>['fees']['sourceNetwork'];
gasLimits: AcrossGasLimits;
is7702: boolean;
requiresAuthorizationList?: true;
}> {
const acrossFallbackGas =
getPayStrategiesConfig(messenger).across.fallbackGas;
Expand All @@ -409,7 +408,7 @@ async function calculateSourceNetworkCost(
value: transaction.value ?? '0x0',
})),
});
const { batchGasLimit, is7702 } = gasEstimates;
const { batchGasLimit, is7702, requiresAuthorizationList } = gasEstimates;

if (is7702) {
if (!batchGasLimit) {
Expand Down Expand Up @@ -438,6 +437,7 @@ async function calculateSourceNetworkCost(
max,
},
is7702: true,
...(requiresAuthorizationList ? { requiresAuthorizationList } : {}),
gasLimits: [
{
estimate: batchGasLimit.estimate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export type AcrossQuote = {
metamask: {
gasLimits: AcrossGasLimits;
is7702?: boolean;
requiresAuthorizationList?: true;
};
quote: AcrossSwapApprovalResponse;
request: {
Expand Down
Loading
Loading