diff --git a/api/_dexes/cross-swap.ts b/api/_dexes/cross-swap.ts index bcaa5bce9..4d6d0e655 100644 --- a/api/_dexes/cross-swap.ts +++ b/api/_dexes/cross-swap.ts @@ -8,7 +8,6 @@ import { isOutputTokenBridgeable, getBridgeQuoteForMinOutput, getSpokePool, - Profiler, } from "../_utils"; import { getBestUniswapCrossSwapQuotesForOutputA2A, @@ -21,6 +20,7 @@ import { CrossSwap, CrossSwapQuotes } from "./types"; import { buildExactOutputBridgeTokenMessage, buildMinOutputBridgeTokenMessage, + getUniversalSwapAndBridge, } from "./utils"; import { getSpokePoolPeriphery } from "../_spoke-pool-periphery"; import { tagIntegratorId } from "../_integrator-id"; @@ -32,8 +32,6 @@ export type CrossSwapType = export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE]; -export type LeftoverType = (typeof LEFTOVER_TYPE)[keyof typeof LEFTOVER_TYPE]; - export const AMOUNT_TYPE = { EXACT_INPUT: "exactInput", EXACT_OUTPUT: "exactOutput", @@ -47,15 +45,11 @@ export const CROSS_SWAP_TYPE = { ANY_TO_ANY: "anyToAny", } as const; -export const LEFTOVER_TYPE = { - OUTPUT_TOKEN: "outputToken", - BRIDGEABLE_TOKEN: "bridgeableToken", -} as const; - export const PREFERRED_BRIDGE_TOKENS = ["WETH"]; const defaultQuoteFetchStrategy: UniswapQuoteFetchStrategy = - getSwapRouter02Strategy(); + // This will be our default strategy until the periphery contract is audited + getSwapRouter02Strategy("UniversalSwapAndBridge"); const strategyOverrides = { [CHAIN_IDs.BLAST]: defaultQuoteFetchStrategy, }; @@ -79,47 +73,28 @@ export async function getCrossSwapQuotes( } export async function getCrossSwapQuotesForOutput(crossSwap: CrossSwap) { - const profiler = new Profiler({ - at: "api/cross-swap#getCrossSwapQuotesForOutput", - logger: console, - }); const crossSwapType = getCrossSwapType({ inputToken: crossSwap.inputToken.address, originChainId: crossSwap.inputToken.chainId, outputToken: crossSwap.outputToken.address, destinationChainId: crossSwap.outputToken.chainId, + isInputNative: Boolean(crossSwap.isInputNative), }); if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputB2B(crossSwap), - "getCrossSwapQuotesForOutputB2B", - crossSwap - ); + return getCrossSwapQuotesForOutputB2B(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputB2A(crossSwap), - "getCrossSwapQuotesForOutputB2A", - crossSwap - ); + return getCrossSwapQuotesForOutputB2A(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputA2B(crossSwap), - "getCrossSwapQuotesForOutputA2B", - crossSwap - ); + return getCrossSwapQuotesForOutputA2B(crossSwap); } if (crossSwapType === CROSS_SWAP_TYPE.ANY_TO_ANY) { - return profiler.measureAsync( - getCrossSwapQuotesForOutputA2A(crossSwap), - "getCrossSwapQuotesForOutputA2A", - crossSwap - ); + return getCrossSwapQuotesForOutputA2A(crossSwap); } throw new Error("Invalid cross swap type"); @@ -194,6 +169,7 @@ export function getCrossSwapType(params: { originChainId: number; outputToken: string; destinationChainId: number; + isInputNative: boolean; }): CrossSwapType { if ( isRouteEnabled( @@ -206,6 +182,20 @@ export function getCrossSwapType(params: { return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE; } + // Prefer destination swap if input token is native because legacy + // `UniversalSwapAndBridge` does not support native tokens as input. + if (params.isInputNative) { + if (isInputTokenBridgeable(params.inputToken, params.originChainId)) { + return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY; + } + // We can't bridge native tokens that are not ETH, e.g. MATIC or AZERO. Therefore + // throw until we have periphery contract audited so that it can accept native + // tokens and do an origin swap. + throw new Error( + "Unsupported swap: Input token is native but not bridgeable" + ); + } + if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) { return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE; } @@ -230,24 +220,45 @@ export async function buildCrossSwapTxForAllowanceHolder( let toAddress: string; if (crossSwapQuotes.originSwapQuote) { - const spokePoolPeriphery = getSpokePoolPeriphery( - crossSwapQuotes.originSwapQuote.peripheryAddress, - originChainId - ); - tx = await spokePoolPeriphery.populateTransaction.swapAndBridge( - crossSwapQuotes.originSwapQuote.tokenIn.address, - crossSwapQuotes.originSwapQuote.tokenOut.address, - crossSwapQuotes.originSwapQuote.swapTx.data, - crossSwapQuotes.originSwapQuote.maximumAmountIn, - crossSwapQuotes.originSwapQuote.minAmountOut, - deposit, - { - value: crossSwapQuotes.crossSwap.isInputNative - ? crossSwapQuotes.originSwapQuote.maximumAmountIn - : 0, - } - ); - toAddress = spokePoolPeriphery.address; + const { entryPointContract } = crossSwapQuotes.originSwapQuote; + if (entryPointContract.name === "SpokePoolPeriphery") { + const spokePoolPeriphery = getSpokePoolPeriphery( + entryPointContract.address, + originChainId + ); + tx = await spokePoolPeriphery.populateTransaction.swapAndBridge( + crossSwapQuotes.originSwapQuote.tokenIn.address, + crossSwapQuotes.originSwapQuote.tokenOut.address, + crossSwapQuotes.originSwapQuote.swapTx.data, + crossSwapQuotes.originSwapQuote.maximumAmountIn, + crossSwapQuotes.originSwapQuote.minAmountOut, + deposit, + { + value: crossSwapQuotes.crossSwap.isInputNative + ? crossSwapQuotes.originSwapQuote.maximumAmountIn + : 0, + } + ); + toAddress = spokePoolPeriphery.address; + } else if (entryPointContract.name === "UniversalSwapAndBridge") { + const universalSwapAndBridge = getUniversalSwapAndBridge( + entryPointContract.dex, + originChainId + ); + tx = await universalSwapAndBridge.populateTransaction.swapAndBridge( + crossSwapQuotes.originSwapQuote.tokenIn.address, + crossSwapQuotes.originSwapQuote.tokenOut.address, + crossSwapQuotes.originSwapQuote.swapTx.data, + crossSwapQuotes.originSwapQuote.maximumAmountIn, + crossSwapQuotes.originSwapQuote.minAmountOut, + deposit + ); + toAddress = universalSwapAndBridge.address; + } else { + throw new Error( + `Could not build cross swap tx for unknown entry point contract` + ); + } } else { tx = await spokePool.populateTransaction.depositV3( deposit.depositor, diff --git a/api/_dexes/types.ts b/api/_dexes/types.ts index 8eab02695..e69fc1418 100644 --- a/api/_dexes/types.ts +++ b/api/_dexes/types.ts @@ -1,6 +1,6 @@ import { BigNumber } from "ethers"; import { getSuggestedFees } from "../_utils"; -import { AmountType, CrossSwapType, LeftoverType } from "./cross-swap"; +import { AmountType, CrossSwapType } from "./cross-swap"; export type { AmountType, CrossSwapType }; @@ -31,7 +31,6 @@ export type CrossSwap = { recipient: string; slippageTolerance: number; type: AmountType; - leftoverType?: LeftoverType; refundOnOrigin: boolean; refundAddress?: string; isInputNative?: boolean; @@ -77,10 +76,21 @@ export type CrossSwapQuotes = { }; destinationSwapQuote?: SwapQuote; originSwapQuote?: SwapQuote & { - peripheryAddress: string; + entryPointContract: OriginSwapEntryPointContract; }; }; +export type OriginSwapEntryPointContract = + | { + name: "SpokePoolPeriphery"; + address: string; + } + | { + name: "UniversalSwapAndBridge"; + address: string; + dex: SupportedDex; + }; + export type CrossSwapQuotesWithFees = CrossSwapQuotes & { fees: CrossSwapFees; }; diff --git a/api/_dexes/uniswap/quote-resolver.ts b/api/_dexes/uniswap/quote-resolver.ts index b4506573e..0ce219063 100644 --- a/api/_dexes/uniswap/quote-resolver.ts +++ b/api/_dexes/uniswap/quote-resolver.ts @@ -7,6 +7,7 @@ import { getBridgeQuoteForMinOutput, getRoutesByChainIds, getRouteByOutputTokenAndOriginChain, + Profiler, } from "../../_utils"; import { buildMulticallHandlerMessage, @@ -42,6 +43,10 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( crossSwap: CrossSwap, strategy: UniswapQuoteFetchStrategy ): Promise { + const profiler = new Profiler({ + at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputB2A", + logger: console, + }); const destinationSwapChainId = crossSwap.outputToken.chainId; const bridgeRoute = getRouteByInputTokenAndDestinationChain( crossSwap.inputToken.address, @@ -83,27 +88,33 @@ export async function getUniswapCrossSwapQuotesForOutputB2A( }; // 1. Get destination swap quote for bridgeable output token -> any token // with exact output amount. - let destinationSwapQuote = await strategy.fetchFn( - { - ...destinationSwap, - amount: crossSwap.amount.toString(), - }, - TradeType.EXACT_OUTPUT + let destinationSwapQuote = await profiler.measureAsync( + strategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ), + "getDestinationSwapQuote" ); // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: crossSwap.inputToken, - outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - routerAddress: strategy.getRouterAddress(destinationSwapChainId), + const bridgeQuote = await profiler.measureAsync( + getBridgeQuoteForMinOutput({ + inputToken: crossSwap.inputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: strategy.getRouterAddress(destinationSwapChainId), + }), }), - }); + "getBridgeQuote" + ); return { crossSwap, @@ -123,6 +134,10 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( crossSwap: CrossSwap, strategy: UniswapQuoteFetchStrategy ): Promise { + const profiler = new Profiler({ + at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputA2B", + logger: console, + }); const originSwapChainId = crossSwap.inputToken.chainId; const destinationChainId = crossSwap.outputToken.chainId; const bridgeRoute = getRouteByOutputTokenAndOriginChain( @@ -156,13 +171,16 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( }; // 1. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: bridgeableInputToken, - outputToken: crossSwap.outputToken, - minOutputAmount: crossSwap.amount, - recipient: getMultiCallHandlerAddress(destinationChainId), - message: buildExactOutputBridgeTokenMessage(crossSwap), - }); + const bridgeQuote = await profiler.measureAsync( + getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: crossSwap.outputToken, + minOutputAmount: crossSwap.amount, + recipient: getMultiCallHandlerAddress(destinationChainId), + message: buildExactOutputBridgeTokenMessage(crossSwap), + }), + "getBridgeQuote" + ); // 1.1. Update bridge quote message for min. output amount if (crossSwap.type === AMOUNT_TYPE.MIN_OUTPUT && crossSwap.isOutputNative) { bridgeQuote.message = buildMinOutputBridgeTokenMessage( @@ -171,38 +189,44 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( ); } - const spokePoolPeripheryAddress = - strategy.getPeripheryAddress(originSwapChainId); + const originSwapEntryPoint = + strategy.getOriginSwapEntryPoint(originSwapChainId); const originSwap = { chainId: originSwapChainId, tokenIn: crossSwap.inputToken, tokenOut: bridgeableInputToken, - recipient: spokePoolPeripheryAddress, + recipient: originSwapEntryPoint.address, slippageTolerance: crossSwap.slippageTolerance, type: crossSwap.type, }; // 2.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await strategy.fetchFn( - { - ...originSwap, - amount: bridgeQuote.inputAmount.toString(), - }, - TradeType.EXACT_OUTPUT, - { - useIndicativeQuote: true, - } + const originSwapQuote = await profiler.measureAsync( + strategy.fetchFn( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } + ), + "INDICATIVE_getOriginSwapQuote" ); // 2.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await strategy.fetchFn( - { - ...originSwap, - amount: addMarkupToAmount( - originSwapQuote.maximumAmountIn, - indicativeQuoteBuffer - ).toString(), - }, - TradeType.EXACT_INPUT + let adjOriginSwapQuote = await profiler.measureAsync( + strategy.fetchFn( + { + ...originSwap, + amount: addMarkupToAmount( + originSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ).toString(), + }, + TradeType.EXACT_INPUT + ), + "getOriginSwapQuote" ); assertMinOutputAmount( adjOriginSwapQuote.minAmountOut, @@ -215,7 +239,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B( destinationSwapQuote: undefined, originSwapQuote: { ...adjOriginSwapQuote, - peripheryAddress: spokePoolPeripheryAddress, + entryPointContract: originSwapEntryPoint, }, }; } @@ -302,6 +326,10 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( originStrategy: UniswapQuoteFetchStrategy, destinationStrategy: UniswapQuoteFetchStrategy ): Promise { + const profiler = new Profiler({ + at: "api/_dexes/uniswap/quote-resolver#getUniswapCrossSwapQuotesForOutputA2A", + logger: console, + }); const originSwapChainId = crossSwap.inputToken.chainId; const destinationSwapChainId = crossSwap.outputToken.chainId; @@ -341,11 +369,13 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( const multiCallHandlerAddress = getMultiCallHandlerAddress( destinationSwapChainId ); + const originSwapEntryPoint = + originStrategy.getOriginSwapEntryPoint(originSwapChainId); const originSwap = { chainId: originSwapChainId, tokenIn: crossSwap.inputToken, tokenOut: bridgeableInputToken, - recipient: originStrategy.getPeripheryAddress(originSwapChainId), + recipient: originSwapEntryPoint.address, slippageTolerance: crossSwap.slippageTolerance, type: crossSwap.type, }; @@ -360,52 +390,64 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( // 1. Get destination swap quote for bridgeable output token -> any token // with exact output amount - let destinationSwapQuote = await destinationStrategy.fetchFn( - { - ...destinationSwap, - amount: crossSwap.amount.toString(), - }, - TradeType.EXACT_OUTPUT + let destinationSwapQuote = await profiler.measureAsync( + destinationStrategy.fetchFn( + { + ...destinationSwap, + amount: crossSwap.amount.toString(), + }, + TradeType.EXACT_OUTPUT + ), + "getDestinationSwapQuote" ); // 2. Get bridge quote for bridgeable input token -> bridgeable output token - const bridgeQuote = await getBridgeQuoteForMinOutput({ - inputToken: bridgeableInputToken, - outputToken: bridgeableOutputToken, - minOutputAmount: destinationSwapQuote.maximumAmountIn, - recipient: getMultiCallHandlerAddress(destinationSwapChainId), - message: buildDestinationSwapCrossChainMessage({ - crossSwap, - destinationSwapQuote, - bridgeableOutputToken, - routerAddress: destinationStrategy.getRouterAddress( - destinationSwapChainId - ), + const bridgeQuote = await profiler.measureAsync( + getBridgeQuoteForMinOutput({ + inputToken: bridgeableInputToken, + outputToken: bridgeableOutputToken, + minOutputAmount: destinationSwapQuote.maximumAmountIn, + recipient: getMultiCallHandlerAddress(destinationSwapChainId), + message: buildDestinationSwapCrossChainMessage({ + crossSwap, + destinationSwapQuote, + bridgeableOutputToken, + routerAddress: destinationStrategy.getRouterAddress( + destinationSwapChainId + ), + }), }), - }); + "getBridgeQuote" + ); // 3.1. Get origin swap quote for any input token -> bridgeable input token - const originSwapQuote = await originStrategy.fetchFn( - { - ...originSwap, - amount: bridgeQuote.inputAmount.toString(), - }, - TradeType.EXACT_OUTPUT, - { - useIndicativeQuote: true, - } + const originSwapQuote = await profiler.measureAsync( + originStrategy.fetchFn( + { + ...originSwap, + amount: bridgeQuote.inputAmount.toString(), + }, + TradeType.EXACT_OUTPUT, + { + useIndicativeQuote: true, + } + ), + "INDICATIVE_getOriginSwapQuote" ); // 3.2. Re-fetch origin swap quote with updated input amount and EXACT_INPUT type. // This prevents leftover tokens in the SwapAndBridge contract. - let adjOriginSwapQuote = await originStrategy.fetchFn( - { - ...originSwap, - amount: addMarkupToAmount( - originSwapQuote.maximumAmountIn, - indicativeQuoteBuffer - ).toString(), - }, - TradeType.EXACT_INPUT + let adjOriginSwapQuote = await profiler.measureAsync( + originStrategy.fetchFn( + { + ...originSwap, + amount: addMarkupToAmount( + originSwapQuote.maximumAmountIn, + indicativeQuoteBuffer + ).toString(), + }, + TradeType.EXACT_INPUT + ), + "getOriginSwapQuote" ); assertMinOutputAmount( adjOriginSwapQuote.minAmountOut, @@ -418,7 +460,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A( bridgeQuote, originSwapQuote: { ...adjOriginSwapQuote, - peripheryAddress: originStrategy.getPeripheryAddress(originSwapChainId), + entryPointContract: originSwapEntryPoint, }, }; } diff --git a/api/_dexes/uniswap/swap-router-02.ts b/api/_dexes/uniswap/swap-router-02.ts index 91482ac1a..094f5428b 100644 --- a/api/_dexes/uniswap/swap-router-02.ts +++ b/api/_dexes/uniswap/swap-router-02.ts @@ -5,7 +5,7 @@ import { SwapRouter } from "@uniswap/router-sdk"; import { CHAIN_IDs } from "@across-protocol/constants"; import { getLogger } from "../../_utils"; -import { Swap, SwapQuote } from "../types"; +import { OriginSwapEntryPointContract, Swap, SwapQuote } from "../types"; import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery"; import { addMarkupToAmount, @@ -18,6 +18,7 @@ import { UniswapClassicQuoteFromApi, } from "./trading-api"; import { RouterTradeAdapter } from "./adapter"; +import { getUniversalSwapAndBridgeAddress } from "../utils"; // Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/ export const SWAP_ROUTER_02_ADDRESS = { @@ -32,10 +33,27 @@ export const SWAP_ROUTER_02_ADDRESS = { [CHAIN_IDs.ZORA]: "0x7De04c96BE5159c3b5CeffC82aa176dc81281557", }; -export function getSwapRouter02Strategy(): UniswapQuoteFetchStrategy { +export function getSwapRouter02Strategy( + originSwapEntryPointContractName: OriginSwapEntryPointContract["name"] +): UniswapQuoteFetchStrategy { const getRouterAddress = (chainId: number) => SWAP_ROUTER_02_ADDRESS[chainId]; - const getPeripheryAddress = (chainId: number) => - getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId); + const getOriginSwapEntryPoint = (chainId: number) => { + if (originSwapEntryPointContractName === "SpokePoolPeriphery") { + return { + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId), + } as const; + } else if (originSwapEntryPointContractName === "UniversalSwapAndBridge") { + return { + name: "UniversalSwapAndBridge", + address: getUniversalSwapAndBridgeAddress("uniswap", chainId), + dex: "uniswap", + } as const; + } + throw new Error( + `Unknown origin swap entry point contract '${originSwapEntryPointContractName}'` + ); + }; const fetchFn = async ( swap: Swap, @@ -127,7 +145,7 @@ export function getSwapRouter02Strategy(): UniswapQuoteFetchStrategy { return { getRouterAddress, - getPeripheryAddress, + getOriginSwapEntryPoint, fetchFn, }; } diff --git a/api/_dexes/uniswap/universal-router.ts b/api/_dexes/uniswap/universal-router.ts index b2722c5ed..bbaad27df 100644 --- a/api/_dexes/uniswap/universal-router.ts +++ b/api/_dexes/uniswap/universal-router.ts @@ -34,8 +34,11 @@ export const UNIVERSAL_ROUTER_ADDRESS = { export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy { const getRouterAddress = (chainId: number) => UNIVERSAL_ROUTER_ADDRESS[chainId]; - const getPeripheryAddress = (chainId: number) => - getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId); + const getOriginSwapEntryPoint = (chainId: number) => + ({ + name: "SpokePoolPeriphery", + address: getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId), + }) as const; const fetchFn = async ( swap: Swap, @@ -127,7 +130,7 @@ export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy { return { getRouterAddress, - getPeripheryAddress, + getOriginSwapEntryPoint, fetchFn, }; } diff --git a/api/_dexes/uniswap/utils.ts b/api/_dexes/uniswap/utils.ts index b85818564..2840ff4cb 100644 --- a/api/_dexes/uniswap/utils.ts +++ b/api/_dexes/uniswap/utils.ts @@ -8,7 +8,16 @@ import { Swap, SwapQuote, Token } from "../types"; export type UniswapQuoteFetchStrategy = { getRouterAddress: (chainId: number) => string; - getPeripheryAddress: (chainId: number) => string; + getOriginSwapEntryPoint: (chainId: number) => + | { + name: "SpokePoolPeriphery"; + address: string; + } + | { + name: "UniversalSwapAndBridge"; + address: string; + dex: "uniswap" | "1inch"; + }; fetchFn: UniswapQuoteFetchFn; }; export type UniswapQuoteFetchFn = ( diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index 6c1dfccbc..d90f95664 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,4 +1,7 @@ -import { SwapAndBridge__factory } from "@across-protocol/contracts"; +import { + SwapAndBridge__factory, + UniversalSwapAndBridge__factory, +} from "@across-protocol/contracts"; import { BigNumber, constants } from "ethers"; import { ENABLED_ROUTES, getProvider } from "../_utils"; @@ -11,15 +14,19 @@ import { } from "../_multicall-handler"; import { CrossSwap } from "./types"; +type SwapAndBridgeType = "SwapAndBridge" | "UniversalSwapAndBridge"; + export class UnsupportedDex extends Error { - constructor(dex: string) { - super(`DEX/Aggregator ${dex} not supported`); + constructor(dex: string, type: SwapAndBridgeType) { + super(`DEX/Aggregator '${dex}' not supported for '${type}'`); } } export class UnsupportedDexOnChain extends Error { - constructor(chainId: number, dex: string) { - super(`DEX/Aggregator ${dex} not supported on chain ${chainId}`); + constructor(chainId: number, dex: string, type: SwapAndBridgeType) { + super( + `DEX/Aggregator '${dex}' not supported on chain ${chainId} for '${type}'` + ); } } @@ -41,16 +48,37 @@ export const swapAndBridgeDexes = Object.keys( ENABLED_ROUTES.swapAndBridgeAddresses ); +export const universalSwapAndBridgeDexes = Object.keys( + ENABLED_ROUTES.universalSwapAndBridgeAddresses +); + export function getSwapAndBridgeAddress(dex: string, chainId: number) { - if (!_isDexSupported(dex)) { - throw new UnsupportedDex(dex); + if (!_isDexSupportedForSwapAndBridge(dex)) { + throw new UnsupportedDex(dex, "SwapAndBridge"); } const address = ( ENABLED_ROUTES.swapAndBridgeAddresses[dex] as Record )?.[chainId]; if (!address) { - throw new UnsupportedDexOnChain(chainId, dex); + throw new UnsupportedDexOnChain(chainId, dex, "SwapAndBridge"); + } + return address; +} + +export function getUniversalSwapAndBridgeAddress(dex: string, chainId: number) { + if (!_isDexSupportedForUniversalSwapAndBridge(dex)) { + throw new UnsupportedDex(dex, "UniversalSwapAndBridge"); + } + + const address = ( + ENABLED_ROUTES.universalSwapAndBridgeAddresses[dex] as Record< + string, + string + > + )?.[chainId]; + if (!address) { + throw new UnsupportedDexOnChain(chainId, dex, "UniversalSwapAndBridge"); } return address; } @@ -64,12 +92,30 @@ export function getSwapAndBridge(dex: string, chainId: number) { ); } -function _isDexSupported( +export function getUniversalSwapAndBridge(dex: string, chainId: number) { + const universalSwapAndBridgeAddress = getUniversalSwapAndBridgeAddress( + dex, + chainId + ); + + return UniversalSwapAndBridge__factory.connect( + universalSwapAndBridgeAddress, + getProvider(chainId) + ); +} + +function _isDexSupportedForSwapAndBridge( dex: string ): dex is keyof typeof ENABLED_ROUTES.swapAndBridgeAddresses { return swapAndBridgeDexes.includes(dex); } +function _isDexSupportedForUniversalSwapAndBridge( + dex: string +): dex is keyof typeof ENABLED_ROUTES.universalSwapAndBridgeAddresses { + return universalSwapAndBridgeDexes.includes(dex); +} + /** * This builds a cross-chain message for a (any/bridgeable)-to-bridgeable cross swap * with a specific amount of output tokens that the recipient will receive. Excess diff --git a/api/_utils.ts b/api/_utils.ts index 1774b8d1e..95bd9659f 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -85,6 +85,7 @@ import { } from "./_errors"; import { Token } from "./_dexes/types"; import { CoingeckoQueryParams } from "./coingecko"; +import { addMarkupToAmount } from "./_dexes/uniswap/utils"; export { InputError, handleErrorCondition } from "./_errors"; export const { Profiler } = sdk.utils; @@ -203,6 +204,9 @@ export const getLogger = (): LoggingUtility => { * @returns A valid URL of the current endpoint in vercel */ export const resolveVercelEndpoint = () => { + if (process.env.REACT_APP_VERCEL_API_BASE_URL_OVERRIDE) { + return process.env.REACT_APP_VERCEL_API_BASE_URL_OVERRIDE; + } const url = process.env.VERCEL_URL ?? "across.to"; const env = process.env.VERCEL_ENV ?? "development"; switch (env) { @@ -894,6 +898,8 @@ export async function getBridgeQuoteForMinOutput(params: { recipient?: string; message?: string; }) { + const maxTries = 3; + const tryChunkSize = 3; const baseParams = { inputToken: params.inputToken.address, outputToken: params.outputToken.address, @@ -908,7 +914,7 @@ export async function getBridgeQuoteForMinOutput(params: { // 1. Use the suggested fees to get an indicative quote with // input amount equal to minOutputAmount let tries = 0; - let adjustedInputAmount = params.minOutputAmount; + let adjustedInputAmount = addMarkupToAmount(params.minOutputAmount, 0.005); let indicativeQuote = await getSuggestedFees({ ...baseParams, amount: adjustedInputAmount.toString(), @@ -918,27 +924,45 @@ export async function getBridgeQuoteForMinOutput(params: { undefined; // 2. Adjust input amount to meet minOutputAmount - while (tries < 3) { - adjustedInputAmount = adjustedInputAmount - .mul(utils.parseEther("1").add(adjustmentPct)) - .div(sdk.utils.fixedPointAdjustment); - const adjustedQuote = await getSuggestedFees({ - ...baseParams, - amount: adjustedInputAmount.toString(), + while (tries < maxTries) { + const inputAmounts = Array.from({ length: tryChunkSize }).map((_, i) => { + const buffer = 0.001 * i; + return addMarkupToAmount( + adjustedInputAmount + .mul(utils.parseEther("1").add(adjustmentPct)) + .div(sdk.utils.fixedPointAdjustment), + buffer + ); }); - const outputAmount = adjustedInputAmount.sub( - adjustedInputAmount - .mul(adjustedQuote.totalRelayFee.pct) - .div(sdk.utils.fixedPointAdjustment) + const quotes = await Promise.all( + inputAmounts.map((inputAmount) => { + return getSuggestedFees({ + ...baseParams, + amount: inputAmount.toString(), + }); + }) ); - if (outputAmount.gte(params.minOutputAmount)) { - finalQuote = adjustedQuote; + for (const [i, quote] of Object.entries(quotes)) { + const inputAmount = inputAmounts[Number(i)]; + const outputAmount = inputAmount.sub( + inputAmount + .mul(quote.totalRelayFee.pct) + .div(sdk.utils.fixedPointAdjustment) + ); + if (outputAmount.gte(params.minOutputAmount)) { + finalQuote = quote; + adjustedInputAmount = inputAmount; + break; + } + } + + if (finalQuote) { break; - } else { - adjustmentPct = adjustedQuote.totalRelayFee.pct; - tries++; } + + adjustedInputAmount = inputAmounts[inputAmounts.length - 1]; + tries++; } if (!finalQuote) { diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index 27abfeecd..18237320a 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -10,7 +10,6 @@ import { getCachedTokenInfo, getWrappedNativeTokenAddress, getCachedTokenPrice, - Profiler, } from "../_utils"; import { AMOUNT_TYPE, @@ -49,10 +48,6 @@ export type BaseSwapQueryParams = Infer; export async function handleBaseSwapQueryParams({ query, }: TypedVercelRequest) { - const profiler = new Profiler({ - at: "api/_utils#handleBaseSwapQueryParams", - logger: console, - }); assert(query, BaseSwapQueryParamsSchema); const { @@ -121,25 +116,19 @@ export async function handleBaseSwapQueryParams({ ]); // 2. Get swap quotes and calldata based on the swap type - const crossSwapQuotes = await profiler.measureAsync( - getCrossSwapQuotes({ - amount, - inputToken, - outputToken, - depositor, - recipient: recipient || depositor, - slippageTolerance: Number(slippageTolerance), - type: amountType, - refundOnOrigin, - refundAddress, - isInputNative, - isOutputNative, - }), - "getCrossSwapQuotes", - { - swapType: amountType, - } - ); + const crossSwapQuotes = await getCrossSwapQuotes({ + amount, + inputToken, + outputToken, + depositor, + recipient: recipient || depositor, + slippageTolerance: Number(slippageTolerance), + type: amountType, + refundOnOrigin, + refundAddress, + isInputNative, + isOutputNative, + }); // 3. Calculate fees based for full route const fees = await calculateCrossSwapFees(crossSwapQuotes); diff --git a/api/swap/allowance.ts b/api/swap/allowance.ts index f2baafc28..bdace9f82 100644 --- a/api/swap/allowance.ts +++ b/api/swap/allowance.ts @@ -45,18 +45,20 @@ const handler = async ( integratorId ); - const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; + const { originSwapQuote, bridgeQuote, destinationSwapQuote, crossSwap } = + crossSwapQuotes; + + const originChainId = crossSwap.inputToken.chainId; const inputTokenAddress = isInputNative ? constants.AddressZero - : crossSwapQuotes.crossSwap.inputToken.address; + : crossSwap.inputToken.address; const inputAmount = - crossSwapQuotes.originSwapQuote?.maximumAmountIn || - crossSwapQuotes.bridgeQuote.inputAmount; + originSwapQuote?.maximumAmountIn || bridgeQuote.inputAmount; const { allowance, balance } = await getBalanceAndAllowance({ chainId: originChainId, tokenAddress: inputTokenAddress, - owner: crossSwapQuotes.crossSwap.depositor, + owner: crossSwap.depositor, spender: crossSwapTx.to, }); @@ -72,7 +74,7 @@ const handler = async ( [originTxGas, originTxGasPrice] = await Promise.all([ provider.estimateGas({ ...crossSwapTx, - from: crossSwapQuotes.crossSwap.depositor, + from: crossSwap.depositor, }), latestGasPriceCache(originChainId).get(), ]); @@ -91,15 +93,15 @@ const handler = async ( const approvalAmount = constants.MaxUint256; if (allowance.lt(inputAmount)) { approvalTxns = getApprovalTxns({ - token: crossSwapQuotes.crossSwap.inputToken, + token: crossSwap.inputToken, spender: crossSwapTx.to, amount: approvalAmount, }); } - const refundToken = crossSwapQuotes.crossSwap.refundOnOrigin - ? crossSwapQuotes.bridgeQuote.inputToken - : crossSwapQuotes.bridgeQuote.outputToken; + const refundToken = crossSwap.refundOnOrigin + ? bridgeQuote.inputToken + : bridgeQuote.outputToken; const responseJson = { fees: crossSwapQuotes.fees, @@ -117,6 +119,34 @@ const handler = async ( }, }, approvalTxns, + quotes: { + originSwap: originSwapQuote + ? { + tokenIn: originSwapQuote.tokenIn, + tokenOut: originSwapQuote.tokenOut, + inputAmount: originSwapQuote.expectedAmountIn.toString(), + outputAmount: originSwapQuote.expectedAmountOut.toString(), + minOutputAmount: originSwapQuote.minAmountOut.toString(), + maxInputAmount: originSwapQuote.maximumAmountIn.toString(), + } + : undefined, + bridge: { + inputAmount: bridgeQuote.inputAmount.toString(), + outputAmount: bridgeQuote.outputAmount.toString(), + tokenIn: bridgeQuote.inputToken, + tokenOut: bridgeQuote.outputToken, + }, + destinationSwap: destinationSwapQuote + ? { + tokenIn: destinationSwapQuote.tokenIn, + tokenOut: destinationSwapQuote.tokenOut, + inputAmount: destinationSwapQuote.expectedAmountIn.toString(), + maxInputAmount: destinationSwapQuote.maximumAmountIn.toString(), + outputAmount: destinationSwapQuote.expectedAmountOut.toString(), + minOutputAmount: destinationSwapQuote.minAmountOut.toString(), + } + : undefined, + }, swapTx: { simulationSuccess: !!originTxGas, chainId: originChainId, @@ -134,21 +164,17 @@ const handler = async ( } : refundToken, inputAmount: - crossSwapQuotes.originSwapQuote?.expectedAmountIn.toString() ?? - crossSwapQuotes.bridgeQuote.inputAmount.toString(), + originSwapQuote?.expectedAmountIn.toString() ?? + bridgeQuote.inputAmount.toString(), expectedOutputAmount: - crossSwapQuotes.destinationSwapQuote?.expectedAmountOut.toString() ?? - crossSwapQuotes.bridgeQuote.outputAmount.toString(), + destinationSwapQuote?.expectedAmountOut.toString() ?? + bridgeQuote.outputAmount.toString(), minOutputAmount: - crossSwapQuotes.destinationSwapQuote?.minAmountOut.toString() ?? - crossSwapQuotes.bridgeQuote.outputAmount.toString(), - expectedFillTime: - crossSwapQuotes.bridgeQuote.suggestedFees.estimatedFillTimeSec, + destinationSwapQuote?.minAmountOut.toString() ?? + bridgeQuote.outputAmount.toString(), + expectedFillTime: bridgeQuote.suggestedFees.estimatedFillTimeSec, }; - mark.stop({ - crossSwapQuotes, - request, - }); + mark.stop(); logger.debug({ at: "Swap/allowance", message: "Response data", diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index ca07e01fb..125f48956 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -91,6 +91,7 @@ const enabledRoutes = { CHAIN_IDs.WORLD_CHAIN, ], }, + // Addresses of token-scoped `SwapAndBridge` contracts, i.e. USDC.e -> USDC swaps swapAndBridgeAddresses: { "1inch": { [CHAIN_IDs.POLYGON]: "0xaBa0F11D55C5dDC52cD0Cb2cd052B621d45159d5", @@ -104,6 +105,26 @@ const enabledRoutes = { [CHAIN_IDs.ARBITRUM]: "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D", }, }, + // Addresses of `UniversalSwapAndBridge` contracts from deployment: + // https://github.com/across-protocol/contracts/pull/731/commits/6bdbfd38f50b616ac25e49687cbac6fb6bcb543b + universalSwapAndBridgeAddresses: { + "1inch": { + [CHAIN_IDs.ARBITRUM]: "0x81C7601ac0c5825e89F967f9222B977CCD78aD77", + [CHAIN_IDs.BASE]: "0x98285D11B9F7aFec2d475805E5255f26B4490167", + [CHAIN_IDs.OPTIMISM]: "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", + [CHAIN_IDs.POLYGON]: "0xc2dcb88873e00c9d401de2cbba4c6a28f8a6e2c2", + }, + uniswap: { + [CHAIN_IDs.ARBITRUM]: "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb", + [CHAIN_IDs.BASE]: "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", + [CHAIN_IDs.BLAST]: "0x57EE47829369e2EF62fBb423648bec70d0366204", + [CHAIN_IDs.MAINNET]: "0x0e84f089B0923EfeA51C6dF91581BFBa66A3484A", + [CHAIN_IDs.OPTIMISM]: "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", + [CHAIN_IDs.POLYGON]: "0xa55490E20057BD4775618D0FC8D51F59f602FED0", + [CHAIN_IDs.WORLD_CHAIN]: "0x56e2d1b8C7dE8D11B282E1b4C924C32D91f9102B", + [CHAIN_IDs.ZORA]: "0x75b84707e6Bf5bc48DbC3AD883c23192C869AAE4", + }, + }, spokePoolPeripheryAddresses: { "uniswap-swapRouter02": { [CHAIN_IDs.POLYGON]: "0x8EB5FF2e23FD7789e59989aDe055A398800E394e", @@ -148,9 +169,12 @@ const enabledRoutes = { "0x17496824Ba574A4e9De80110A91207c4c63e552a", // Mocked }, }, - spokePoolPeripheryAddresses: { + universalSwapAndBridgeAddresses: { uniswap: {}, }, + spokePoolPeripheryAddresses: { + "uniswap-universalRouter": {}, + }, routes: transformChainConfigs(enabledSepoliaChainConfigs), }, } as const; @@ -373,35 +397,20 @@ async function generateRoutes(hubPoolChainId = 1) { ), merkleDistributorAddress: utils.getAddress(config.merkleDistributorAddress), claimAndStakeAddress: utils.getAddress(config.claimAndStakeAddress), - swapAndBridgeAddresses: Object.entries( - config.swapAndBridgeAddresses - ).reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: Object.entries(value).reduce( - (acc, [chainId, address]) => ({ - ...acc, - [chainId]: utils.getAddress(address as string), - }), - {} - ), - }), - {} + swapAndBridgeAddresses: checksumAddressesOfNestedMap( + config.swapAndBridgeAddresses as Record> ), - spokePoolPeripheryAddresses: Object.entries( - config.spokePoolPeripheryAddresses - ).reduce( - (acc, [key, value]) => ({ - ...acc, - [key]: Object.entries(value).reduce( - (acc, [chainId, address]) => ({ - ...acc, - [chainId]: utils.getAddress(address as string), - }), - {} - ), - }), - {} + universalSwapAndBridgeAddresses: checksumAddressesOfNestedMap( + config.universalSwapAndBridgeAddresses as Record< + string, + Record + > + ), + spokePoolPeripheryAddresses: checksumAddressesOfNestedMap( + config.spokePoolPeripheryAddresses as Record< + string, + Record + > ), routes: config.routes.flatMap((route) => transformBridgeRoute(route, config.hubPoolChain) @@ -656,4 +665,22 @@ function getBridgedUsdcSymbol(chainId: number) { } } +function checksumAddressesOfNestedMap( + nestedMap: Record> +) { + return Object.entries(nestedMap).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: Object.entries(value).reduce( + (acc, [chainId, address]) => ({ + ...acc, + [chainId]: utils.getAddress(address as string), + }), + {} + ), + }), + {} + ); +} + generateRoutes(Number(process.argv[2])); diff --git a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json index 5d7264fa4..c70c802f3 100644 --- a/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json +++ b/src/data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json @@ -13,9 +13,12 @@ "11155420": "0x17496824Ba574A4e9De80110A91207c4c63e552a" } }, - "spokePoolPeripheryAddresses": { + "universalSwapAndBridgeAddresses": { "uniswap": {} }, + "spokePoolPeripheryAddresses": { + "uniswap-universalRouter": {} + }, "routes": [ { "fromChain": 11155111, diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index 63b16f428..ed6a01693 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -20,6 +20,24 @@ "42161": "0xF633b72A4C2Fb73b77A379bf72864A825aD35b6D" } }, + "universalSwapAndBridgeAddresses": { + "1inch": { + "10": "0x7631eA29479Ee265241F13FB48555A2C886d3Bf8", + "137": "0xC2dCB88873E00c9d401De2CBBa4C6A28f8A6e2c2", + "8453": "0x98285D11B9F7aFec2d475805E5255f26B4490167", + "42161": "0x81C7601ac0c5825e89F967f9222B977CCD78aD77" + }, + "uniswap": { + "1": "0x0e84f089B0923EfeA51C6dF91581BFBa66A3484A", + "10": "0x04989eaF03547E6583f9d9e42aeD11D2b78A808b", + "137": "0xa55490E20057BD4775618D0FC8D51F59f602FED0", + "480": "0x56e2d1b8C7dE8D11B282E1b4C924C32D91f9102B", + "8453": "0xed8b9c9aE7aCEf12eb4650d26Eb876005a4752d2", + "42161": "0x2414A759d4EFF700Ad81e257Ab5187d07eCeEbAb", + "81457": "0x57EE47829369e2EF62fBb423648bec70d0366204", + "7777777": "0x75b84707e6Bf5bc48DbC3AD883c23192C869AAE4" + } + }, "spokePoolPeripheryAddresses": { "uniswap-swapRouter02": { "1": "0x8EB5FF2e23FD7789e59989aDe055A398800E394e",