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: 2 additions & 2 deletions api/_bridges/cctp-sponsored/utils/final-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export function getSponsoredCctpFinalTokenAddress(
SPONSORED_CCTP_FINAL_TOKEN_PER_OUTPUT_TOKEN[outputTokenSymbol];
if (!finalToken) {
throw new Error(
`'finalToken' not found for output token ${outputTokenSymbol}`
`Sponsored CCTP 'finalToken' not found for output token ${outputTokenSymbol}`
);
}
const finalTokenAddress = finalToken.addresses[intermediaryChainId];
if (!finalTokenAddress) {
throw new Error(
`'finalTokenAddress' not found for ${finalToken.symbol} on chain ${intermediaryChainId}`
`Sponsored CCTP 'finalTokenAddress' not found for ${finalToken.symbol} on chain ${intermediaryChainId}`
);
}
return finalTokenAddress;
Expand Down
8 changes: 8 additions & 0 deletions api/_bridges/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getCctpBridgeStrategy } from "./cctp/strategy";
import { routeStrategyForCctp } from "./cctp/utils/routing";
import { routeStrategyForSponsorship } from "../_sponsorship-routing";
import { getSponsoredCctpBridgeStrategy } from "./cctp-sponsored/strategy";
import { getOftSponsoredBridgeStrategy } from "./oft-sponsored/strategy";

