Skip to content
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

feat: support UniversalSwapAndBridge in swap endpoint #1311

Merged
merged 1 commit into from
Dec 16, 2024
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
84 changes: 58 additions & 26 deletions api/_dexes/cross-swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import {
buildExactOutputBridgeTokenMessage,
buildMinOutputBridgeTokenMessage,
getUniversalSwapAndBridge,
} from "./utils";
import { getSpokePoolPeriphery } from "../_spoke-pool-periphery";
import { tagIntegratorId } from "../_integrator-id";
Expand All @@ -32,8 +33,6 @@

export type AmountType = (typeof AMOUNT_TYPE)[keyof typeof AMOUNT_TYPE];

export type LeftoverType = (typeof LEFTOVER_TYPE)[keyof typeof LEFTOVER_TYPE];

export const AMOUNT_TYPE = {
EXACT_INPUT: "exactInput",
EXACT_OUTPUT: "exactOutput",
Expand All @@ -47,15 +46,11 @@
ANY_TO_ANY: "anyToAny",
} as const;

export const LEFTOVER_TYPE = {
OUTPUT_TOKEN: "outputToken",
BRIDGEABLE_TOKEN: "bridgeableToken",
} as const;

export const PREFERRED_BRIDGE_TOKENS = ["WETH"];

const defaultQuoteFetchStrategy: UniswapQuoteFetchStrategy =
getSwapRouter02Strategy();
// This will be our default strategy until the periphery contract is audited
getSwapRouter02Strategy("UniversalSwapAndBridge");
const strategyOverrides = {
[CHAIN_IDs.BLAST]: defaultQuoteFetchStrategy,
};
Expand Down Expand Up @@ -88,6 +83,7 @@
originChainId: crossSwap.inputToken.chainId,
outputToken: crossSwap.outputToken.address,
destinationChainId: crossSwap.outputToken.chainId,
isInputNative: Boolean(crossSwap.isInputNative),
});

if (crossSwapType === CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE) {
Expand Down Expand Up @@ -126,7 +122,7 @@
}

// @TODO: Implement the following function
export async function getCrossSwapQuotesForExactInput(crossSwap: CrossSwap) {

Check warning on line 125 in api/_dexes/cross-swap.ts

View workflow job for this annotation

GitHub Actions / format-and-lint

'crossSwap' is defined but never used. Allowed unused args must match /^_/u
throw new Error("Not implemented yet");
}

