Skip to content

Commit e2c6a9a

Browse files
dan437micaelae
authored andcommitted
fix(transaction-pay): allow perps withdraw to Arbitrum USDC by skipping same-token filter for HyperLiquid source (#8387)
## Explanation When a user initiates a perps withdrawal targeting Arbitrum USDC, the `calculatePostQuoteSourceAmounts` function filters out the source token because it matches the destination token (both are Arbitrum USDC on chain `0xa4b1`). This prevents any Relay quote requests from being built, so `calculateTotals` falls back to computing raw gas cost on the dummy Arbitrum transaction — producing an absurd ~$16M transaction fee. The relay strategy's `normalizeRequest()` would have renormalized the source from Arbitrum USDC to HyperCore USDC (chain `0x539`, 8 decimals), making them distinct tokens. But this normalization happens downstream of the source-amounts filter, so it never gets a chance to run. Withdrawing to a different token (e.g. BNB) works fine because `isSameToken` returns false, so the filter doesn't apply and the Relay quote is fetched normally. Fixes https://consensyssoftware.atlassian.net/browse/CONF-1150 ## Changes - Pass `isHyperliquidSource` from `transactionData` into `calculatePostQuoteSourceAmounts` - Bypass the `isSameToken` filter when `isHyperliquidSource` is true, since the relay strategy will renormalize the source to HyperCore USDC (a different chain) - Add unit tests for the new behavior and confirm existing behavior is preserved ## References - Affected flow: Perps → Withdraw → Receive USDC (Arbitrum) - Root cause: `isSameToken(ArbitrumUSDC, ArbitrumUSDC)` → `true` → source amount filtered out → no quotes → fallback to raw transaction gas cost <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adjusts post-quote source-amount filtering logic for `isHyperliquidSource`, which affects whether Relay quotes are requested and therefore impacts fee/quote calculation for withdrawals. Scoped behind a specific flag, but regressions could alter quoting behavior for post-quote flows. > > **Overview** > Fixes HyperLiquid *post-quote (withdrawal)* flows where the source token could be incorrectly dropped as “same as destination,” preventing Relay quote requests and leading to wildly incorrect fee estimates. > > `updateSourceAmounts` now passes `transactionData.isHyperliquidSource` into `calculatePostQuoteSourceAmounts`, which bypasses the same-token/same-chain filter when that flag is true. Adds unit tests covering the new HyperLiquid behavior and preserving the existing same-token filtering when the flag is false, and records the fix in the changelog. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fbcb2ee. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com>
1 parent 219f334 commit e2c6a9a

File tree

3 files changed

+70
-2
lines changed

3 files changed

+70
-2
lines changed

packages/transaction-pay-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
- Support for perps deposit for Across ([#8334](https://github.com/MetaMask/core/pull/8334))
1313

14+
### Fixed
15+
16+
- Fix perps withdraw to Arbitrum USDC showing inflated transaction fee by bypassing same-token filter when `isHyperliquidSource` is set ([#8387](https://github.com/MetaMask/core/pull/8387))
17+
1418
## [19.0.3]
1519

1620
### Changed

packages/transaction-pay-controller/src/utils/source-amounts.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,63 @@ describe('Source Amounts Utils', () => {
353353
expect(transactionData.sourceAmounts).toStrictEqual([]);
354354
});
355355

356+
it('does not filter out same token when isHyperliquidSource is true in post-quote flow', () => {
357+
const transactionData: TransactionData = {
358+
isLoading: false,
359+
isPostQuote: true,
360+
isHyperliquidSource: true,
361+
paymentToken: {
362+
...DESTINATION_TOKEN_MOCK,
363+
address: ARBITRUM_USDC_ADDRESS,
364+
chainId: CHAIN_ID_ARBITRUM,
365+
decimals: 6,
366+
symbol: 'USDC',
367+
},
368+
tokens: [
369+
{
370+
...TRANSACTION_TOKEN_MOCK,
371+
address: ARBITRUM_USDC_ADDRESS,
372+
chainId: CHAIN_ID_ARBITRUM,
373+
skipIfBalance: false,
374+
},
375+
],
376+
};
377+
378+
updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger);
379+
380+
expect(transactionData.sourceAmounts).toStrictEqual([
381+
{
382+
sourceAmountHuman: TRANSACTION_TOKEN_MOCK.amountHuman,
383+
sourceAmountRaw: TRANSACTION_TOKEN_MOCK.amountRaw,
384+
sourceBalanceRaw: TRANSACTION_TOKEN_MOCK.balanceRaw,
385+
sourceChainId: CHAIN_ID_ARBITRUM,
386+
sourceTokenAddress: ARBITRUM_USDC_ADDRESS,
387+
targetTokenAddress: ARBITRUM_USDC_ADDRESS,
388+
},
389+
]);
390+
});
391+
392+
it('still filters out same token when isHyperliquidSource is false in post-quote flow', () => {
393+
const transactionData: TransactionData = {
394+
isLoading: false,
395+
isPostQuote: true,
396+
isHyperliquidSource: false,
397+
paymentToken: DESTINATION_TOKEN_MOCK,
398+
tokens: [
399+
{
400+
...TRANSACTION_TOKEN_MOCK,
401+
address: DESTINATION_TOKEN_MOCK.address,
402+
chainId: DESTINATION_TOKEN_MOCK.chainId,
403+
skipIfBalance: false,
404+
},
405+
],
406+
};
407+
408+
updateSourceAmounts(TRANSACTION_ID_MOCK, transactionData, messenger);
409+
410+
expect(transactionData.sourceAmounts).toStrictEqual([]);
411+
});
412+
356413
it('uses token balance when isMaxAmount is true in post-quote flow', () => {
357414
const transactionData: TransactionData = {
358415
isLoading: false,

packages/transaction-pay-controller/src/utils/source-amounts.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ export function updateSourceAmounts(
4545
// For post-quote flows, source amounts are calculated differently
4646
// The source is the transaction's required token, not the selected token
4747
if (isPostQuote) {
48+
const { isHyperliquidSource } = transactionData;
4849
const sourceAmounts = calculatePostQuoteSourceAmounts(
4950
tokens,
5051
paymentToken,
5152
isMaxAmount ?? false,
53+
isHyperliquidSource,
5254
);
5355
log('Updated post-quote source amounts', { transactionId, sourceAmounts });
5456
transactionData.sourceAmounts = sourceAmounts;
@@ -80,12 +82,14 @@ export function updateSourceAmounts(
8082
* @param tokens - Required tokens from the transaction.
8183
* @param paymentToken - Selected payment/destination token.
8284
* @param isMaxAmount - Whether the transaction is a maximum amount transaction.
85+
* @param isHyperliquidSource - Whether the source is HyperLiquid (perps withdrawal).
8386
* @returns Array of source amounts.
8487
*/
8588
function calculatePostQuoteSourceAmounts(
8689
tokens: TransactionPayRequiredToken[],
8790
paymentToken: TransactionPaymentToken,
8891
isMaxAmount: boolean,
92+
isHyperliquidSource?: boolean,
8993
): TransactionPaySourceAmount[] {
9094
return tokens
9195
.filter((token) => {
@@ -99,8 +103,11 @@ function calculatePostQuoteSourceAmounts(
99103
return false;
100104
}
101105

102-
// Skip same token on same chain
103-
if (isSameToken(token, paymentToken)) {
106+
// Skip same token on same chain, unless the source is HyperLiquid.
107+
// For HyperLiquid withdrawals the relay strategy renormalizes the
108+
// source from Arbitrum USDC to HyperCore USDC (a different chain),
109+
// so the tokens are not actually the same after normalization.
110+
if (isSameToken(token, paymentToken) && !isHyperliquidSource) {
104111
log('Skipping token as same as destination token');
105112
return false;
106113
}

0 commit comments

Comments
 (0)