export const bridgeStrategies: BridgeStrategiesConfig = {
default: getAcrossBridgeStrategy(),
Expand All @@ -23,6 +24,13 @@ export const bridgeStrategies: BridgeStrategiesConfig = {
},
},
inputTokens: {
USDT: {
// @TODO: Remove this once we can correctly route via eligibility checks.
// Currently we are using hardcoded true for eligibility checks.
[CHAIN_IDs.ARBITRUM]: {
[CHAIN_IDs.HYPERCORE]: getOftSponsoredBridgeStrategy(true),
},
},
USDC: {
// Testnet routes
[CHAIN_IDs.HYPEREVM_TESTNET]: {
Expand Down
62 changes: 52 additions & 10 deletions api/_bridges/oft-sponsored/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getEstimatedFillTime,
getOftBridgeFees,
getQuote,
getSponsoredOftComposerMessageForQuoting,
roundAmountToSharedDecimals,
} from "../oft/utils/shared";
import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../_constants";
Expand All @@ -23,7 +24,11 @@ import {
import { InvalidParamError } from "../../_errors";
import { simulateMarketOrder, SPOT_TOKEN_DECIMALS } from "../../_hypercore";
import { tagIntegratorId, tagSwapApiMarker } from "../../_integrator-id";
import { ConvertDecimals, getCachedTokenInfo } from "../../_utils";
import {
addMarkupToAmount,
ConvertDecimals,
getCachedTokenInfo,
} from "../../_utils";
import { getNativeTokenInfo } from "../../_token-info";
import { SPONSORED_OFT_SRC_PERIPHERY_ABI } from "./utils/abi";
import {
Expand All @@ -35,6 +40,12 @@ import {
} from "./utils/constants";
import { buildSponsoredOFTQuote } from "./utils/quote-builder";
import { getSlippage } from "../../_slippage";
import { ExecutionMode, generateQuoteNonce } from "../../_sponsorship-utils";
import { toBytes32 } from "../../_address";

// We add some markup to the native fee for the initial quote to account for gas price
// volatility. Expressed as a percentage, e.g., 0.01 = 1%.
const INITIAL_QUOTE_NATIVE_FEE_MARKUP = 0.1;

const name = "sponsored-oft" as const;

Expand Down Expand Up @@ -126,6 +137,19 @@ export async function getSponsoredOftQuoteForExactInput(
// All sponsored OFT transfers route through HyperEVM USDT before reaching final destination
const intermediaryToken = await getIntermediaryToken();

// The following composer options are used for the initial quote and are not used for
// the final quote execution.
const composerOptsForInitialQuote = getSponsoredOftComposerMessageForQuoting({
nonce: generateQuoteNonce(params.recipient!),
deadline: Math.floor(Date.now() / 1000),
maxBpsToSponsor: BigNumber.from(0),
maxUserSlippageBps: BigNumber.from(0),
finalRecipient: toBytes32(recipient!),
finalToken: toBytes32(intermediaryToken.address),
executionMode: ExecutionMode.Default,
actionData: "0x",
});

// Get OFT quote to intermediary token and estimated fill time
const [{ inputAmount, outputAmount, nativeFee }, estimatedFillTimeSec] =
await Promise.all([
Expand All @@ -134,6 +158,11 @@ export async function getSponsoredOftQuoteForExactInput(
outputToken: intermediaryToken,
inputAmount: exactInputAmount,
recipient: recipient!,
composerOpts: {
toAddress: recipient!,
composeMsg: composerOptsForInitialQuote.composeMsg,
extraOptions: composerOptsForInitialQuote.extraOptions,
},
}),
getEstimatedFillTime(
inputToken.chainId,
Expand Down Expand Up @@ -161,7 +190,10 @@ export async function getSponsoredOftQuoteForExactInput(
provider: name,
fees: getOftBridgeFees({
inputToken,
nativeFee,
nativeFee: addMarkupToAmount(
nativeFee,
INITIAL_QUOTE_NATIVE_FEE_MARKUP
),
nativeToken,
}),
},
Expand Down Expand Up @@ -310,8 +342,10 @@ async function buildTransaction(params: {
crossSwap: CrossSwap;
bridgeQuote: CrossSwapQuotes["bridgeQuote"];
integratorId?: string;
isEligibleForSponsorship: boolean;
}) {
const { crossSwap, bridgeQuote, integratorId } = params;
const { crossSwap, bridgeQuote, integratorId, isEligibleForSponsorship } =
params;

const originChainId = crossSwap.inputToken.chainId;

Expand All @@ -323,12 +357,17 @@ async function buildTransaction(params: {
});
}

// Calculate maxBpsToSponsor based on output token and market simulation
const maxBpsToSponsor = await calculateMaxBpsToSponsor({
outputTokenSymbol: crossSwap.outputToken.symbol,
bridgeInputAmount: bridgeQuote.inputAmount,
bridgeOutputAmount: bridgeQuote.outputAmount,
});
const maxBpsToSponsor = isEligibleForSponsorship
? // If eligible for sponsorship, we calculate the maxBpsToSponsor based on output
// token and market simulation.
await calculateMaxBpsToSponsor({
outputTokenSymbol: crossSwap.outputToken.symbol,
bridgeInputAmount: bridgeQuote.inputAmount,
bridgeOutputAmount: bridgeQuote.outputAmount,
})
: // If not eligible for sponsorship, we use 0 bps as maxBpsToSponsor. This will
// trigger the un-sponsored flow in the destination periphery contract.
BigNumber.from(0);

// Convert slippage tolerance to bps (slippageTolerance is a decimal, e.g., 0.5 = 0.5% = 50 bps)
const maxUserSlippageBps = Math.floor(
Expand Down Expand Up @@ -378,7 +417,9 @@ async function buildTransaction(params: {
/**
* OFT sponsored bridge strategy
*/
export function getOftSponsoredBridgeStrategy(): BridgeStrategy {
export function getOftSponsoredBridgeStrategy(
isEligibleForSponsorship: boolean
): BridgeStrategy {
return {
name,
capabilities,
Expand Down Expand Up @@ -443,6 +484,7 @@ export function getOftSponsoredBridgeStrategy(): BridgeStrategy {
crossSwap,
bridgeQuote,
integratorId: params.integratorId,
isEligibleForSponsorship,
});
},
};
Expand Down
21 changes: 15 additions & 6 deletions api/_bridges/oft-sponsored/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { CHAIN_IDs } from "../../../_constants";
import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../../_constants";

/**
* SponsoredOFTSrcPeriphery contract addresses per chain
* TODO: Update with actual deployed addresses
*/
export const SPONSORED_OFT_SRC_PERIPHERY: Record<number, string | undefined> = {
[CHAIN_IDs.MAINNET]: "0x0000000000000000000000000000000000000000", // TODO
[CHAIN_IDs.ARBITRUM]: "0x1235Ac1010FeeC8ae22744f323416cBBE37feDbE", // TODO
[CHAIN_IDs.HYPEREVM]: "0x0000000000000000000000000000000000000000", // TODO
[CHAIN_IDs.POLYGON]: "0x0000000000000000000000000000000000000000", // TODO
[CHAIN_IDs.MAINNET]: "0xE35d1205a523B699785967FFfe99b72059B46707",
[CHAIN_IDs.ARBITRUM]: "0xcC0A3e41304c43BD13f520d300f0c2F8B17ABe7B",
};

/**
* DstOFTHandler contract addresses per chain
* TODO: Update with actual deployed addresses
*/
export const DST_OFT_HANDLER: Record<number, string | undefined> = {
[CHAIN_IDs.HYPEREVM]: "0x0000000000000000000000000000000000000000", // TODO
[CHAIN_IDs.HYPEREVM]: "0xd9f40794367a2EcB0B409Ca8DBc55345c0dB6E9F",
};

/**
Expand All @@ -40,3 +38,14 @@ export const SPONSORED_OFT_OUTPUT_TOKENS = ["USDT-SPOT", "USDC"];
* Supported destination chains for sponsored OFT flows
*/
export const SPONSORED_OFT_DESTINATION_CHAINS = [CHAIN_IDs.HYPERCORE];

/**
* Final token per output token for sponsored OFT flows
*/
export const SPONSORED_OFT_FINAL_TOKEN_PER_OUTPUT_TOKEN: Record<
string,
(typeof TOKEN_SYMBOLS_MAP)[keyof typeof TOKEN_SYMBOLS_MAP]
> = {
USDC: TOKEN_SYMBOLS_MAP.USDC,
"USDT-SPOT": TOKEN_SYMBOLS_MAP.USDT,
};
21 changes: 21 additions & 0 deletions api/_bridges/oft-sponsored/utils/final-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SPONSORED_OFT_FINAL_TOKEN_PER_OUTPUT_TOKEN } from "./constants";

export function getSponsoredOftFinalTokenAddress(
outputTokenSymbol: string,
intermediaryChainId: number
) {
const finalToken =
SPONSORED_OFT_FINAL_TOKEN_PER_OUTPUT_TOKEN[outputTokenSymbol];
if (!finalToken) {
throw new Error(
`Sponsored OFT 'finalToken' not found for output token ${outputTokenSymbol}`
);
}
const finalTokenAddress = finalToken.addresses[intermediaryChainId];
if (!finalTokenAddress) {
throw new Error(
`Sponsored OFT 'finalTokenAddress' not found for ${finalToken.symbol} on chain ${intermediaryChainId}`
);
}
return finalTokenAddress;
}
5 changes: 4 additions & 1 deletion api/_bridges/oft-sponsored/utils/quote-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DEFAULT_QUOTE_EXPIRY_SECONDS,
ExecutionMode,
} from "../../../_sponsorship-utils";
import { getSponsoredOftFinalTokenAddress } from "./final-token";

/**
* Builds a complete sponsored OFT quote with signature
Expand Down Expand Up @@ -70,7 +71,9 @@ export function buildSponsoredOFTQuote(params: BuildSponsoredQuoteParams): {
deadline,
maxBpsToSponsor,
finalRecipient: toBytes32(recipient),
finalToken: toBytes32(outputToken.address),
finalToken: toBytes32(
getSponsoredOftFinalTokenAddress(outputToken.symbol, intermediaryChainId)
),
lzReceiveGasLimit: DEFAULT_LZ_RECEIVE_GAS_LIMIT,
lzComposeGasLimit: DEFAULT_LZ_COMPOSE_GAS_LIMIT,
executionMode: ExecutionMode.Default, // Default HyperCore flow
Expand Down
66 changes: 60 additions & 6 deletions api/_bridges/oft/utils/shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber, Contract, ethers } from "ethers";
import { BigNumber, BigNumberish, Contract, ethers } from "ethers";
import { CHAIN_IDs } from "../../../_constants";
import { Token } from "../../../_dexes/types";
import { InvalidParamError } from "../../../_errors";
Expand Down Expand Up @@ -122,6 +122,52 @@ export async function getRequiredDVNCount(
}
}

/**
* Composes the message for a sponsored OFT transfer. This differs from the message
* returned by `getHyperLiquidComposerMessage` in that it encodes the message as
* required by the `SponsoredOFTSrcPeriphery.sol` contract and not the `HyperliquidComposer.sol`
* contract from LayerZero.
* @param params The parameters for a sponsored OFT transfer.
*/
export function getSponsoredOftComposerMessageForQuoting(params: {
nonce: string;
deadline: BigNumberish;
maxBpsToSponsor: BigNumberish;
maxUserSlippageBps: BigNumberish;
finalRecipient: string;
finalToken: string;
executionMode: number;
actionData: string;
}) {
const composeMsg = ethers.utils.defaultAbiCoder.encode(
[
"bytes32",
"uint256",
"uint256",
"uint256",
"bytes32",
"bytes32",
"uint8",
"bytes",
],
[
params.nonce,
params.deadline,
params.maxBpsToSponsor,
params.maxUserSlippageBps,
params.finalRecipient,
params.finalToken,
params.executionMode,
params.actionData,
]
);
// Encoded `lzReceiveGasLimit` and `lzComposeGasLimit`. We can use hardcoded values
// as we use this message for quoting only.
const extraOptions =
"0x000301001101000000000000000000000000000186A0010013030000000000000000000000000000000186A0010011010000000000000000000000000002AB98010013030000000000000000000000000000000493E0";
return { composeMsg, extraOptions };
}

/**
* Composes the message for a Hyperliquid transfer.
* This function creates the `composeMsg` and `extraOptions` for a Hyperliquid transfer.
Expand Down Expand Up @@ -229,8 +275,20 @@ export async function getQuote(params: {
outputToken: Token;
inputAmount: BigNumber;
recipient: string;
composerOpts?: {
toAddress: string;
composeMsg: string;
extraOptions: string;
};
}) {
const { inputToken, outputToken, inputAmount, recipient } = params;
const { inputToken, outputToken, inputAmount, recipient, composerOpts } =
params;

const { toAddress, composeMsg, extraOptions } = composerOpts
? composerOpts
: outputToken.chainId === CHAIN_IDs.HYPERCORE
? getHyperLiquidComposerMessage(recipient, outputToken.symbol)
: { toAddress: recipient, composeMsg: "0x", extraOptions: "0x" };

// Get OFT messenger contract
const oftMessengerAddress = getOftMessengerForToken(
Expand All @@ -251,10 +309,6 @@ export async function getQuote(params: {
inputToken.symbol,
inputToken.decimals
);
const { toAddress, composeMsg, extraOptions } =
outputToken.chainId === CHAIN_IDs.HYPERCORE
? getHyperLiquidComposerMessage(recipient, outputToken.symbol)
: { toAddress: recipient, composeMsg: "0x", extraOptions: "0x" };
// Create SendParam struct for quoting
const sendParam = createSendParamStruct({
// If sending to Hypercore, the destinationChainId in the SendParam is always HyperEVM
Expand Down
15 changes: 14 additions & 1 deletion api/_slippage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import {
STABLE_COIN_SYMBOLS,
TOKEN_SYMBOLS_MAP,
CUSTOM_GAS_TOKENS,
CHAIN_IDs,
} from "./_constants";
import { OriginOrDestination, Token } from "./_dexes/types";

export const STABLE_COIN_SWAP_SLIPPAGE = {
origin: 0.25, // 0.25%
destination: 0.5, // 0.5%
};
export const STABLE_COIN_SWAP_SLIPPAGE_BY_CHAIN: Record<
number,
typeof STABLE_COIN_SWAP_SLIPPAGE
> = {
[CHAIN_IDs.HYPERCORE]: {
origin: 0.5, // 0.5%
destination: 1, // 1%
},
};
export const MAJOR_PAIR_SLIPPAGE = {
origin: 0.75, // 0.75%
destination: 1.5, // 1.5%
Expand Down Expand Up @@ -101,7 +111,10 @@ function resolveAutoSlippage(params: {
isStableCoinSymbol(params.tokenOut.symbol);

if (isStableCoinSwap) {
return STABLE_COIN_SWAP_SLIPPAGE[params.originOrDestination];
const stableCoinSwapSlippage =
STABLE_COIN_SWAP_SLIPPAGE_BY_CHAIN[params.tokenIn.chainId] ||
STABLE_COIN_SWAP_SLIPPAGE;
return stableCoinSwapSlippage[params.originOrDestination];
}

const isTokenInStableOrMajor =
Expand Down
2 changes: 1 addition & 1 deletion api/_sponsorship-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type SponsorshipRoutingRule = RoutingRule<
const makeRoutingRuleGetStrategyFn =
(isEligibleForSponsorship: boolean) => (inputToken?: Token) => {
if (inputToken?.symbol === "USDT") {
return getOftSponsoredBridgeStrategy();
return getOftSponsoredBridgeStrategy(isEligibleForSponsorship);
} else if (inputToken?.symbol === "USDC") {
return getSponsoredCctpBridgeStrategy(isEligibleForSponsorship);
}
Expand Down
Loading
Loading