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

patch: Improve autorouter #561

Merged
merged 14 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
16 changes: 12 additions & 4 deletions core/src/calleeFunctions/CurveLpTokenUniv3Callee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import BigNumber from '../bignumber';
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
import { convertCrvethToEth, CURVE_COIN_INDEX, CURVE_POOL_ADDRESS } from './helpers/curve';
import { convertCollateralToDai, encodePools } from './helpers/uniswapV3';
import { routeToPool } from './helpers/pools';

export const CHARTER_MANAGER_ADDRESS = '0x8377CD01a5834a6EaD3b7efb482f678f2092b77e';

Expand Down Expand Up @@ -38,18 +39,25 @@ const getCalleeData = async function (

const getMarketPrice = async function (
network: string,
_collateral: CollateralConfig,
_marketId: string,
collateral: CollateralConfig,
marketId: string,
collateralAmount: BigNumber
): Promise<BigNumber> {
): Promise<{ price: BigNumber; pools: Pool[] }> {
const marketData = collateral.exchanges[marketId];
if (marketData.callee !== 'CurveLpTokenUniv3Callee') {
throw new Error(`Can not get market price for the "${collateral.ilk}"`);
}
// convert stETH into ETH
const wethAmount = await convertCrvethToEth(network, collateralAmount);

// convert ETH into DAI
const daiAmount = await convertCollateralToDai(network, 'ETH', wethAmount);

// return price per unit
return daiAmount.dividedBy(collateralAmount);
return {
price: daiAmount.dividedBy(collateralAmount),
pools: await routeToPool(network, marketData.route, collateral.symbol),
};
};

const CurveLpTokenUniv3Callee: CalleeFunctions = {
Expand Down
14 changes: 11 additions & 3 deletions core/src/calleeFunctions/UniswapV2CalleeDai.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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 { getUniswapRouteAddressesBySymbol, getRegularTokenExchangeRateBySymbol } from './helpers/uniswapV2';
import { routeToPool } from './helpers/pools';

const getCalleeData = async function (
network: string,
Expand Down Expand Up @@ -30,8 +31,15 @@ const getMarketPrice = async function (
collateral: CollateralConfig,
marketId: string,
amount: BigNumber
): Promise<BigNumber> {
return await getRegularTokenExchangeRateBySymbol(network, collateral.symbol, marketId, amount);
): Promise<{ price: BigNumber; pools: Pool[] }> {
const marketData = collateral.exchanges[marketId];
if (marketData.callee !== 'UniswapV2CalleeDai') {
throw new Error(`Can not get market price for the "${collateral.ilk}"`);
}
return {
price: await getRegularTokenExchangeRateBySymbol(network, collateral.symbol, marketId, amount),
pools: await routeToPool(network, marketData.route, collateral.symbol),
};
};

const UniswapV2CalleeDai: CalleeFunctions = {
Expand Down
4 changes: 2 additions & 2 deletions core/src/calleeFunctions/UniswapV2LpTokenCalleeDai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const getMarketPrice = async function (
collateral: CollateralConfig,
marketId: string,
amount: BigNumber
): Promise<BigNumber> {
): Promise<{ price: BigNumber; pools: undefined }> {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee !== 'UniswapV2LpTokenCalleeDai') {
throw new Error(`"${collateral.symbol}" is not a UniSwap LP token`);
Expand All @@ -58,7 +58,7 @@ const getMarketPrice = async function (
portionOfTheTotalSupply
);
const totalPrice = totalPriceOfToken0.plus(totalPriceOfToken1);
return totalPrice.dividedBy(amount);
return { price: totalPrice.dividedBy(amount), pools: undefined };
};

const UniswapV2CalleeDai: CalleeFunctions = {
Expand Down
34 changes: 19 additions & 15 deletions core/src/calleeFunctions/UniswapV3Callee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import type { CalleeFunctions, CollateralConfig, Pool } from '../types';
import { ethers } from 'ethers';
import BigNumber from '../bignumber';
import { getContractAddressByName, getJoinNameByCollateralType } from '../contracts';
import { convertCollateralToDaiUsingPool, encodePools } from './helpers/uniswapV3';
import { convertCollateralToDaiUsingPool, encodePools, getRouteAndGasQuote } from './helpers/uniswapV3';
import { getPools } from '.';
import { routeToPool } from './helpers/pools';
import { getRouteAndGasQuote } from './helpers/uniswapV3';

const getCalleeData = async function (
network: string,
Expand Down Expand Up @@ -40,23 +39,28 @@ const getMarketPrice = async function (
collateral: CollateralConfig,
marketId: string,
collateralAmount: BigNumber
): Promise<BigNumber> {
// convert collateral into DAI
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(
): Promise<{ price: BigNumber; pools: Pool[] }> {
const { route, fees, totalPrice, pools } = await getRouteAndGasQuote(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I would remove getRouteAndGasQuote whatsoever and move its logic here, inside getMarketPrice. Because it's just adds a very strange layer of abstraction which doesn't add any value, but complicates understanding of what's going on

network,
collateral.symbol,
marketId,
collateralAmount,
pools
marketId
);

// return price per unit
return daiAmount.dividedBy(collateralAmount);
if (!pools && route) {
const generatedPools = await routeToPool(network, route, collateral.symbol, fees);
const daiAmount = await convertCollateralToDaiUsingPool(
network,
collateral.symbol,
marketId,
collateralAmount,
generatedPools
);
return { price: daiAmount.dividedBy(collateralAmount), pools: generatedPools };
}
if (totalPrice && pools) {
return { price: totalPrice.dividedBy(collateralAmount), pools: pools };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only remaining problem I see now is that totalPrice here is not the adjusted price that takes exchange fees into account (so we can properly compare exchange prices between each other), but the normal one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i observe that the autorouter returns 2 amounts:

    /**
     * The quote for the swap.
     * For EXACT_IN swaps this will be an amount of token out.
     * For EXACT_OUT this will be an amount of token in.
     */
    quote: CurrencyAmount;
    /**
     * The quote adjusted for the estimated gas used by the swap.
     * This is computed by estimating the amount of gas used by the swap, converting
     * this estimate to be in terms of the quote token, and subtracting that from the quote.
     * i.e. quoteGasAdjusted = quote - estimatedGasUsedQuoteToken
     */
    quoteGasAdjusted: CurrencyAmount;

My understanding of the above does not combine well with your claim.

The documentation claims that quote is the amount of "token out". If the value does not account for exchange fees, then the amount out is different from quote and hence the documentation is not valid.

Did you perhaps mean the gas fees? should i use quoteGasAdjusted value instead of quote?

}
throw new Error(`Failed to compute market data due to lack of information`);
};

const UniswapV2CalleeDai: CalleeFunctions = {
Expand Down
10 changes: 7 additions & 3 deletions core/src/calleeFunctions/WstETHCurveUniv3Callee.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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 { convertStethToEth } from './helpers/curve';
import { convertCollateralToDai, UNISWAP_FEE } from './helpers/uniswapV3';
import { convertWstethToSteth } from './helpers/wsteth';
import { routeToPool } from './helpers/pools';

const getCalleeData = async function (
network: string,
Expand All @@ -29,7 +30,7 @@ const getMarketPrice = async function (
collateral: CollateralConfig,
marketId: string,
collateralAmount: BigNumber
): Promise<BigNumber> {
): Promise<{ price: BigNumber; pools: Pool[] }> {
const marketData = collateral.exchanges[marketId];
if (marketData?.callee !== 'WstETHCurveUniv3Callee') {
throw new Error(`Invalid callee used to get market price for ${collateral.ilk}`);
Expand All @@ -44,7 +45,10 @@ const getMarketPrice = async function (
const daiAmount = await convertCollateralToDai(network, 'ETH', ethAmount);

// return price per unit
return daiAmount.dividedBy(collateralAmount);
return {
price: daiAmount.dividedBy(collateralAmount),
pools: await routeToPool(network, marketData.route, collateral.symbol),
};
};

const UniswapV2CalleeDai: CalleeFunctions = {
Expand Down
7 changes: 7 additions & 0 deletions core/src/calleeFunctions/helpers/uniswapAutoRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ const _fetchAutoRouteInformation = async function (
throw new Error('Only V3 routes are supported.');
}
const fees = bestRoute.route.pools.map(pool => pool.fee);
const pools = bestRoute.route.pools.map(pool => ({
addresses: [pool.token1.address, pool.token0.address],
fee: pool.fee,
routes: [pool.token1.symbol, pool.token0.symbol],
}));
valiafetisov marked this conversation as resolved.
Show resolved Hide resolved
const quote = new BigNumber(autoRouteData.quote.toFixed());
const quoteGasAdjusted = new BigNumber(autoRouteData.quoteGasAdjusted.toFixed());
return {
Expand All @@ -100,13 +105,15 @@ const _fetchAutoRouteInformation = async function (
quoteGasAdjusted,
errorMessage: undefined,
fees,
pools,
};
} catch (error: any) {
return {
totalPrice: undefined,
route: undefined,
quote: undefined,
fees: undefined,
pools: undefined,
errorMessage: error.toString(),
};
}
Expand Down
12 changes: 9 additions & 3 deletions core/src/calleeFunctions/helpers/uniswapV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,20 @@ export const getRouteAndGasQuote = async (
throw new Error(`getCalleeData called with invalid collateral type "${collateral.ilk}"`);
}
if (isAutorouted) {
const { route, quoteGasAdjusted, fees } = await fetchAutoRouteInformation(
const { route, quoteGasAdjusted, fees, pools, totalPrice } = await fetchAutoRouteInformation(
network,
collateralSymbol,
collateralAmount.toFixed()
);
return { route, quoteGasAdjusted, fees };
return { route, quoteGasAdjusted, fees, pools, totalPrice };
} else {
return { route: calleeConfig.route, quoteGasAdjusted: undefined, fees: undefined };
return {
route: calleeConfig.route,
quoteGasAdjusted: undefined,
fees: undefined,
pools: undefined,
totalPrice: undefined,
};
}
};

Expand Down
59 changes: 26 additions & 33 deletions core/src/calleeFunctions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import type {
MarketData,
CollateralConfig,
CollateralSymbol,
RegularCalleeConfig,
UniswapV2LpTokenCalleeConfig,
AutoRouterCalleeConfig,
Pool,
PriceWithPools,
} from '../types';
import memoizee from 'memoizee';
import BigNumber from '../bignumber';
Expand Down Expand Up @@ -64,63 +62,58 @@ export const getPools = async (
return await routeToPool(network, calleeConfig.route, collateral.symbol);
}
if ('automaticRouter' in calleeConfig) {
const { route, fees } = await fetchAutoRouteInformation(network, collateral.symbol, amount.toFixed());
if (!route) {
throw new Error('No automatic route can be found');
const { pools } = await fetchAutoRouteInformation(network, collateral.symbol, amount.toFixed());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Where do we still use getPools if we the proposal is to never use them separately from the price?
  • Why do we ever send amount as a string?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do we still use getPools if we the proposal is to never use them separately from the price?

getCalleeData for UniswapV3 has 2 use cases:

  • autorouter
    • the prices and pools are extracted at the same time. No need to perform other actions (getPools is not called)
  • hardcoded
    • the route is provided, prices are not extracted with pools (because alpharouter is not called) and we have to do the old fashioned extraction from the hardcoded route. But we also have to unify the format to use pools primarily. So the getPools is called for the sake of converting route to pool.

I will refactor the code a bit so that getPools cannot be called for callee configs where autorouter is enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed unused arg (amount)

if (!pools) {
throw new Error('No automatic pools can be found');
}
return await routeToPool(network, route, collateral.symbol, fees);
return pools;
}
return undefined;
};

export const enrichCalleeConfigWithPools = async (
network: string,
collateral: CollateralConfig,
marketId: string,
amount: BigNumber
): Promise<UniswapV2LpTokenCalleeConfig | ((RegularCalleeConfig | AutoRouterCalleeConfig) & { pools: Pool[] })> => {
const config = collateral.exchanges[marketId];
if (config.callee === 'UniswapV2LpTokenCalleeDai') {
return { ...config };
}
const pools = await getPools(network, collateral, marketId, amount);
if (pools) {
return { ...config, pools };
}
throw new Error('Failed to get pools');
};

export const getMarketDataById = async function (
network: string,
collateral: CollateralConfig,
marketId: string,
amount: BigNumber = new BigNumber('1')
): Promise<MarketData> {
const calleeConfig = await enrichCalleeConfigWithPools(network, collateral, marketId, amount);
const calleeConfig = collateral.exchanges[marketId];
if (!allCalleeFunctions[calleeConfig?.callee]) {
throw new Error(`Unsupported callee "${calleeConfig?.callee}" for collateral symbol "${collateral.symbol}"`);
}
let marketUnitPrice: BigNumber;
let marketPrice: PriceWithPools;

try {
marketUnitPrice = await allCalleeFunctions[calleeConfig.callee].getMarketPrice(
marketPrice = await allCalleeFunctions[calleeConfig.callee].getMarketPrice(
network,
collateral,
marketId,
amount
);
} catch (error: any) {
const errorMessage = error?.message;
marketUnitPrice = new BigNumber(NaN);
return {
...calleeConfig,
marketUnitPrice,
marketUnitPrice: new BigNumber(NaN),
pools: [],
errorMessage,
};
}
return {
...calleeConfig,
marketUnitPrice: marketUnitPrice ? marketUnitPrice : new BigNumber(NaN),
};
const marketUnitPrice = marketPrice.price;
if (calleeConfig.callee !== 'UniswapV2LpTokenCalleeDai' && marketPrice.pools) {
return {
...calleeConfig,
marketUnitPrice: marketUnitPrice ? marketUnitPrice : new BigNumber(NaN),
pools: marketPrice.pools,
};
}
if (calleeConfig.callee === 'UniswapV2LpTokenCalleeDai') {
return {
...calleeConfig,
marketUnitPrice: marketUnitPrice ? marketUnitPrice : new BigNumber(NaN),
};
}
throw new Error('No pools found where expected');
};

export const getMarketDataRecords = async function (
Expand Down
16 changes: 12 additions & 4 deletions core/src/calleeFunctions/rETHCurveUniv3Callee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { convertStethToEth } from './helpers/curve';
import { convertRethToWsteth } from './helpers/rocket';
import { convertWstethToSteth } from './helpers/wsteth';
import { NULL_ADDRESS } from '../constants/UNITS';
import { routeToPool } from './helpers/pools';

const getCalleeData = async function (
network: string,
Expand Down Expand Up @@ -37,10 +38,14 @@ const getCalleeData = async function (

const getMarketPrice = async function (
network: string,
_collateral: CollateralConfig,
_marketId: string,
collateral: CollateralConfig,
marketId: string,
collateralAmount: BigNumber
): Promise<BigNumber> {
): Promise<{ price: BigNumber; pools: Pool[] }> {
const marketData = collateral.exchanges[marketId];
if (marketData.callee !== 'rETHCurveUniv3Callee') {
throw new Error(`Can not get market price for the "${collateral.ilk}"`);
}
// convert rETH into wstETH
const wstethAmount = await convertRethToWsteth(network, collateralAmount);

Expand All @@ -54,7 +59,10 @@ const getMarketPrice = async function (
const daiAmount = await convertCollateralToDai(network, 'ETH', ethAmount);

// return price per unit
return daiAmount.dividedBy(collateralAmount);
return {
price: daiAmount.dividedBy(collateralAmount),
pools: await routeToPool(network, marketData.route, collateral.symbol),
};
};

const rETHCurveUniv3Callee: CalleeFunctions = {
Expand Down
9 changes: 7 additions & 2 deletions core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ declare interface MarketDataBase extends Partial<ExchangeFees> {
export declare interface Pool {
addresses: string[];
fee: number;
routes: string[];
routes: (string | undefined)[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which case it can be an array of undefined? 😓

Copy link
Contributor Author

@KirillDogadin-std KirillDogadin-std Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uniswap autorouter does not promise to return the symbol of the token. It's optional there.

will throw if the autorouter does not return symbol in pool

}

export declare interface MarketDataRegular extends MarketDataBase {
Expand Down Expand Up @@ -183,6 +183,11 @@ export declare interface CalleeAddresses {

export type CalleeNames = keyof CalleeAddresses;

export interface PriceWithPools {
price: BigNumber;
pools?: Pool[];
}

export declare interface CalleeFunctions {
getCalleeData: (
network: string,
Expand All @@ -196,7 +201,7 @@ export declare interface CalleeFunctions {
collateral: CollateralConfig,
marketId: string,
amount: BigNumber
) => Promise<BigNumber>;
) => Promise<PriceWithPools>;
}

export declare interface MakerParams {
Expand Down