-
Notifications
You must be signed in to change notification settings - Fork 63
feat: mint + burn routing logic #1864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/mint-burn-bridge-routing
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(), | ||
|
@@ -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(); | ||
} | ||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see we have some utils in |
||
|
||
export type BridgeStrategiesConfig = { | ||
default: BridgeStrategy; | ||
|
@@ -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; |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
.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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be part of the CCTP bridge strategy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, for this use case we are already exporting There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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"; |
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; | ||
}; | ||
}; |
There was a problem hiding this comment.
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?