Skip to content
Merged
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
4 changes: 4 additions & 0 deletions api/_bridges/across/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,9 @@ export function getAcrossBridgeStrategy(): BridgeStrategy {
);
return tx;
},

isRouteSupported: () => {
return true;
},
};
}
2 changes: 2 additions & 0 deletions api/_bridges/cctp/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ export function getCctpBridgeStrategy(): BridgeStrategy {
ecosystem: "evm" as const,
};
},

isRouteSupported,
};
}

Expand Down
2 changes: 2 additions & 0 deletions api/_bridges/hypercore/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ export function getHyperCoreBridgeStrategy(): BridgeStrategy {
ecosystem: "evm",
};
},

isRouteSupported,
};
}

Expand Down
103 changes: 94 additions & 9 deletions api/_bridges/index.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -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<BridgeStrategy> {
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<BridgeStrategy> {
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();
}
Comment on lines +91 to +93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when utilization is high CCTP strategy should be used only for USDC, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we won't ever reach this line unless we're bridging USDC to USDC. See line 65 for the early exit clause.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for all these checks, should we check if the strategy we want to return is part of the supportedBridgeStrategies list? That way we avoid returning here a strategy that was previously filtered out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored it a bit to achieve this. It will help us when we add the OFT strategy as well.

}
}
}
1 change: 1 addition & 0 deletions api/_bridges/oft/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ export function getOftBridgeStrategy(): BridgeStrategy {
ecosystem: "evm" as const,
};
},
isRouteSupported,
};
}

Expand Down
33 changes: 33 additions & 0 deletions api/_bridges/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we have some utils in api/_logger.ts. Can we use getLogger from there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I checked and that's also using the same one: https://github.com/across-protocol/frontend/blob/master/api/_logger.ts#L8

In this file, I'm only using the type Logger, not the actual logger instance.


export type BridgeStrategiesConfig = {
default: BridgeStrategy;
Expand Down Expand Up @@ -92,4 +93,36 @@ export type BridgeStrategy = {
quotes: CrossSwapQuotes;
integratorId?: string;
}) => Promise<OriginTx>;

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;
131 changes: 131 additions & 0 deletions api/_bridges/utils.ts
Original file line number Diff line number Diff line change
@@ -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<BridgeStrategyData> {
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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be part of the CCTP bridge strategy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for this use case we are already exporting CCTP_FILL_TIME_ESTIMATES from the strategy. Maybe we can use that combined with a threshold.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fast CCTP chains can be taken from here. Chains with "seconds" are considered fast

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could instead return the expected JSON object but all fields set to false?
This would be a more clear return type than undefined.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point, that's how I initially had it but decided to give a nullable return type instead because:

I think it makes sense to keep each field nullable like you said if we decide to support partial fetching of bridge strategy data, where none, some, or all of the fields can nullable. Let me know what you think though.

Copy link
Contributor

@NikolasHaimerl NikolasHaimerl Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we show this behaviour in the JS doc string of the function? As a user of this function it is not immediately clear what undefined means here. Undefined can mean anything if the function does not explicitly state its meaning. And then we have to be careful, that the function can never return undefined at any other point of the function as it now has a distinct meaning.

I'll leave the decision up to you. Usually, I would argue it makes sense to make things either intuitive/the code is documentation enough or specify the behaviour as stringently as possible to avoid ambiguity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can add some docs. My reasoning is that each of the fields individually should never be undefined. We either have the bridge strategy data or we don't - undefined just means that we don't.

If we make all the fields in the type nullable, the structure technically feels wrong. Another option is to take the undefined out of the type and keep it in the function return type.

}
}
1 change: 1 addition & 0 deletions api/_types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./generic.types";
export * from "./utility.types";
export * from "./response.types";
19 changes: 19 additions & 0 deletions api/_types/response.types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
};
23 changes: 7 additions & 16 deletions api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<LimitsResponse> => {
const messageTooLong = isMessageTooLong(message ?? "");

const params = {
Expand Down
Loading