Skip to content

Commit

Permalink
feat: Enable uniswap v3 automatic routing (#531)
Browse files Browse the repository at this point in the history
  • Loading branch information
KirillDogadin-std authored Dec 5, 2022
1 parent bb70a42 commit 0c51938
Show file tree
Hide file tree
Showing 26 changed files with 1,733 additions and 977 deletions.
2,078 changes: 1,189 additions & 889 deletions core/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dev": "tsc-watch --noClear",
"build": "tsc --build tsconfig.json",
"start": "node .",
"test": "npx hardhat test --network testnetwork --config local.hardhat.config.ts",
"test": "NODE_ENV=test npx hardhat test --network testnetwork --config local.hardhat.config.ts",
"lint": "eslint --ext \".js,.ts\" --ignore-path .gitignore . --max-warnings=0",
"hardhat:silent": "npx hardhat node &> /dev/null",
"hardhat": "npx hardhat node",
Expand Down
4 changes: 2 additions & 2 deletions core/simulations/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import vaultLiquidation from './configs/vaultLiquidation';
import onboardNewCollateral from './configs/onboardNewCollateral';

export const SIMULATIONS: Simulation[] = [
vaultLiquidation,
debtAuctionSimulation,
surplusAuctionBlockchain,
surplusAuctionSimulation,
surplusAuctionBlockchain,
wstethAuctionSimulation,
blockWithVaultsInAllStates,
vaultLiquidation,
onboardNewCollateral,
];
2 changes: 1 addition & 1 deletion core/simulations/configs/vaultLiquidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { fetchMaximumAuctionDurationInSeconds } from '../../src/fetch';
const TWO_YEARS_IN_MINUTES = 60 * 24 * 30 * 12 * 2;

const simulation: Simulation = {
title: 'Simulate collateral auction per collateral type',
title: 'Create collateral auction',
steps: [
{
title: 'Reset blockchain fork',
Expand Down
19 changes: 14 additions & 5 deletions core/src/auctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,21 @@ export const enrichMarketDataRecordsWithValues = async function (

export const enrichAuctionWithMarketDataRecords = async function (
auction: AuctionTransaction,
network: string
network: string,
useAutoRouter = false
): Promise<AuctionTransaction> {
if (!auction.isActive || !auction.approximateUnitPrice || auction.isFinished) {
return auction;
}
try {
const collateralToCoverDebt = await calculateCollateralToCoverDebt(network, auction);
const exchangeFees = await getDefaultMarketFee(network);
const marketDataRecords = await getMarketDataRecords(network, auction.collateralSymbol, collateralToCoverDebt);
const marketDataRecords = await getMarketDataRecords(
network,
auction.collateralSymbol,
collateralToCoverDebt,
useAutoRouter
);
const enrichedMarketDataRecords = await enrichMarketDataRecordsWithValues(
auction,
marketDataRecords,
Expand Down Expand Up @@ -206,12 +212,13 @@ export const enrichAuctionWithPriceDrop = async function (auction: Auction): Pro

export const enrichAuctionWithPriceDropAndMarketDataRecords = async function (
auction: Auction,
network: string
network: string,
useAutoRouter?: boolean
): Promise<Auction> {
const enrichedAuctionWithNewPriceDrop = await enrichAuctionWithPriceDrop(auction);
const fees = await getApproximateTransactionFees(network);
const auctionWithFees = await enrichAuctionWithTransactionFees(enrichedAuctionWithNewPriceDrop, fees, network);
return await enrichAuctionWithMarketDataRecords(auctionWithFees, network);
return await enrichAuctionWithMarketDataRecords(auctionWithFees, network, useAutoRouter);
};

export const fetchSingleAuctionById = async function (
Expand Down Expand Up @@ -338,7 +345,9 @@ export const bidWithCallee = async function (
notifier?: Notifier
): Promise<string> {
const calleeAddress = getCalleeAddressByCollateralType(network, auction.collateralType, marketId);
const calleeData = await getCalleeData(network, auction.collateralType, marketId, profitAddress);
const marketData = auction.marketDataRecords?.[marketId];
const preloadedPools = marketData && 'pools' in marketData ? marketData.pools : undefined;
const calleeData = await getCalleeData(network, auction.collateralType, marketId, profitAddress, preloadedPools);
const contractName = getClipperNameByCollateralType(auction.collateralType);
const contractParameters = [
convertNumberTo32Bytes(auction.index),
Expand Down
12 changes: 8 additions & 4 deletions core/src/calleeFunctions/CurveLpTokenUniv3Callee.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import type { CalleeFunctions, CollateralConfig } from '../types';
import type { CalleeFunctions, CollateralConfig, Pool } from '../types';
import { ethers } from 'ethers';
import BigNumber from '../bignumber';
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
import { convertCrvethToEth, CURVE_COIN_INDEX, CURVE_POOL_ADDRESS } from './helpers/curve';
import { encodeRoute, convertCollateralToDai } from './helpers/uniswapV3';
import { convertCollateralToDai, encodePools } from './helpers/uniswapV3';

export const CHARTER_MANAGER_ADDRESS = '0x8377CD01a5834a6EaD3b7efb482f678f2092b77e';

const getCalleeData = async function (
network: string,
collateral: CollateralConfig,
marketId: string,
profitAddress: string
profitAddress: string,
preloadedPools?: Pool[]
): Promise<string> {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee !== 'CurveLpTokenUniv3Callee') {
throw new Error(`Can not encode route for the "${collateral.ilk}"`);
}
const route = await encodeRoute(network, marketData.route);
if (!preloadedPools) {
throw new Error(`Can not encode route for the "${collateral.ilk}" without preloaded pools`);
}
const route = await encodePools(network, preloadedPools);
const curveData = [CURVE_POOL_ADDRESS, CURVE_COIN_INDEX];
const joinAdapterAddress = await getContractAddressByName(network, getJoinNameByCollateralType(collateral.ilk));
const minProfit = 1;
Expand Down
31 changes: 25 additions & 6 deletions core/src/calleeFunctions/UniswapV3Callee.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import type { CalleeFunctions, CollateralConfig } from '../types';
import type { CalleeFunctions, CollateralConfig, Pool } from '../types';
import { ethers } from 'ethers';
import BigNumber from '../bignumber';
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
import { convertCollateralToDaiUsingRoute, encodeRoute } from './helpers/uniswapV3';
import { convertCollateralToDaiUsingPool, encodePools } from './helpers/uniswapV3';
import { getPools } from '.';
import { routeToPool } from './helpers/pools';
import { getRouteAndGasQuote } from './helpers/uniswapV3';

const getCalleeData = async function (
network: string,
collateral: CollateralConfig,
marketId: string,
profitAddress: string
profitAddress: string,
preloadedPools?: Pool[]
): Promise<string> {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee !== 'UniswapV3Callee') {
throw new Error(`getCalleeData called with invalid collateral type "${collateral.ilk}"`);
}
const pools = preloadedPools || (await getPools(network, collateral, marketId));
if (!pools) {
throw new Error(`getCalleeData called with invalid pools`);
}
const joinAdapterAddress = await getContractAddressByName(network, getJoinNameByCollateralType(collateral.ilk));
const minProfit = 1;
const uniswapV3route = await encodeRoute(network, [collateral.symbol, ...marketData.route]);
const uniswapV3pools = await encodePools(network, pools);
const typesArray = ['address', 'address', 'uint256', 'bytes', 'address'];
return ethers.utils.defaultAbiCoder.encode(typesArray, [
profitAddress,
joinAdapterAddress,
minProfit,
uniswapV3route,
uniswapV3pools,
ethers.constants.AddressZero,
]);
};
Expand All @@ -34,7 +42,18 @@ const getMarketPrice = async function (
collateralAmount: BigNumber
): Promise<BigNumber> {
// convert collateral into DAI
const daiAmount = await convertCollateralToDaiUsingRoute(network, collateral.symbol, marketId, collateralAmount);
const { route, fees } = await getRouteAndGasQuote(network, collateral.symbol, collateralAmount, marketId);
if (!route) {
throw new Error(`No route found for ${collateral.symbol} to DAI`);
}
const pools = await routeToPool(network, route, collateral.symbol, fees);
const daiAmount = await convertCollateralToDaiUsingPool(
network,
collateral.symbol,
marketId,
collateralAmount,
pools
);

// return price per unit
return daiAmount.dividedBy(collateralAmount);
Expand Down
30 changes: 30 additions & 0 deletions core/src/calleeFunctions/helpers/pools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getTokenAddressByNetworkAndSymbol } from '../../tokens';
import { CollateralSymbol, Pool } from '../../types';

const getRouteSteps = (route: string[], fees: number[]) => {
const routeSteps = [];
for (let i = 0; i < route.length - 1; i++) {
routeSteps.push({ tokens: [route[i], route[i + 1]], fee: fees[i] });
}
return routeSteps;
};

export const routeToPool = async (
network: string,
route: string[],
collateralSymbol: CollateralSymbol,
uniswapFees?: number[]
): Promise<Pool[]> => {
const fees = uniswapFees || Array.from({ length: route.length + 2 }, () => 3000);
const fullRoute = [collateralSymbol, ...route, 'DAI'];
const routeSteps = getRouteSteps(fullRoute, fees);
return await Promise.all(
routeSteps.map(async step => ({
addresses: await Promise.all(
step.tokens.map(symbol => getTokenAddressByNetworkAndSymbol(network, symbol))
),
fee: step.fee,
routes: step.tokens,
}))
);
};
67 changes: 56 additions & 11 deletions core/src/calleeFunctions/helpers/uniswapAutoRouter.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import { AlphaRouter } from '@uniswap/smart-order-router';
import { Protocol } from '@uniswap/router-sdk';
import { Token, Percent, TradeType, CurrencyAmount } from '@uniswap/sdk-core';
import BigNumber from '../../bignumber';
import getProvider from '../../provider';
import { getDecimalChainIdByNetworkType } from '../../network';
import { getActualDecimalChainIdByNetworkType } from '../../network';
import { getTokenAddressByNetworkAndSymbol, getTokenDecimalsBySymbol } from '../../tokens';
import { getCollateralConfigBySymbol } from '../../constants/COLLATERALS';
import { DAI_NUMBER_OF_DIGITS } from '../../constants/UNITS';
import memoizee from 'memoizee';

const routers: Record<string, AlphaRouter> = {};

const getAutoRouter = async function (network: string): Promise<AlphaRouter> {
if (routers[network]) {
return routers[network];
}
routers[network] = new AlphaRouter({
chainId: getActualDecimalChainIdByNetworkType(network),
provider: await getProvider(network),
});
return routers[network];
};

const getUniswapTokenBySymbol = async function (network: string, symbol: string): Promise<Token> {
const tokenAddress = await getTokenAddressByNetworkAndSymbol(network, symbol);
const tokenDecimals = getTokenDecimalsBySymbol(symbol);
const decimalChainId = getDecimalChainIdByNetworkType(network);
const decimalChainId = getActualDecimalChainIdByNetworkType(network);
return new Token(decimalChainId, tokenAddress, tokenDecimals, symbol);
};

Expand All @@ -20,8 +36,7 @@ export const getUniswapAutoRoute = async function (
walletAddress?: string
) {
const collateralConfig = getCollateralConfigBySymbol(collateralSymbol);
const provider = await getProvider(network);
const router = new AlphaRouter({ chainId: 1, provider });
const router = await getAutoRouter(network);
const inputToken = await getUniswapTokenBySymbol(network, collateralConfig.symbol);
const outputToken = await getUniswapTokenBySymbol(network, 'DAI');

Expand All @@ -40,6 +55,7 @@ export const getUniswapAutoRoute = async function (
},
{
maxSplits: 0,
protocols: [Protocol.V3],
}
);
if (!route) {
Expand All @@ -48,27 +64,56 @@ export const getUniswapAutoRoute = async function (
return route;
};

export const fetchAutoRouteInformation = async function (
const trimRoute = (route: string[]): string[] => {
if (route.length < 2) {
throw new Error('Route array must have at least 2 elements.');
}
return route.slice(1, route.length - 1);
};

const _fetchAutoRouteInformation = async function (
network: string,
collateralSymbol: string,
inputAmount: string | number = 1,
walletAddress?: string
) {
try {
const token = await getUniswapTokenBySymbol(network, collateralSymbol);
const autoRouteData = await getUniswapAutoRoute(network, collateralSymbol, inputAmount, walletAddress);
const routes = autoRouteData.route[0].tokenPath.map(p => p.symbol);

const bestRoute = autoRouteData.route[0];
const fullRoute = bestRoute.tokenPath.map(p => {
if (!p.symbol) {
throw new Error(`Could not get symbol for token "${p.address}".`);
}
return p.symbol;
});
const route = trimRoute(fullRoute);
if (bestRoute.route.protocol !== Protocol.V3) {
throw new Error('Only V3 routes are supported.');
}
const fees = bestRoute.route.pools.map(pool => pool.fee);
const quote = new BigNumber(autoRouteData.quote.toFixed());
const quoteGasAdjusted = new BigNumber(autoRouteData.quoteGasAdjusted.toFixed());
return {
totalPrice: new BigNumber(autoRouteData.quote.toFixed(token.decimals)),
routes,
totalPrice: new BigNumber(autoRouteData.quote.toFixed(DAI_NUMBER_OF_DIGITS)),
route,
quote,
quoteGasAdjusted,
errorMessage: undefined,
fees,
};
} catch (error: any) {
return {
totalPrice: undefined,
routes: undefined,
route: undefined,
quote: undefined,
fees: undefined,
errorMessage: error.toString(),
};
}
};

export const fetchAutoRouteInformation = memoizee(_fetchAutoRouteInformation, {
promise: true,
maxAge: 1000 * 60, // 1 minute
length: 4,
});
5 changes: 4 additions & 1 deletion core/src/calleeFunctions/helpers/uniswapV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const EXCHANGE_RATE_CACHE = 20 * 1000;

const getCalleeConfig = function (collateral: CollateralConfig, marketId: string): RegularCalleeConfig {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee === 'UniswapV2CalleeDai' || marketData?.callee === 'UniswapV3Callee') {
const isUniswapTokenNonAutoRouted =
(marketData?.callee === 'UniswapV2CalleeDai' || marketData?.callee === 'UniswapV3Callee') &&
!('automaticRouter' in marketData);
if (isUniswapTokenNonAutoRouted) {
return marketData;
}
throw new Error(`"${collateral.symbol}" is not an UniSwap token`);
Expand Down
Loading

0 comments on commit 0c51938

Please sign in to comment.