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
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 @@ -227,6 +227,8 @@ export function getHyperCoreBridgeStrategy(): BridgeStrategy {
ecosystem: "evm",
};
},

isRouteSupported,
};
}

Expand Down
81 changes: 72 additions & 9 deletions api/_bridges/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { getAcrossBridgeStrategy } from "./across/strategy";
import { getHyperCoreBridgeStrategy } from "./hypercore/strategy";
import { BridgeStrategiesConfig } from "./types";
import {
BridgeStrategiesConfig,
BridgeStrategy,
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 @@ -13,16 +19,73 @@ 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 availableBridgeStrategies = [
getAcrossBridgeStrategy(),
getHyperCoreBridgeStrategy(),
getCctpBridgeStrategy(),
];

export async function getBridgeStrategy({
originChainId,
destinationChainId,
}: {
originChainId: number;
destinationChainId: number;
}) {
inputToken,
outputToken,
amount,
amountType,
recipient,
depositor,
logger,
}: GetBridgeStrategyParams): Promise<BridgeStrategy> {
const fromToChainOverride =
bridgeStrategies.fromToChains?.[originChainId]?.[destinationChainId];
return fromToChainOverride ?? bridgeStrategies.default;
if (fromToChainOverride) {
return fromToChainOverride;
}
const supportedBridgeStrategies = availableBridgeStrategies.filter(
(strategy) => strategy.isRouteSupported({ inputToken, outputToken })
);
if (supportedBridgeStrategies.length === 1) {
return supportedBridgeStrategies[0];
}
const bridgeStrategyData = await getBridgeStrategyData({
inputToken,
outputToken,
amount,
amountType,
recipient,
depositor,
logger,
});
if (!bridgeStrategyData) {
return bridgeStrategies.default;
}
if (!bridgeStrategyData.isUsdcToUsdc) {
return getAcrossBridgeStrategy();
}
if (bridgeStrategyData.isUtilizationHigh) {
return getCctpBridgeStrategy();
}
Comment on lines +65 to +67
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?

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 {
// Use OFT bridge if not CCTP
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.

}
}
}
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?


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;
104 changes: 104 additions & 0 deletions api/_bridges/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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";

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 utilizationThreshold = sdk.utils.fixedPointAdjustment.mul(80).div(100); // 80%

// Calculate current utilization percentage
const currentUtilization = _utilizedReserves
Copy link
Contributor

Choose a reason for hiding this comment

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

I think utilizedReserves can be negative, would that have any impact in this calculation?
I see in other places we floored it to zero when that happens. See for example: https://github.com/across-protocol/frontend/blob/master/api/_utils.ts#L2709

.mul(sdk.utils.fixedPointAdjustment)
.div(_liquidReserves.add(_utilizedReserves));

return currentUtilization.gt(utilizationThreshold);
}

/**
* Fetches bridge limits and utilization data in parallel to determine strategy requirements
*/
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 <= 10_000; // 10K USD
const isLargeDeposit = depositAmountUsd > 1_000_000; // 1M USD

// 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) ||
fastCctpChains.includes(outputToken.chainId);
Comment on lines +77 to +79
Copy link
Contributor

Choose a reason for hiding this comment

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

I think only the origin chain matters here.

const isFastCctpEligible = isFastCctpChain && depositAmountUsd > 10_000; // 10K USD

// Check if Linea is the source chain
const isLineaSource = inputToken.chainId === CHAIN_IDs.LINEA;

return {
canFillInstantly,
isUtilizationHigh,
isUsdcToUsdc,
isLargeDeposit,
isInThreshold,
isFastCctpEligible,
isLineaSource,
};
} catch (error) {
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;
}
}
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 @@ -51,7 +51,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 @@ -1043,21 +1048,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
7 changes: 6 additions & 1 deletion api/limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,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];
Expand Down Expand Up @@ -557,6 +558,10 @@ const handler = async (
tokenGasCost: gasFeeDetails.tokenGasCost.toString(),
}
: undefined,
reserves: {
liquidReserves: String(_liquidReserves),
utilizedReserves: String(utilizedReserves),
},
};
logger.debug({
at: "Limits",
Expand Down
12 changes: 9 additions & 3 deletions api/swap/approval/_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,18 @@ 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,
logger,
});

const crossSwapQuotes = await getCrossSwapQuotes(
{
amount,
Expand Down