-
Notifications
You must be signed in to change notification settings - Fork 65
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
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,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<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(); | ||
} | ||
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(); | ||
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 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. Refactored it a bit to achieve this. It will help us when we add the OFT strategy as well.
NikolasHaimerl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} |
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 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. 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 |
||
|
||
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,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]; | ||
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); | ||
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; | ||
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. We could instead return the expected JSON object but all fields set to false? 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. 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. 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. 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. 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. 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 |
||
} | ||
} |
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?
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.
I think we won't ever reach this line unless we're bridging USDC to USDC. See line 65 for the early exit clause.