Expand Down Expand Up @@ -194,6 +190,7 @@
originChainId: number;
outputToken: string;
destinationChainId: number;
isInputNative: boolean;
}): CrossSwapType {
if (
isRouteEnabled(
Expand All @@ -206,6 +203,20 @@
return CROSS_SWAP_TYPE.BRIDGEABLE_TO_BRIDGEABLE;
}

// Prefer destination swap if input token is native because legacy
// `UniversalSwapAndBridge` does not support native tokens as input.
if (params.isInputNative) {
if (isInputTokenBridgeable(params.inputToken, params.originChainId)) {
return CROSS_SWAP_TYPE.BRIDGEABLE_TO_ANY;
}
// We can't bridge native tokens that are not ETH, e.g. MATIC or AZERO. Therefore
// throw until we have periphery contract audited so that it can accept native
// tokens and do an origin swap.
throw new Error(
"Unsupported swap: Input token is native but not bridgeable"
);
}

if (isOutputTokenBridgeable(params.outputToken, params.destinationChainId)) {
return CROSS_SWAP_TYPE.ANY_TO_BRIDGEABLE;
}
Expand All @@ -230,24 +241,45 @@
let toAddress: string;

if (crossSwapQuotes.originSwapQuote) {
const spokePoolPeriphery = getSpokePoolPeriphery(
crossSwapQuotes.originSwapQuote.peripheryAddress,
originChainId
);
tx = await spokePoolPeriphery.populateTransaction.swapAndBridge(
crossSwapQuotes.originSwapQuote.tokenIn.address,
crossSwapQuotes.originSwapQuote.tokenOut.address,
crossSwapQuotes.originSwapQuote.swapTx.data,
crossSwapQuotes.originSwapQuote.maximumAmountIn,
crossSwapQuotes.originSwapQuote.minAmountOut,
deposit,
{
value: crossSwapQuotes.crossSwap.isInputNative
? crossSwapQuotes.originSwapQuote.maximumAmountIn
: 0,
}
);
toAddress = spokePoolPeriphery.address;
const { entryPointContract } = crossSwapQuotes.originSwapQuote;
if (entryPointContract.name === "SpokePoolPeriphery") {
const spokePoolPeriphery = getSpokePoolPeriphery(
entryPointContract.address,
originChainId
);
tx = await spokePoolPeriphery.populateTransaction.swapAndBridge(
crossSwapQuotes.originSwapQuote.tokenIn.address,
crossSwapQuotes.originSwapQuote.tokenOut.address,
crossSwapQuotes.originSwapQuote.swapTx.data,
crossSwapQuotes.originSwapQuote.maximumAmountIn,
crossSwapQuotes.originSwapQuote.minAmountOut,
deposit,
{
value: crossSwapQuotes.crossSwap.isInputNative
? crossSwapQuotes.originSwapQuote.maximumAmountIn
: 0,
}
);
toAddress = spokePoolPeriphery.address;
} else if (entryPointContract.name === "UniversalSwapAndBridge") {
const universalSwapAndBridge = getUniversalSwapAndBridge(
entryPointContract.dex,
originChainId
);
tx = await universalSwapAndBridge.populateTransaction.swapAndBridge(
crossSwapQuotes.originSwapQuote.tokenIn.address,
crossSwapQuotes.originSwapQuote.tokenOut.address,
crossSwapQuotes.originSwapQuote.swapTx.data,
crossSwapQuotes.originSwapQuote.maximumAmountIn,
crossSwapQuotes.originSwapQuote.minAmountOut,
deposit
);
toAddress = universalSwapAndBridge.address;
} else {
throw new Error(
`Could not build cross swap tx for unknown entry point contract`
);
}
} else {
tx = await spokePool.populateTransaction.depositV3(
deposit.depositor,
Expand Down
16 changes: 13 additions & 3 deletions api/_dexes/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BigNumber } from "ethers";
import { getSuggestedFees } from "../_utils";
import { AmountType, CrossSwapType, LeftoverType } from "./cross-swap";
import { AmountType, CrossSwapType } from "./cross-swap";

export type { AmountType, CrossSwapType };

Expand Down Expand Up @@ -31,7 +31,6 @@ export type CrossSwap = {
recipient: string;
slippageTolerance: number;
type: AmountType;
leftoverType?: LeftoverType;
refundOnOrigin: boolean;
refundAddress?: string;
isInputNative?: boolean;
Expand Down Expand Up @@ -77,10 +76,21 @@ export type CrossSwapQuotes = {
};
destinationSwapQuote?: SwapQuote;
originSwapQuote?: SwapQuote & {
peripheryAddress: string;
entryPointContract: OriginSwapEntryPointContract;
};
};

export type OriginSwapEntryPointContract =
| {
name: "SpokePoolPeriphery";
address: string;
}
| {
name: "UniversalSwapAndBridge";
address: string;
dex: SupportedDex;
};

export type CrossSwapQuotesWithFees = CrossSwapQuotes & {
fees: CrossSwapFees;
};
Expand Down
14 changes: 8 additions & 6 deletions api/_dexes/uniswap/quote-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,13 @@ export async function getUniswapCrossSwapQuotesForOutputA2B(
);
}

const spokePoolPeripheryAddress =
strategy.getPeripheryAddress(originSwapChainId);
const originSwapEntryPoint =
strategy.getOriginSwapEntryPoint(originSwapChainId);
const originSwap = {
chainId: originSwapChainId,
tokenIn: crossSwap.inputToken,
tokenOut: bridgeableInputToken,
recipient: spokePoolPeripheryAddress,
recipient: originSwapEntryPoint.address,
slippageTolerance: crossSwap.slippageTolerance,
type: crossSwap.type,
};
Expand Down Expand Up @@ -215,7 +215,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2B(
destinationSwapQuote: undefined,
originSwapQuote: {
...adjOriginSwapQuote,
peripheryAddress: spokePoolPeripheryAddress,
entryPointContract: originSwapEntryPoint,
},
};
}
Expand Down Expand Up @@ -341,11 +341,13 @@ export async function getUniswapCrossSwapQuotesForOutputA2A(
const multiCallHandlerAddress = getMultiCallHandlerAddress(
destinationSwapChainId
);
const originSwapEntryPoint =
originStrategy.getOriginSwapEntryPoint(originSwapChainId);
const originSwap = {
chainId: originSwapChainId,
tokenIn: crossSwap.inputToken,
tokenOut: bridgeableInputToken,
recipient: originStrategy.getPeripheryAddress(originSwapChainId),
recipient: originSwapEntryPoint.address,
slippageTolerance: crossSwap.slippageTolerance,
type: crossSwap.type,
};
Expand Down Expand Up @@ -418,7 +420,7 @@ export async function getUniswapCrossSwapQuotesForOutputA2A(
bridgeQuote,
originSwapQuote: {
...adjOriginSwapQuote,
peripheryAddress: originStrategy.getPeripheryAddress(originSwapChainId),
entryPointContract: originSwapEntryPoint,
},
};
}
Expand Down
28 changes: 23 additions & 5 deletions api/_dexes/uniswap/swap-router-02.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SwapRouter } from "@uniswap/router-sdk";
import { CHAIN_IDs } from "@across-protocol/constants";

