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

- Bump `@metamask/bridge-controller` from `^75.1.1` to `^75.2.0` ([#9214](https://github.com/MetaMask/core/pull/9214))

### Fixed

- Inherit `isGasFeeSponsored` from the parent transaction into same-chain Relay source-network fees and submission options ([#9216](https://github.com/MetaMask/core/pull/9216))

## [23.13.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ describe('fiat-direct-musd', () => {
networkClientId: NETWORK_CLIENT_ID_MOCK,
origin: 'metamask',
requireApproval: false,
skipInitialGasEstimate: true,
transactions: [
{
params: { data: '0xnewApprove', to: '0xapprove', value: '0x0' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export async function submitDirectMusdVaultDeposit({
networkClientId,
origin: ORIGIN_METAMASK,
requireApproval: false,
skipInitialGasEstimate: true,
transactions: nestedTransactions.map((nestedTransaction, index) => ({
params: {
data: nestedTransaction.data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2529,6 +2529,64 @@ describe('Relay Quotes Utils', () => {
);
});

it('zeroes source network fees and gas limits when parent sponsorship applies', async () => {
successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => QUOTE_MOCK,
} as never);

const result = await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [
{
...QUOTE_REQUEST_MOCK,
targetChainId: QUOTE_REQUEST_MOCK.sourceChainId,
},
],
transaction: {
...TRANSACTION_META_MOCK,
chainId: QUOTE_REQUEST_MOCK.sourceChainId,
isGasFeeSponsored: true,
},
});

const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' };

expect(result[0].fees.sourceNetwork.estimate).toStrictEqual(zeroAmount);
expect(result[0].fees.sourceNetwork.max).toStrictEqual(zeroAmount);
expect(result[0].original.metamask.gasLimits).toStrictEqual([0]);
});

it('does not zero source network fees when parent sponsorship is missing', async () => {
successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => QUOTE_MOCK,
} as never);

const result = await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [
{
...QUOTE_REQUEST_MOCK,
targetChainId: QUOTE_REQUEST_MOCK.sourceChainId,
},
],
transaction: {
...TRANSACTION_META_MOCK,
chainId: QUOTE_REQUEST_MOCK.sourceChainId,
},
});

expect(result[0].fees.sourceNetwork.estimate).toStrictEqual({
fiat: '4.56',
human: '1.725',
raw: '1725000000000000',
usd: '3.45',
});
});

it('using gas total from multiple transactions', async () => {
const quoteMock = cloneDeep(QUOTE_MOCK);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { TOKEN_TRANSFER_FOUR_BYTE } from './constants';
import { applyPolymarketDepositWalletOverrides } from './polymarket/withdraw';
import { fetchRelayQuote } from './relay-api';
import { getRelayMaxGasStationQuote } from './relay-max-gas-station';

import type {
RelayQuote,
RelayQuoteMetamask,
Expand All @@ -75,6 +76,16 @@ const POST_QUOTE_GAS_BUFFER = 1.1;

// Hardcoded gas allowance for the prepended payment override transaction(s).
const PAYMENT_OVERRIDE_GAS = 75_000;
const ZERO_AMOUNT = { fiat: '0', human: '0', raw: '0', usd: '0' };

type RelayStepData = RelayTransactionStep['items'][0]['data'];

type RelayGasResult = {
totalGasEstimate: number;
totalGasLimit: number;
gasLimits: number[];
is7702: boolean;
};

/**
* Fetches Relay quotes.
Expand Down Expand Up @@ -749,11 +760,9 @@ async function calculateSourceNetworkCost(
if (quote.metamask?.isExecute) {
log('Zeroing network fees for execute flow');

const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' };

return {
estimate: zeroAmount,
max: zeroAmount,
estimate: ZERO_AMOUNT,
max: ZERO_AMOUNT,
gasLimits: [],
is7702: false,
};
Expand All @@ -764,16 +773,31 @@ async function calculateSourceNetworkCost(
if (request.isHyperliquidSource) {
log('Zeroing network fees for HyperLiquid withdrawal (gasless)');

const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' };

return {
estimate: zeroAmount,
max: zeroAmount,
estimate: ZERO_AMOUNT,
max: ZERO_AMOUNT,
gasLimits: [],
is7702: false,
};
}

if (
transaction.isGasFeeSponsored &&
request.sourceChainId === transaction.chainId &&
request.targetChainId === transaction.chainId
) {
log('Zeroing source network fees for sponsored same-chain Relay route');

// Gas limit is zero as sponsored transactions go through the EIP-7702
// gas station hook and do not require user-paid gas.
return {
estimate: ZERO_AMOUNT,
max: ZERO_AMOUNT,
gasLimits: [0],
is7702: true,
};
}

const txSteps = quote.steps.filter(
(step): step is RelayTransactionStep => step.kind === 'transaction',
);
Expand Down Expand Up @@ -1019,8 +1043,6 @@ function toRelayQuoteGasTransaction(
};
}

type RelayStepData = RelayTransactionStep['items'][0]['data'];

function getOriginalTxGasParams(
request: QuoteRequest,
transaction: TransactionMeta,
Expand Down Expand Up @@ -1058,13 +1080,6 @@ function getOriginalTxGasParams(
};
}

type RelayGasResult = {
totalGasEstimate: number;
totalGasLimit: number;
gasLimits: number[];
is7702: boolean;
};

function combinePrependedGas(
relayOnlyGas: RelayGasResult,
request: QuoteRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,22 @@ describe('Relay Submit Utils', () => {
);
});

it('passes sponsored gas options when parent sponsorship applies to same-chain quote', async () => {
request.transaction.chainId = CHAIN_ID_MOCK;
request.transaction.isGasFeeSponsored = true;
request.quotes[0].request.targetChainId = CHAIN_ID_MOCK;
request.quotes[0].original.details.currencyOut.currency.chainId = 1;

await submitRelayQuotes(request);

expect(addTransactionMock).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
isGasFeeSponsored: true,
}),
);
});

it('uses predictRelayDeposit type when parent transaction is predictDeposit', async () => {
request.transaction = {
...request.transaction,
Expand Down Expand Up @@ -1176,25 +1192,6 @@ describe('Relay Submit Utils', () => {
);
});

it('sets gas to undefined when gasLimits entry is missing', async () => {
request.quotes[0].original.metamask.gasLimits = [];

await submitRelayQuotes(request);

expect(addTransactionBatchMock).toHaveBeenCalledWith(
expect.objectContaining({
transactions: expect.arrayContaining([
expect.objectContaining({
params: expect.objectContaining({
gas: undefined,
}),
type: TransactionType.relayDeposit,
}),
]),
}),
);
});

it('does not activate 7702 mode with multiple post-quote gas limits', async () => {
request.quotes[0].original.metamask.gasLimits = [21000, 21000];

Expand Down Expand Up @@ -1385,6 +1382,24 @@ describe('Relay Submit Utils', () => {
);
});

it('passes sponsored gas options to same-chain batch submissions', async () => {
request.transaction.chainId = CHAIN_ID_MOCK;
request.transaction.isGasFeeSponsored = true;
request.quotes[0].request.targetChainId = CHAIN_ID_MOCK;
request.quotes[0].original.details.currencyOut.currency.chainId = 1;
request.quotes[0].original.steps[0].items.push({
...request.quotes[0].original.steps[0].items[0],
});

await submitRelayQuotes(request);

expect(addTransactionBatchMock).toHaveBeenCalledWith(
expect.objectContaining({
isGasFeeSponsored: true,
}),
);
});

it('validates source balance before submitting single transaction', async () => {
await submitRelayQuotes(request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
submitPolymarketWithdraw,
} from './polymarket/withdraw';
import { getRelayStatus, submitRelayExecute } from './relay-api';

import type {
RelayExecuteRequest,
RelayQuote,
Expand Down Expand Up @@ -661,7 +662,12 @@ async function submitViaTransactionController(

let result: { result: Promise<string> } | undefined;

const gasFeeToken = quote.fees.isSourceGasFeeToken
const isSourceGasFeeSponsored =
transaction.isGasFeeSponsored &&
quote.request.sourceChainId === transaction.chainId &&
quote.request.targetChainId === transaction.chainId;

const gasFeeToken = !isSourceGasFeeSponsored && quote.fees.isSourceGasFeeToken
? sourceTokenAddress
: undefined;

Expand Down Expand Up @@ -701,6 +707,7 @@ async function submitViaTransactionController(
networkClientId,
origin: ORIGIN_METAMASK,
isInternal: true,
isGasFeeSponsored: isSourceGasFeeSponsored,
requireApproval: false,
type: getRelayDepositType(getEffectiveTransactionType(transaction)),
},
Expand Down Expand Up @@ -745,6 +752,7 @@ async function submitViaTransactionController(
networkClientId,
origin: ORIGIN_METAMASK,
isInternal: true,
isGasFeeSponsored: isSourceGasFeeSponsored,
overwriteUpgrade: true,
requireApproval: false,
transactions,
Expand Down