From 789e5532e04e26ff969a12b3a934038d73331a0b Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 11 Nov 2024 18:52:40 +0700 Subject: [PATCH] refine min output and leftover tokens handling --- api/_dexes/cross-swap.ts | 1 + api/_dexes/types.ts | 6 +++ api/_dexes/uniswap.ts | 89 +++++++++++++++++++++++++++++++++++++--- api/_dexes/utils.ts | 13 +++++- api/_utils.ts | 14 +++++-- scripts/tests/swap.ts | 14 +++---- 6 files changed, 119 insertions(+), 18 deletions(-) diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index 8130c9809..c435a20c3 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -86,6 +86,7 @@ export async function getCrossSwapQuotesForMinOutputB2B(crossSwap: CrossSwap) { message: "0x", }); return { + crossSwap, destinationSwapQuote: undefined, bridgeQuote, originSwapQuote: undefined, diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index 62e7885f3..da5e448c8 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -54,10 +54,16 @@ export type SwapQuote = { data: string; value: string; }; + tokenIn: Token; + tokenOut: Token; }; export type CrossSwapQuotes = { + crossSwap: CrossSwap; bridgeQuote: { + message?: string; + inputToken: Token; + outputToken: Token; inputAmount: BigNumber; outputAmount: BigNumber; minOutputAmount: BigNumber; diff --git a/api/_dexes/uniswap.ts b/api/_dexes/uniswap.ts index bbc002e0f..cf2fc2c53 100644 --- a/api/_dexes/uniswap.ts +++ b/api/_dexes/uniswap.ts @@ -26,6 +26,8 @@ import { Token as AcrossToken, Swap, CrossSwap, + SwapQuote, + CrossSwapQuotes, } from "./types"; import { getSwapAndBridgeAddress, NoSwapRouteError } from "./utils"; import { AMOUNT_TYPE } from "./cross-swap"; @@ -163,9 +165,35 @@ export async function getUniswapCrossSwapQuotesForMinOutputB2A( }), }); + // 3. Re-fetch destination swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the MulticallHandler contract. + const updatedDestinationSwapQuote = await getUniswapQuote({ + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + amount: bridgeQuote.outputAmount.toString(), + recipient: crossSwap.recipient, + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.EXACT_INPUT, + }); + + // 4. Rebuild message + bridgeQuote.message = buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.recipient, + actions: [ + { + target: updatedDestinationSwapQuote.swapTx.to, + callData: updatedDestinationSwapQuote.swapTx.data, + value: updatedDestinationSwapQuote.swapTx.value, + }, + ], + }); + return { + crossSwap, bridgeQuote, - destinationSwapQuote, + destinationSwapQuote: updatedDestinationSwapQuote, originSwapQuote: undefined, }; } @@ -230,6 +258,7 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2B( }); return { + crossSwap, bridgeQuote, destinationSwapQuote: undefined, originSwapQuote, @@ -277,15 +306,26 @@ export async function getBestUniswapCrossSwapQuotesForMinOutputA2A( ); } - const crossSwapQuotes = await Promise.all( + const crossSwapQuotesSettledResults = await Promise.allSettled( bridgeRoutesToCompare.map((bridgeRoute) => getUniswapCrossSwapQuotesForMinOutputA2A(crossSwap, bridgeRoute) ) ); + const crossSwapQuotes = crossSwapQuotesSettledResults + .filter((res) => res.status === "fulfilled") + .map((res) => (res as PromiseFulfilledResult).value); + + if (crossSwapQuotes.length === 0) { + console.log("crossSwapQuotesSettledResults", crossSwapQuotesSettledResults); + throw new Error( + `No successful bridge quotes found for ${originSwapChainId} -> ${destinationSwapChainId}` + ); + } + // Compare quotes by lowest input amount const bestCrossSwapQuote = crossSwapQuotes.reduce((prev, curr) => - prev.originSwapQuote.maximumAmountIn.lt( - curr.originSwapQuote.maximumAmountIn + prev.originSwapQuote!.maximumAmountIn.lt( + curr.originSwapQuote!.maximumAmountIn ) ? prev : curr @@ -375,6 +415,31 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2A( }), }); + // 3. Re-fetch destination swap quote with updated input amount and EXACT_INPUT type. + // This prevents leftover tokens in the MulticallHandler contract. + const updatedDestinationSwapQuote = await getUniswapQuote({ + chainId: destinationSwapChainId, + tokenIn: bridgeableOutputToken, + tokenOut: crossSwap.outputToken, + amount: bridgeQuote.outputAmount.toString(), + recipient: crossSwap.recipient, + slippageTolerance: crossSwap.slippageTolerance, + type: AMOUNT_TYPE.EXACT_INPUT, + }); + + // 4. Rebuild message + bridgeQuote.message = buildMulticallHandlerMessage({ + // @TODO: handle fallback recipient for params `refundOnOrigin` and `refundAddress` + fallbackRecipient: crossSwap.recipient, + actions: [ + { + target: updatedDestinationSwapQuote.swapTx.to, + callData: updatedDestinationSwapQuote.swapTx.data, + value: updatedDestinationSwapQuote.swapTx.value, + }, + ], + }); + // 3. Get origin swap quote for any input token -> bridgeable input token const originSwapQuote = await getUniswapQuote({ chainId: originSwapChainId, @@ -387,13 +452,14 @@ export async function getUniswapCrossSwapQuotesForMinOutputA2A( }); return { - destinationSwapQuote, + crossSwap, + destinationSwapQuote: updatedDestinationSwapQuote, bridgeQuote, originSwapQuote, }; } -export async function getUniswapQuote(swap: Swap) { +export async function getUniswapQuote(swap: Swap): Promise { const { router, options } = getSwapRouterAndOptions(swap); const amountCurrency = @@ -458,6 +524,17 @@ export async function getUniswapQuote(swap: Swap) { }, }; + console.log("swapQuote", { + type: swap.type, + tokenIn: swapQuote.tokenIn.symbol, + tokenOut: swapQuote.tokenOut.symbol, + chainId: swap.chainId, + maximumAmountIn: swapQuote.maximumAmountIn.toString(), + minAmountOut: swapQuote.minAmountOut.toString(), + expectedAmountOut: swapQuote.expectedAmountOut.toString(), + expectedAmountIn: swapQuote.expectedAmountIn.toString(), + }); + return swapQuote; } diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index faeed9659..06cb97ece 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,6 @@ -import { ENABLED_ROUTES } from "../_utils"; +import { UniversalSwapAndBridge__factory } from "@across-protocol/contracts/dist/typechain"; + +import { ENABLED_ROUTES, getProvider } from "../_utils"; export class UnsupportedDex extends Error { constructor(dex: string) { @@ -44,6 +46,15 @@ export function getSwapAndBridgeAddress(dex: string, chainId: number) { return address; } +export function getSwapAndBridge(dex: string, chainId: number) { + const swapAndBridgeAddress = getSwapAndBridgeAddress(dex, chainId); + + return UniversalSwapAndBridge__factory.connect( + swapAndBridgeAddress, + getProvider(chainId) + ); +} + function _isDexSupported( dex: string ): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { diff --git a/api/_utils.ts b/api/_utils.ts index ab064d51d..0b50770f8 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -869,15 +869,20 @@ export async function getBridgeQuoteForMinOutput(params: { outputToken: params.outputToken.address, originChainId: params.inputToken.chainId, destinationChainId: params.outputToken.chainId, - skipAmountLimit: true, + skipAmountLimit: false, recipient: params.recipient, message: params.message, }; - // 1. Use 1 bps as indicative relayer fee pct + // 1. Use the suggested fees to get an indicative quote with + // input amount equal to minOutputAmount let tries = 0; let adjustedInputAmount = params.minOutputAmount; - let adjustmentPct = utils.parseEther("0.001").toString(); + let indicativeQuote = await getSuggestedFees({ + ...baseParams, + amount: adjustedInputAmount.toString(), + }); + let adjustmentPct = indicativeQuote.totalRelayFee.pct; let finalQuote: Awaited> | undefined = undefined; @@ -900,6 +905,7 @@ export async function getBridgeQuoteForMinOutput(params: { finalQuote = adjustedQuote; break; } else { + adjustmentPct = adjustedQuote.totalRelayFee.pct; tries++; } } @@ -914,6 +920,8 @@ export async function getBridgeQuoteForMinOutput(params: { minOutputAmount: params.minOutputAmount, suggestedFees: finalQuote, message: params.message, + inputToken: params.inputToken, + outputToken: params.outputToken, }; } diff --git a/scripts/tests/swap.ts b/scripts/tests/swap.ts index e6cab46a2..c2d185abb 100644 --- a/scripts/tests/swap.ts +++ b/scripts/tests/swap.ts @@ -2,8 +2,6 @@ import axios from "axios"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; import { ethers } from "ethers"; -import { resolveVercelEndpoint } from "../../api/_utils"; - /** * Manual test script for the swap API. Should be converted to a proper test suite. */ @@ -22,7 +20,7 @@ const MIN_OUTPUT_CASES = [ }, // B2A { - minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), + minOutputAmount: ethers.utils.parseUnits("0.001", 18).toString(), inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.WETH.addresses[CHAIN_IDs.ARBITRUM], @@ -32,8 +30,8 @@ const MIN_OUTPUT_CASES = [ }, // A2B { - minOutputAmount: ethers.utils.parseUnits("100", 6).toString(), - inputToken: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed", // DEGEN + minOutputAmount: ethers.utils.parseUnits("10", 6).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], destinationChainId: CHAIN_IDs.ARBITRUM, @@ -42,8 +40,8 @@ const MIN_OUTPUT_CASES = [ }, // A2A { - minOutputAmount: ethers.utils.parseUnits("100", 18).toString(), - inputToken: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed", // DEGEN + minOutputAmount: ethers.utils.parseUnits("1", 18).toString(), + inputToken: TOKEN_SYMBOLS_MAP.USDbC.addresses[CHAIN_IDs.BASE], originChainId: CHAIN_IDs.BASE, outputToken: "0x74885b4D524d497261259B38900f54e6dbAd2210", // APE Coin destinationChainId: CHAIN_IDs.ARBITRUM, @@ -54,7 +52,7 @@ const MIN_OUTPUT_CASES = [ async function swap() { for (const testCase of MIN_OUTPUT_CASES) { - const response = await axios.get(`${resolveVercelEndpoint()}/api/swap`, { + const response = await axios.get(`http://localhost:3000/api/swap`, { params: testCase, }); console.log(response.data);