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

- Bump `@metamask/network-controller` from `^35.0.0` to `^35.0.1` ([#9758](https://github.com/MetaMask/core/pull/9758))

### Fixed

- Request `EXACT_OUTPUT` instead of `EXPECTED_OUTPUT` from Relay for HyperCore perps deposits, so the full deposit target is guaranteed to arrive ([#9751](https://github.com/MetaMask/core/pull/9751))
- `EXPECTED_OUTPUT` only guarantees `target * (1 - slippage)` on the destination, so a deposit sized to the exact margin required could arrive short and the follow-on order would fail with insufficient margin.

## [26.2.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3913,6 +3913,58 @@ describe('Relay Quotes Utils', () => {
);
});

// A HyperCore deposit funds an order that needs the whole target as margin.
// EXPECTED_OUTPUT only guarantees `target * (1 - slippage)`, which leaves the
// follow-on order short and it fails on insufficient margin.
it('requests an exact output for Hyperliquid deposits so the full margin is guaranteed', async () => {
const arbitrumToHyperliquidRequest: QuoteRequest = {
...QUOTE_REQUEST_MOCK,
targetChainId: CHAIN_ID_ARBITRUM,
targetTokenAddress: ARBITRUM_USDC_ADDRESS,
};

successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => QUOTE_MOCK,
} as never);

await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [arbitrumToHyperliquidRequest],
transaction: {
...TRANSACTION_META_MOCK,
type: TransactionType.perpsDepositAndOrder,
},
});

const body = JSON.parse(
successfulFetchMock.mock.calls[0][1]?.body as string,
);

expect(body.tradeType).toBe('EXACT_OUTPUT');
});

it('still requests an expected output for non-Hyperliquid targets', async () => {
successfulFetchMock.mockResolvedValue({
ok: true,
json: async () => QUOTE_MOCK,
} as never);

await getRelayQuotes({
accountSupports7702: true,
messenger,
requests: [QUOTE_REQUEST_MOCK],
transaction: TRANSACTION_META_MOCK,
});

const body = JSON.parse(
successfulFetchMock.mock.calls[0][1]?.body as string,
);

expect(body.tradeType).toBe('EXPECTED_OUTPUT');
});

it('does not convert to Hyperliquid deposit when parent transaction is not a Perps deposit', async () => {
const arbitrumUsdcRequest: QuoteRequest = {
...QUOTE_REQUEST_MOCK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ async function getSingleQuote(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const useExactInput = isMaxAmount || request.isPostQuote;

// HyperCore perps deposits fund an order that requires the full target as
// margin, so the delivered amount must be guaranteed rather than expected.
// EXPECTED_OUTPUT only guarantees `target * (1 - slippage)`, which leaves
// the follow-on order short and it fails on insufficient margin.
const useExactOutput = !useExactInput && isHypercoreDeposit(request);

const useExecute =
supports7702 &&
isRelayExecuteEnabled(messenger) &&
Expand Down Expand Up @@ -338,7 +344,7 @@ async function getSingleQuote(
: {}),
recipient: effectiveRequest.recipient ?? from,
slippageTolerance,
tradeType: useExactInput ? 'EXACT_INPUT' : 'EXPECTED_OUTPUT',
tradeType: getTradeType(useExactInput, useExactOutput),
user: from,
};

Expand Down Expand Up @@ -613,6 +619,42 @@ async function processMoneyAccountPostQuote(
* Hyperliquid-specific rewrites on transaction type.
* @returns Normalized request.
*/
/**
* Whether the quote deposits into HyperCore USDC.
*
* `normalizeRequest` remaps Arbitrum-USDC perps deposits to HyperCore before
* the quote is built, so the check is against the normalized target.
*
* @param request - Normalized quote request.
* @returns True when the target is HyperCore USDC.
*/
function isHypercoreDeposit(request: QuoteRequest): boolean {
return (
!request.isHyperliquidSource &&
request.targetChainId === CHAIN_ID_HYPERCORE &&
request.targetTokenAddress.toLowerCase() ===
HYPERCORE_USDC_ADDRESS.toLowerCase()
);
}

/**
* Resolve the Relay trade type for a quote.
*
* @param useExactInput - Whether the user specified the amount to send.
* @param useExactOutput - Whether the delivered amount must be guaranteed.
* @returns The Relay trade type.
*/
function getTradeType(
useExactInput: boolean | undefined,
useExactOutput: boolean,
): RelayQuoteRequest['tradeType'] {
if (useExactInput) {
return 'EXACT_INPUT';
}

return useExactOutput ? 'EXACT_OUTPUT' : 'EXPECTED_OUTPUT';
}

function normalizeRequest(
request: QuoteRequest,
transaction: TransactionMeta,
Expand Down