diff --git a/api/_bridges/across/strategy.ts b/api/_bridges/across/strategy.ts index e716328ba..f28682ceb 100644 --- a/api/_bridges/across/strategy.ts +++ b/api/_bridges/across/strategy.ts @@ -161,5 +161,9 @@ export function getAcrossBridgeStrategy(): BridgeStrategy { ); return tx; }, + + isRouteSupported: () => { + return true; + }, }; } diff --git a/api/_bridges/cctp/strategy.ts b/api/_bridges/cctp/strategy.ts index e19f8e2b0..c9e619533 100644 --- a/api/_bridges/cctp/strategy.ts +++ b/api/_bridges/cctp/strategy.ts @@ -241,6 +241,8 @@ export function getCctpBridgeStrategy(): BridgeStrategy { ecosystem: "evm" as const, }; }, + + isRouteSupported, }; } diff --git a/api/_bridges/hypercore/strategy.ts b/api/_bridges/hypercore/strategy.ts index c51af5a78..4ae1795c4 100644 --- a/api/_bridges/hypercore/strategy.ts +++ b/api/_bridges/hypercore/strategy.ts @@ -271,6 +271,8 @@ export function getHyperCoreBridgeStrategy(): BridgeStrategy { ecosystem: "evm", }; }, + + isRouteSupported, }; } diff --git a/api/_bridges/index.ts b/api/_bridges/index.ts index f4908aed5..c933b3c83 100644 --- a/api/_bridges/index.ts +++ b/api/_bridges/index.ts @@ -1,7 +1,14 @@ import { getAcrossBridgeStrategy } from "./across/strategy"; import { getHyperCoreBridgeStrategy } from "./hypercore/strategy"; -import { BridgeStrategiesConfig } from "./types"; +import { + BridgeStrategiesConfig, + BridgeStrategy, + BridgeStrategyDataParams, + GetBridgeStrategyParams, +} from "./types"; import { CHAIN_IDs } from "../_constants"; +import { getCctpBridgeStrategy } from "./cctp/strategy"; +import { getBridgeStrategyData } from "./utils"; export const bridgeStrategies: BridgeStrategiesConfig = { default: getAcrossBridgeStrategy(), @@ -16,16 +23,94 @@ export const bridgeStrategies: BridgeStrategiesConfig = { // TODO: Add CCTP routes when ready }; -// TODO: Extend the strategy selection based on more sophisticated logic when we start -// implementing burn/mint bridges. -export function getBridgeStrategy({ +export const routableBridgeStrategies = [ + getAcrossBridgeStrategy(), + // TODO: Add CCTP bridge strategy when ready +]; + +export async function getBridgeStrategy({ originChainId, destinationChainId, -}: { - originChainId: number; - destinationChainId: number; -}) { + inputToken, + outputToken, + amount, + amountType, + recipient, + depositor, +}: GetBridgeStrategyParams): Promise { const fromToChainOverride = bridgeStrategies.fromToChains?.[originChainId]?.[destinationChainId]; - return fromToChainOverride ?? bridgeStrategies.default; + if (fromToChainOverride) { + return fromToChainOverride; + } + const supportedBridgeStrategies = routableBridgeStrategies.filter( + (strategy) => strategy.isRouteSupported({ inputToken, outputToken }) + ); + if (supportedBridgeStrategies.length === 1) { + return supportedBridgeStrategies[0]; + } + if ( + supportedBridgeStrategies.some( + (strategy) => strategy.name === getCctpBridgeStrategy().name + ) + ) { + return routeStrategyForCctp({ + inputToken, + outputToken, + amount, + amountType, + recipient, + depositor, + }); + } + return getAcrossBridgeStrategy(); +} + +async function routeStrategyForCctp({ + inputToken, + outputToken, + amount, + amountType, + recipient, + depositor, +}: BridgeStrategyDataParams): Promise { + const bridgeStrategyData = await getBridgeStrategyData({ + inputToken, + outputToken, + amount, + amountType, + recipient, + depositor, + }); + if (!bridgeStrategyData) { + return bridgeStrategies.default; + } + if (!bridgeStrategyData.isUsdcToUsdc) { + return getAcrossBridgeStrategy(); + } + if (bridgeStrategyData.isUtilizationHigh) { + return getCctpBridgeStrategy(); + } + if (bridgeStrategyData.isLineaSource) { + return getAcrossBridgeStrategy(); + } + if (bridgeStrategyData.isFastCctpEligible) { + if (bridgeStrategyData.isInThreshold) { + return getAcrossBridgeStrategy(); + } + if (bridgeStrategyData.isLargeDeposit) { + return getAcrossBridgeStrategy(); + } else { + return getCctpBridgeStrategy(); + } + } + if (bridgeStrategyData.canFillInstantly) { + return getAcrossBridgeStrategy(); + } else { + if (bridgeStrategyData.isLargeDeposit) { + return getAcrossBridgeStrategy(); + } else { + return getCctpBridgeStrategy(); + } + } } diff --git a/api/_bridges/oft/strategy.ts b/api/_bridges/oft/strategy.ts index b1ee2afdb..199edf77c 100644 --- a/api/_bridges/oft/strategy.ts +++ b/api/_bridges/oft/strategy.ts @@ -487,6 +487,7 @@ export function getOftBridgeStrategy(): BridgeStrategy { ecosystem: "evm" as const, }; }, + isRouteSupported, }; } diff --git a/api/_bridges/types.ts b/api/_bridges/types.ts index f941c6d47..0ff69c680 100644 --- a/api/_bridges/types.ts +++ b/api/_bridges/types.ts @@ -2,6 +2,7 @@ import { BigNumber } from "ethers"; import { CrossSwap, CrossSwapQuotes, Token } from "../_dexes/types"; import { AppFee, CrossSwapType } from "../_dexes/utils"; +import { Logger } from "@across-protocol/sdk/dist/types/relayFeeCalculator"; export type BridgeStrategiesConfig = { default: BridgeStrategy; @@ -92,4 +93,36 @@ export type BridgeStrategy = { quotes: CrossSwapQuotes; integratorId?: string; }) => Promise; + + isRouteSupported: (params: { + inputToken: Token; + outputToken: Token; + }) => boolean; +}; + +export type BridgeStrategyData = + | { + canFillInstantly: boolean; + isUtilizationHigh: boolean; + isUsdcToUsdc: boolean; + isLargeDeposit: boolean; + isFastCctpEligible: boolean; + isLineaSource: boolean; + isInThreshold: boolean; + } + | undefined; + +export type BridgeStrategyDataParams = { + inputToken: Token; + outputToken: Token; + amount: BigNumber; + amountType: "exactInput" | "exactOutput" | "minOutput"; + recipient?: string; + depositor: string; + logger?: Logger; }; + +export type GetBridgeStrategyParams = { + originChainId: number; + destinationChainId: number; +} & BridgeStrategyDataParams; diff --git a/api/_bridges/utils.ts b/api/_bridges/utils.ts new file mode 100644 index 000000000..d1cd303a4 --- /dev/null +++ b/api/_bridges/utils.ts @@ -0,0 +1,131 @@ +import { BigNumber, ethers } from "ethers"; +import { LimitsResponse } from "../_types"; +import * as sdk from "@across-protocol/sdk"; +import { getCachedLimits, ConvertDecimals } from "../_utils"; +import { CHAIN_IDs } from "../_constants"; +import { + BridgeStrategyData, + BridgeStrategyDataParams, +} from "../_bridges/types"; + +const ACROSS_THRESHOLD = 10_000; // 10K USD +const LARGE_DEPOSIT_THRESHOLD = 1_000_000; // 1M USD + +export function isFullyUtilized(limits: LimitsResponse): boolean { + // Check if utilization is high (>80%) + const { liquidReserves, utilizedReserves } = limits.reserves; + const _liquidReserves = BigNumber.from(liquidReserves); + const _utilizedReserves = BigNumber.from(utilizedReserves); + const flooredUtilizedReserves = _utilizedReserves.gt(0) + ? _utilizedReserves + : BigNumber.from(0); + + const utilizationThreshold = sdk.utils.fixedPointAdjustment.mul(80).div(100); // 80% + + // Calculate current utilization percentage + const currentUtilization = flooredUtilizedReserves + .mul(sdk.utils.fixedPointAdjustment) + .div(_liquidReserves.add(flooredUtilizedReserves)); + + return currentUtilization.gt(utilizationThreshold); +} + +/** + * Fetches bridge limits and utilization data to determine routing strategy requirements. + * Analyzes various factors including utilization rates, deposit amounts, token types, + * and chain-specific eligibility to determine the optimal bridge strategy. + * + * @param params - The bridge strategy data parameters + * @param params.inputToken - The input token for the bridge transaction + * @param params.outputToken - The output token for the bridge transaction + * @param params.amount - The amount to bridge (in wei) + * @param params.amountType - The type of amount (exactInput, exactOutput, minOutput) + * @param params.recipient - The recipient address (optional) + * @param params.depositor - The depositor address + * @param params.logger - Optional logger instance for error reporting + * @returns Promise resolving to bridge strategy data or undefined if fetch fails + * @returns Returns object containing strategy flags: + * - canFillInstantly: Whether the bridge can fill the deposit instantly + * - isUtilizationHigh: Whether bridge utilization is above 80% threshold + * - isUsdcToUsdc: Whether both input and output tokens are USDC + * - isLargeDeposit: Whether deposit amount exceeds 1M USD threshold + * - isInThreshold: Whether deposit is within 10K USD Across threshold + * - isFastCctpEligible: Whether eligible for Fast CCTP on supported chains + * - isLineaSource: Whether the source chain is Linea + */ +export async function getBridgeStrategyData({ + inputToken, + outputToken, + amount, + amountType, + recipient, + depositor, + logger, +}: BridgeStrategyDataParams): Promise { + try { + const limits = await getCachedLimits( + inputToken.address, + outputToken.address, + inputToken.chainId, + outputToken.chainId, + recipient || depositor + ); + + // Convert amount to input token decimals if it's in output token decimals + let amountInInputTokenDecimals = amount; + if (amountType === "exactOutput" || amountType === "minOutput") { + amountInInputTokenDecimals = ConvertDecimals( + outputToken.decimals, + inputToken.decimals + )(amount); + } + + // Check if we can fill instantly + const maxDepositInstant = BigNumber.from(limits.maxDepositInstant); + const canFillInstantly = amountInInputTokenDecimals.lte(maxDepositInstant); + + // Check if bridge is fully utilized + const isUtilizationHigh = isFullyUtilized(limits); + + // Check if input and output tokens are both USDC + const isUsdcToUsdc = + inputToken.symbol === "USDC" && outputToken.symbol === "USDC"; + + // Check if deposit is > 1M USD or within Across threshold + const depositAmountUsd = parseFloat( + ethers.utils.formatUnits(amountInInputTokenDecimals, inputToken.decimals) + ); + const isInThreshold = depositAmountUsd <= ACROSS_THRESHOLD; + const isLargeDeposit = depositAmountUsd > LARGE_DEPOSIT_THRESHOLD; + + // Check if eligible for Fast CCTP (Polygon, BSC, Solana) and deposit > 10K USD + const fastCctpChains = [CHAIN_IDs.POLYGON, CHAIN_IDs.BSC, CHAIN_IDs.SOLANA]; + const isFastCctpChain = fastCctpChains.includes(inputToken.chainId); + const isFastCctpEligible = + isFastCctpChain && depositAmountUsd > ACROSS_THRESHOLD; + + // Check if Linea is the source chain + const isLineaSource = inputToken.chainId === CHAIN_IDs.LINEA; + + return { + canFillInstantly, + isUtilizationHigh, + isUsdcToUsdc, + isLargeDeposit, + isInThreshold, + isFastCctpEligible, + isLineaSource, + }; + } catch (error) { + if (logger) { + logger.warn({ + at: "getBridgeStrategyData", + message: "Failed to fetch bridge strategy data, using defaults", + error: error instanceof Error ? error.message : String(error), + }); + } + + // Safely return undefined if we can't fetch bridge strategy data + return undefined; + } +} diff --git a/api/_types/index.ts b/api/_types/index.ts index 6723d1603..f6c320b94 100644 --- a/api/_types/index.ts +++ b/api/_types/index.ts @@ -1,2 +1,3 @@ export * from "./generic.types"; export * from "./utility.types"; +export * from "./response.types"; diff --git a/api/_types/response.types.ts b/api/_types/response.types.ts new file mode 100644 index 000000000..966840fca --- /dev/null +++ b/api/_types/response.types.ts @@ -0,0 +1,19 @@ +export type LimitsResponse = { + minDeposit: string; + maxDeposit: string; + maxDepositInstant: string; + maxDepositShortDelay: string; + recommendedDepositInstant: string; + relayerFeeDetails: { + relayFeeTotal: string; + relayFeePercent: string; + capitalFeePercent: string; + capitalFeeTotal: string; + gasFeePercent: string; + gasFeeTotal: string; + }; + reserves: { + liquidReserves: string; + utilizedReserves: string; + }; +}; diff --git a/api/_utils.ts b/api/_utils.ts index e3b7eb230..0d5db3086 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -52,7 +52,12 @@ import { relayerFeeCapitalCostConfig, TOKEN_EQUIVALENCE_REMAPPING, } from "./_constants"; -import { PoolStateOfUser, PoolStateResult, TokenInfo } from "./_types"; +import { + LimitsResponse, + PoolStateOfUser, + PoolStateResult, + TokenInfo, +} from "./_types"; import { buildInternalCacheKey, getCachedValue, @@ -958,21 +963,7 @@ export const getCachedLimits = async ( relayer?: string, message?: string, allowUnmatchedDecimals?: boolean -): Promise<{ - minDeposit: string; - maxDeposit: string; - maxDepositInstant: string; - maxDepositShortDelay: string; - recommendedDepositInstant: string; - relayerFeeDetails: { - relayFeeTotal: string; - relayFeePercent: string; - capitalFeePercent: string; - capitalFeeTotal: string; - gasFeePercent: string; - gasFeeTotal: string; - }; -}> => { +): Promise => { const messageTooLong = isMessageTooLong(message ?? ""); const params = { diff --git a/api/limits.ts b/api/limits.ts index a1c7f61dd..792bafd06 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -332,7 +332,8 @@ const handler = async ( relayerFeeDetails, }); - const { liquidReserves: _liquidReserves } = multicallOutput[1]; + const { liquidReserves: _liquidReserves, utilizedReserves } = + multicallOutput[1]; const [liteChainIdsEncoded] = multicallOutput[2]; const [poolRebalanceRouteOrigin] = multicallOutput[3]; const [poolRebalanceRouteDestination] = multicallOutput[4]; @@ -563,6 +564,10 @@ const handler = async ( tokenGasCost: gasFeeDetails.tokenGasCost.toString(), } : undefined, + reserves: { + liquidReserves: String(_liquidReserves), + utilizedReserves: String(utilizedReserves), + }, }; logger.debug({ at: "Limits", diff --git a/api/swap/approval/_service.ts b/api/swap/approval/_service.ts index d0a85a3f2..9228b0ef1 100644 --- a/api/swap/approval/_service.ts +++ b/api/swap/approval/_service.ts @@ -79,12 +79,17 @@ export async function handleApprovalSwap( const slippageTolerance = _slippageTolerance ?? slippage * 100; - // TODO: Extend the strategy selection based on more sophisticated logic when we start - // implementing burn/mint bridges. - const bridgeStrategy = getBridgeStrategy({ + const bridgeStrategy = await getBridgeStrategy({ originChainId: inputToken.chainId, destinationChainId: outputToken.chainId, + inputToken, + outputToken, + amount, + amountType, + recipient, + depositor, }); + const crossSwapQuotes = await getCrossSwapQuotes( { amount,