import { getLogger } from "../../_utils";
import { Swap, SwapQuote } from "../types";
import { OriginSwapEntryPointContract, Swap, SwapQuote } from "../types";
import { getSpokePoolPeripheryAddress } from "../../_spoke-pool-periphery";
import {
addMarkupToAmount,
Expand All @@ -18,6 +18,7 @@ import {
UniswapClassicQuoteFromApi,
} from "./trading-api";
import { RouterTradeAdapter } from "./adapter";
import { getUniversalSwapAndBridgeAddress } from "../utils";

// Taken from here: https://docs.uniswap.org/contracts/v3/reference/deployments/
export const SWAP_ROUTER_02_ADDRESS = {
Expand All @@ -32,10 +33,27 @@ export const SWAP_ROUTER_02_ADDRESS = {
[CHAIN_IDs.ZORA]: "0x7De04c96BE5159c3b5CeffC82aa176dc81281557",
};

export function getSwapRouter02Strategy(): UniswapQuoteFetchStrategy {
export function getSwapRouter02Strategy(
originSwapEntryPointContractName: OriginSwapEntryPointContract["name"]
): UniswapQuoteFetchStrategy {
const getRouterAddress = (chainId: number) => SWAP_ROUTER_02_ADDRESS[chainId];
const getPeripheryAddress = (chainId: number) =>
getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId);
const getOriginSwapEntryPoint = (chainId: number) => {
if (originSwapEntryPointContractName === "SpokePoolPeriphery") {
return {
name: "SpokePoolPeriphery",
address: getSpokePoolPeripheryAddress("uniswap-swapRouter02", chainId),
} as const;
} else if (originSwapEntryPointContractName === "UniversalSwapAndBridge") {
return {
name: "UniversalSwapAndBridge",
address: getUniversalSwapAndBridgeAddress("uniswap", chainId),
dex: "uniswap",
} as const;
}
throw new Error(
`Unknown origin swap entry point contract '${originSwapEntryPointContractName}'`
);
};

const fetchFn = async (
swap: Swap,
Expand Down Expand Up @@ -127,7 +145,7 @@ export function getSwapRouter02Strategy(): UniswapQuoteFetchStrategy {

return {
getRouterAddress,
getPeripheryAddress,
getOriginSwapEntryPoint,
fetchFn,
};
}
Expand Down
9 changes: 6 additions & 3 deletions api/_dexes/uniswap/universal-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ export const UNIVERSAL_ROUTER_ADDRESS = {
export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy {
const getRouterAddress = (chainId: number) =>
UNIVERSAL_ROUTER_ADDRESS[chainId];
const getPeripheryAddress = (chainId: number) =>
getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId);
const getOriginSwapEntryPoint = (chainId: number) =>
({
name: "SpokePoolPeriphery",
address: getSpokePoolPeripheryAddress("uniswap-universalRouter", chainId),
}) as const;

const fetchFn = async (
swap: Swap,
Expand Down Expand Up @@ -127,7 +130,7 @@ export function getUniversalRouterStrategy(): UniswapQuoteFetchStrategy {

return {
getRouterAddress,
getPeripheryAddress,
getOriginSwapEntryPoint,
fetchFn,
};
}
Expand Down
11 changes: 10 additions & 1 deletion api/_dexes/uniswap/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ import { Swap, SwapQuote, Token } from "../types";

export type UniswapQuoteFetchStrategy = {
getRouterAddress: (chainId: number) => string;
getPeripheryAddress: (chainId: number) => string;
getOriginSwapEntryPoint: (chainId: number) =>
| {
name: "SpokePoolPeriphery";
address: string;
}
| {
name: "UniversalSwapAndBridge";
address: string;
dex: "uniswap" | "1inch";
};
fetchFn: UniswapQuoteFetchFn;
};
export type UniswapQuoteFetchFn = (
Expand Down
Loading
Loading