From 4c0747294d63a88d1bcc6d29ff4af8a7271ef00d Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 09:32:16 -0700 Subject: [PATCH 01/10] quote provider has the enable fot flag awareness --- src/providers/v2/quote-provider.ts | 52 +++++++++++-------- src/routers/alpha-router/quoters/v2-quoter.ts | 2 +- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/providers/v2/quote-provider.ts b/src/providers/v2/quote-provider.ts index b22ba9d51..1506d38f8 100644 --- a/src/providers/v2/quote-provider.ts +++ b/src/providers/v2/quote-provider.ts @@ -9,6 +9,7 @@ import { V2Route } from '../../routers/router'; import { CurrencyAmount } from '../../util/amounts'; import { log } from '../../util/log'; import { routeToString } from '../../util/routes'; +import { ProviderConfig } from '../provider'; // Quotes can be null (e.g. pool did not have enough liquidity). export type V2AmountQuote = { @@ -21,12 +22,14 @@ export type V2RouteWithQuotes = [V2Route, V2AmountQuote[]]; export interface IV2QuoteProvider { getQuotesManyExactIn( amountIns: CurrencyAmount[], - routes: V2Route[] + routes: V2Route[], + providerConfig: ProviderConfig ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }>; getQuotesManyExactOut( amountOuts: CurrencyAmount[], - routes: V2Route[] + routes: V2Route[], + providerConfig: ProviderConfig ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }>; } @@ -44,22 +47,25 @@ export class V2QuoteProvider implements IV2QuoteProvider { public async getQuotesManyExactIn( amountIns: CurrencyAmount[], - routes: V2Route[] + routes: V2Route[], + providerConfig: ProviderConfig ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }> { - return this.getQuotes(amountIns, routes, TradeType.EXACT_INPUT); + return this.getQuotes(amountIns, routes, TradeType.EXACT_INPUT, providerConfig); } public async getQuotesManyExactOut( amountOuts: CurrencyAmount[], - routes: V2Route[] + routes: V2Route[], + providerConfig: ProviderConfig ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }> { - return this.getQuotes(amountOuts, routes, TradeType.EXACT_OUTPUT); + return this.getQuotes(amountOuts, routes, TradeType.EXACT_OUTPUT, providerConfig); } private async getQuotes( amounts: CurrencyAmount[], routes: V2Route[], - tradeType: TradeType + tradeType: TradeType, + providerConfig: ProviderConfig, ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }> { const routesWithQuotes: V2RouteWithQuotes[] = []; @@ -75,14 +81,16 @@ export class V2QuoteProvider implements IV2QuoteProvider { let outputAmount = amount.wrapped; for (const pair of route.pairs) { - if (pair.token0.equals(outputAmount.currency) && pair.token0.sellFeeBps?.gt(BigNumber.from(0))) { - const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token0, outputAmount.quotient); - const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); - outputAmount = outputAmountNew; - } else if (pair.token1.equals(outputAmount.currency) && pair.token1.sellFeeBps?.gt(BigNumber.from(0))) { - const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token1, outputAmount.quotient); - const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); - outputAmount = outputAmountNew; + if (providerConfig.enableFeeOnTransferFeeFetching) { + if (pair.token0.equals(outputAmount.currency) && pair.token0.sellFeeBps?.gt(BigNumber.from(0))) { + const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token0, outputAmount.quotient); + const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); + outputAmount = outputAmountNew; + } else if (pair.token1.equals(outputAmount.currency) && pair.token1.sellFeeBps?.gt(BigNumber.from(0))) { + const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token1, outputAmount.quotient); + const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); + outputAmount = outputAmountNew; + } } else { const [outputAmountNew] = pair.getOutputAmount(outputAmount); outputAmount = outputAmountNew; @@ -98,12 +106,14 @@ export class V2QuoteProvider implements IV2QuoteProvider { for (let i = route.pairs.length - 1; i >= 0; i--) { const pair = route.pairs[i]!; - if (pair.token0.equals(inputAmount.currency) && pair.token0.buyFeeBps?.gt(BigNumber.from(0))) { - const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(pair.token0, inputAmount.quotient); - [inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps); - } else if (pair.token1.equals(inputAmount.currency) && pair.token1.buyFeeBps?.gt(BigNumber.from(0))) { - const inputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token1, inputAmount.quotient); - [inputAmount] = pair.getInputAmount(inputAmountWithSellFeeBps); + if (providerConfig.enableFeeOnTransferFeeFetching) { + if (pair.token0.equals(inputAmount.currency) && pair.token0.buyFeeBps?.gt(BigNumber.from(0))) { + const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(pair.token0, inputAmount.quotient); + [inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps); + } else if (pair.token1.equals(inputAmount.currency) && pair.token1.buyFeeBps?.gt(BigNumber.from(0))) { + const inputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token1, inputAmount.quotient); + [inputAmount] = pair.getInputAmount(inputAmountWithSellFeeBps); + } } else { [inputAmount] = pair.getInputAmount(inputAmount); } diff --git a/src/routers/alpha-router/quoters/v2-quoter.ts b/src/routers/alpha-router/quoters/v2-quoter.ts index 63695eb92..dce1a82a0 100644 --- a/src/routers/alpha-router/quoters/v2-quoter.ts +++ b/src/routers/alpha-router/quoters/v2-quoter.ts @@ -146,7 +146,7 @@ export class V2Quoter extends BaseQuoter { log.info( `Getting quotes for V2 for ${routes.length} routes with ${amounts.length} amounts per route.` ); - const { routesWithQuotes } = await quoteFn(amounts, routes); + const { routesWithQuotes } = await quoteFn(amounts, routes, _routingConfig); const v2GasModel = await this.v2GasModelFactory.buildGasModel({ chainId: this.chainId, From c635829ca5bbfb59a1dd46ac88a6487e8145d8c7 Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:38:02 -0700 Subject: [PATCH 02/10] add safeguard in quote provider, as well as unit test skeleton --- src/providers/v2/quote-provider.ts | 48 +++++++++++++++++-- test/unit/providers/v2/quote-provider.test.ts | 20 ++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 test/unit/providers/v2/quote-provider.test.ts diff --git a/src/providers/v2/quote-provider.ts b/src/providers/v2/quote-provider.ts index 1506d38f8..3c65c9b32 100644 --- a/src/providers/v2/quote-provider.ts +++ b/src/providers/v2/quote-provider.ts @@ -8,6 +8,7 @@ import { import { V2Route } from '../../routers/router'; import { CurrencyAmount } from '../../util/amounts'; import { log } from '../../util/log'; +import { metric, MetricLoggerUnit } from '../../util/metric'; import { routeToString } from '../../util/routes'; import { ProviderConfig } from '../provider'; @@ -81,7 +82,24 @@ export class V2QuoteProvider implements IV2QuoteProvider { let outputAmount = amount.wrapped; for (const pair of route.pairs) { - if (providerConfig.enableFeeOnTransferFeeFetching) { + if (outputAmount.currency.sellFeeBps) { + // this should never happen, but just in case it happens, + // there is a bug in sor. We need to log this and investigate. + const error = new Error(`Sell fee bps should not exist on output amount + ${JSON.stringify(outputAmount)} on amounts ${JSON.stringify(amounts)} + on routes ${JSON.stringify(routes)}`) + + // artificially create error object and pass in log.error so that + // it also log the stack trace + log.error({ error }, 'Sell fee bps should not exist on output amount') + metric.putMetric('V2_QUOTE_PROVIDER_INCONSISTENT_SELL_FEE_BPS_VS_FEATURE_FLAG', 1, MetricLoggerUnit.Count) + } + + // outputAmount.currency.sellFeeBps is extra safeguard, just in case there's a bug + // in sor that causes outputAmount.currency.sellFeeBps to be defined + // If it happens, since we get routes from cached routes most of the time + // the FOT quote will become smaller and smaller, which will be difficult to self-correct in prod + if (providerConfig.enableFeeOnTransferFeeFetching && !outputAmount.currency.sellFeeBps) { if (pair.token0.equals(outputAmount.currency) && pair.token0.sellFeeBps?.gt(BigNumber.from(0))) { const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token0, outputAmount.quotient); const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); @@ -90,6 +108,9 @@ export class V2QuoteProvider implements IV2QuoteProvider { const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token1, outputAmount.quotient); const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); outputAmount = outputAmountNew; + } else { + const [outputAmountNew] = pair.getOutputAmount(outputAmount); + outputAmount = outputAmountNew; } } else { const [outputAmountNew] = pair.getOutputAmount(outputAmount); @@ -106,13 +127,32 @@ export class V2QuoteProvider implements IV2QuoteProvider { for (let i = route.pairs.length - 1; i >= 0; i--) { const pair = route.pairs[i]!; - if (providerConfig.enableFeeOnTransferFeeFetching) { + if (inputAmount.currency.buyFeeBps) { + // this should never happen, but just in case it happens, + // there is a bug in sor. We need to log this and investigate. + const error = new Error(`Buy fee bps should not exist on input amount + ${JSON.stringify(inputAmount)} on amounts ${JSON.stringify(amounts)} + on routes ${JSON.stringify(routes)}`) + + // artificially create error object and pass in log.error so that + // it also log the stack trace + log.error({ error }, 'Buy fee bps should not exist on input amount') + metric.putMetric('V2_QUOTE_PROVIDER_INCONSISTENT_BUY_FEE_BPS_VS_FEATURE_FLAG', 1, MetricLoggerUnit.Count) + } + + // inputAmount.currency.buyFeeBps is extra safeguard, just in case there's a bug + // in sor that causes inputAmount.currency.buyFeeBps to be defined + // If it happens, since we get routes from cached routes most of the time + // the FOT quote will become larger and larger, which will be difficult to self-correct in prod + if (providerConfig.enableFeeOnTransferFeeFetching && !inputAmount.currency.buyFeeBps) { if (pair.token0.equals(inputAmount.currency) && pair.token0.buyFeeBps?.gt(BigNumber.from(0))) { const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(pair.token0, inputAmount.quotient); [inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps); } else if (pair.token1.equals(inputAmount.currency) && pair.token1.buyFeeBps?.gt(BigNumber.from(0))) { - const inputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token1, inputAmount.quotient); - [inputAmount] = pair.getInputAmount(inputAmountWithSellFeeBps); + const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(pair.token1, inputAmount.quotient); + [inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps); + } else { + [inputAmount] = pair.getInputAmount(inputAmount); } } else { [inputAmount] = pair.getInputAmount(inputAmount); diff --git a/test/unit/providers/v2/quote-provider.test.ts b/test/unit/providers/v2/quote-provider.test.ts new file mode 100644 index 000000000..e5ee5b1e1 --- /dev/null +++ b/test/unit/providers/v2/quote-provider.test.ts @@ -0,0 +1,20 @@ +import { CurrencyAmount, V2QuoteProvider, V2Route } from '../../../../src'; +import { ProviderConfig } from '../../../../src/providers/provider'; + +describe('QuoteProvider', () => { + const currencyAmounts: CurrencyAmount[] = [] + const v2Routes: V2Route[] = [] + + const quoteProvider = new V2QuoteProvider() + + describe('fee-on-transfer flag enabled', async () => { + const providerConfig: ProviderConfig = { enableFeeOnTransferFeeFetching: true } + + it('should return correct quote for exact in', async () => { + + const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(currencyAmounts, v2Routes, providerConfig) + routesWithQuotes + }) + + }) +}) From 89f8f1b6a5d1a916b53248bfdd198e0d4b5b9c2d Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 11:48:08 -0700 Subject: [PATCH 03/10] remove extra safeguard and keep logging only --- src/providers/v2/quote-provider.ts | 20 +++----- test/test-util/mock-data.ts | 49 +++++++++++++++++++ test/unit/providers/v2/quote-provider.test.ts | 47 ++++++++++++++---- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/providers/v2/quote-provider.ts b/src/providers/v2/quote-provider.ts index 3c65c9b32..a681978f5 100644 --- a/src/providers/v2/quote-provider.ts +++ b/src/providers/v2/quote-provider.ts @@ -82,11 +82,11 @@ export class V2QuoteProvider implements IV2QuoteProvider { let outputAmount = amount.wrapped; for (const pair of route.pairs) { - if (outputAmount.currency.sellFeeBps) { + if (amount.wrapped.currency.sellFeeBps) { // this should never happen, but just in case it happens, // there is a bug in sor. We need to log this and investigate. const error = new Error(`Sell fee bps should not exist on output amount - ${JSON.stringify(outputAmount)} on amounts ${JSON.stringify(amounts)} + ${JSON.stringify(amount)} on amounts ${JSON.stringify(amounts)} on routes ${JSON.stringify(routes)}`) // artificially create error object and pass in log.error so that @@ -95,11 +95,7 @@ export class V2QuoteProvider implements IV2QuoteProvider { metric.putMetric('V2_QUOTE_PROVIDER_INCONSISTENT_SELL_FEE_BPS_VS_FEATURE_FLAG', 1, MetricLoggerUnit.Count) } - // outputAmount.currency.sellFeeBps is extra safeguard, just in case there's a bug - // in sor that causes outputAmount.currency.sellFeeBps to be defined - // If it happens, since we get routes from cached routes most of the time - // the FOT quote will become smaller and smaller, which will be difficult to self-correct in prod - if (providerConfig.enableFeeOnTransferFeeFetching && !outputAmount.currency.sellFeeBps) { + if (providerConfig.enableFeeOnTransferFeeFetching) { if (pair.token0.equals(outputAmount.currency) && pair.token0.sellFeeBps?.gt(BigNumber.from(0))) { const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token0, outputAmount.quotient); const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); @@ -127,11 +123,11 @@ export class V2QuoteProvider implements IV2QuoteProvider { for (let i = route.pairs.length - 1; i >= 0; i--) { const pair = route.pairs[i]!; - if (inputAmount.currency.buyFeeBps) { + if (amount.wrapped.currency.buyFeeBps) { // this should never happen, but just in case it happens, // there is a bug in sor. We need to log this and investigate. const error = new Error(`Buy fee bps should not exist on input amount - ${JSON.stringify(inputAmount)} on amounts ${JSON.stringify(amounts)} + ${JSON.stringify(amount)} on amounts ${JSON.stringify(amounts)} on routes ${JSON.stringify(routes)}`) // artificially create error object and pass in log.error so that @@ -140,11 +136,7 @@ export class V2QuoteProvider implements IV2QuoteProvider { metric.putMetric('V2_QUOTE_PROVIDER_INCONSISTENT_BUY_FEE_BPS_VS_FEATURE_FLAG', 1, MetricLoggerUnit.Count) } - // inputAmount.currency.buyFeeBps is extra safeguard, just in case there's a bug - // in sor that causes inputAmount.currency.buyFeeBps to be defined - // If it happens, since we get routes from cached routes most of the time - // the FOT quote will become larger and larger, which will be difficult to self-correct in prod - if (providerConfig.enableFeeOnTransferFeeFetching && !inputAmount.currency.buyFeeBps) { + if (providerConfig.enableFeeOnTransferFeeFetching) { if (pair.token0.equals(inputAmount.currency) && pair.token0.buyFeeBps?.gt(BigNumber.from(0))) { const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(pair.token0, inputAmount.quotient); [inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps); diff --git a/test/test-util/mock-data.ts b/test/test-util/mock-data.ts index e92a07504..87012bb66 100644 --- a/test/test-util/mock-data.ts +++ b/test/test-util/mock-data.ts @@ -361,3 +361,52 @@ export const mockTokenList: TokenList = { }, ], }; + +export const BLAST_WITHOUT_TAX = new Token( + ChainId.MAINNET, + '0x3ed643e9032230f01c6c36060e305ab53ad3b482', + 18, + 'BLAST', + 'BLAST', +) +export const BLAST = new Token( + ChainId.MAINNET, + '0x3ed643e9032230f01c6c36060e305ab53ad3b482', + 18, + 'BLAST', + 'BLAST', + false, + BigNumber.from(400), + BigNumber.from(10000) +) +export const BULLET_WITHOUT_TAX = new Token( + ChainId.MAINNET, + '0x8ef32a03784c8Fd63bBf027251b9620865bD54B6', + 8, + 'BULLET', + 'Bullet Game Betting Token', + false +) +export const BULLET = new Token( + ChainId.MAINNET, + '0x8ef32a03784c8Fd63bBf027251b9620865bD54B6', + 8, + 'BULLET', + 'Bullet Game Betting Token', + false, + BigNumber.from(500), + BigNumber.from(500) +) +/* +// stETH is a special case (rebase token), that would make the token include buyFeeBps and sellFeeBps of 0 as always +const STETH = new Token( + ChainId.MAINNET, + '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + 18, + 'stETH', + 'stETH', + false, + BigNumber.from(0), + BigNumber.from(0) +) + */ diff --git a/test/unit/providers/v2/quote-provider.test.ts b/test/unit/providers/v2/quote-provider.test.ts index e5ee5b1e1..8bb0c51cb 100644 --- a/test/unit/providers/v2/quote-provider.test.ts +++ b/test/unit/providers/v2/quote-provider.test.ts @@ -1,19 +1,48 @@ -import { CurrencyAmount, V2QuoteProvider, V2Route } from '../../../../src'; +import { CurrencyAmount, V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; import { ProviderConfig } from '../../../../src/providers/provider'; +import { BLAST, BLAST_WITHOUT_TAX, BULLET, BULLET_WITHOUT_TAX } from '../../../test-util/mock-data'; +import JSBI from 'jsbi'; +import { ChainId, Fraction } from '@uniswap/sdk-core'; +import { computeAllV2Routes } from '../../../../src/routers/alpha-router/functions/compute-all-routes'; +import { Pair } from '@uniswap/v2-sdk'; -describe('QuoteProvider', () => { - const currencyAmounts: CurrencyAmount[] = [] - const v2Routes: V2Route[] = [] +const tokenIn = BULLET_WITHOUT_TAX +const tokenOut = BLAST_WITHOUT_TAX + +const inputBulletOriginalAmount = JSBI.BigInt(10) +const inputBulletCurrencyAmount = CurrencyAmount.fromRawAmount(tokenIn, JSBI.exponentiate(inputBulletOriginalAmount, JSBI.BigInt(tokenIn.decimals))) +const wethOriginalAmount = JSBI.BigInt(10) +const wethCurrencyAmount = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], JSBI.exponentiate(wethOriginalAmount, JSBI.BigInt(WETH9[ChainId.MAINNET].decimals))) +const blastOriginalAmount = JSBI.BigInt(10) +const blastCurrencyAmount = CurrencyAmount.fromRawAmount(BLAST, JSBI.exponentiate(blastOriginalAmount, JSBI.BigInt(BLAST.decimals))) + +// split input amount by 10%, 40%, 50% +const inputBulletCurrencyAmounts: Array = [ + inputBulletCurrencyAmount.multiply(new Fraction(10, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(40, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(50, 100)), +] + +const amountFactorForReserves = JSBI.BigInt(100) +const bulletReserve = CurrencyAmount.fromRawAmount(BULLET, inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient) +const WETHReserve = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], wethCurrencyAmount.multiply(amountFactorForReserves).quotient) +const bulletWETHPool = new Pair(bulletReserve, WETHReserve) +const blastReserve = CurrencyAmount.fromRawAmount(BLAST, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) +const WETHBlastPool = new Pair(WETHReserve, blastReserve) - const quoteProvider = new V2QuoteProvider() +const pools: Pair[] = [bulletWETHPool, WETHBlastPool] +const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, pools, 3) - describe('fee-on-transfer flag enabled', async () => { +const quoteProvider = new V2QuoteProvider() + +describe('QuoteProvider', () => { + + describe('fee-on-transfer flag enabled', () => { const providerConfig: ProviderConfig = { enableFeeOnTransferFeeFetching: true } it('should return correct quote for exact in', async () => { - - const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(currencyAmounts, v2Routes, providerConfig) - routesWithQuotes + const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) + console.log(JSON.stringify(routesWithQuotes)) }) }) From 866741ce4b80614cd83a3ccda4e2f892056984b7 Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:35:11 -0700 Subject: [PATCH 04/10] quote with valid routes for loop --- test/test-util/mock-data.ts | 4 +--- test/unit/providers/v2/quote-provider.test.ts | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/test/test-util/mock-data.ts b/test/test-util/mock-data.ts index 87012bb66..5c4ee5442 100644 --- a/test/test-util/mock-data.ts +++ b/test/test-util/mock-data.ts @@ -397,9 +397,8 @@ export const BULLET = new Token( BigNumber.from(500), BigNumber.from(500) ) -/* // stETH is a special case (rebase token), that would make the token include buyFeeBps and sellFeeBps of 0 as always -const STETH = new Token( +export const STETH = new Token( ChainId.MAINNET, '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', 18, @@ -409,4 +408,3 @@ const STETH = new Token( BigNumber.from(0), BigNumber.from(0) ) - */ diff --git a/test/unit/providers/v2/quote-provider.test.ts b/test/unit/providers/v2/quote-provider.test.ts index 8bb0c51cb..17966d0f6 100644 --- a/test/unit/providers/v2/quote-provider.test.ts +++ b/test/unit/providers/v2/quote-provider.test.ts @@ -1,6 +1,6 @@ import { CurrencyAmount, V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; import { ProviderConfig } from '../../../../src/providers/provider'; -import { BLAST, BLAST_WITHOUT_TAX, BULLET, BULLET_WITHOUT_TAX } from '../../../test-util/mock-data'; +import { BLAST, BLAST_WITHOUT_TAX, BULLET, BULLET_WITHOUT_TAX, STETH } from '../../../test-util/mock-data'; import JSBI from 'jsbi'; import { ChainId, Fraction } from '@uniswap/sdk-core'; import { computeAllV2Routes } from '../../../../src/routers/alpha-router/functions/compute-all-routes'; @@ -13,6 +13,8 @@ const inputBulletOriginalAmount = JSBI.BigInt(10) const inputBulletCurrencyAmount = CurrencyAmount.fromRawAmount(tokenIn, JSBI.exponentiate(inputBulletOriginalAmount, JSBI.BigInt(tokenIn.decimals))) const wethOriginalAmount = JSBI.BigInt(10) const wethCurrencyAmount = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], JSBI.exponentiate(wethOriginalAmount, JSBI.BigInt(WETH9[ChainId.MAINNET].decimals))) +const stEthOriginalAmount = JSBI.BigInt(10) +const stEthCurrencyAmount = CurrencyAmount.fromRawAmount(STETH, JSBI.exponentiate(stEthOriginalAmount, JSBI.BigInt(STETH.decimals))) const blastOriginalAmount = JSBI.BigInt(10) const blastCurrencyAmount = CurrencyAmount.fromRawAmount(BLAST, JSBI.exponentiate(blastOriginalAmount, JSBI.BigInt(BLAST.decimals))) @@ -29,9 +31,12 @@ const WETHReserve = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], wethCur const bulletWETHPool = new Pair(bulletReserve, WETHReserve) const blastReserve = CurrencyAmount.fromRawAmount(BLAST, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) const WETHBlastPool = new Pair(WETHReserve, blastReserve) +const stETHReserve = CurrencyAmount.fromRawAmount(STETH, stEthCurrencyAmount.multiply(amountFactorForReserves).quotient) +const bulletSTETHPool = new Pair(bulletReserve, stETHReserve) +const stETHBlastPool = new Pair(stETHReserve, blastReserve) -const pools: Pair[] = [bulletWETHPool, WETHBlastPool] -const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, pools, 3) +const pools: Pair[] = [bulletWETHPool, WETHBlastPool, bulletSTETHPool, stETHBlastPool] +const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, pools, 7) const quoteProvider = new V2QuoteProvider() @@ -42,8 +47,10 @@ describe('QuoteProvider', () => { it('should return correct quote for exact in', async () => { const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) - console.log(JSON.stringify(routesWithQuotes)) + expect(routesWithQuotes.length).toEqual(2) + routesWithQuotes.forEach(([route, quote]) => { + route.path + }) }) - }) }) From 8c897356a46ba6502fbe116e6f6b36d935f520cf Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:30:43 -0700 Subject: [PATCH 05/10] complete unit test --- test/test-util/mock-data.ts | 8 ++ test/unit/providers/v2/quote-provider.test.ts | 105 +++++++++++++++--- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/test/test-util/mock-data.ts b/test/test-util/mock-data.ts index 5c4ee5442..c0d71f926 100644 --- a/test/test-util/mock-data.ts +++ b/test/test-util/mock-data.ts @@ -397,6 +397,14 @@ export const BULLET = new Token( BigNumber.from(500), BigNumber.from(500) ) +export const STETH_WITHOUT_TAX = new Token( + ChainId.MAINNET, + '0xae7ab96520de3a18e5e111b5eaab095312d7fe84', + 18, + 'stETH', + 'stETH', + false +) // stETH is a special case (rebase token), that would make the token include buyFeeBps and sellFeeBps of 0 as always export const STETH = new Token( ChainId.MAINNET, diff --git a/test/unit/providers/v2/quote-provider.test.ts b/test/unit/providers/v2/quote-provider.test.ts index 17966d0f6..6396104d8 100644 --- a/test/unit/providers/v2/quote-provider.test.ts +++ b/test/unit/providers/v2/quote-provider.test.ts @@ -1,10 +1,11 @@ -import { CurrencyAmount, V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; +import { V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; import { ProviderConfig } from '../../../../src/providers/provider'; import { BLAST, BLAST_WITHOUT_TAX, BULLET, BULLET_WITHOUT_TAX, STETH } from '../../../test-util/mock-data'; import JSBI from 'jsbi'; -import { ChainId, Fraction } from '@uniswap/sdk-core'; +import { ChainId, CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core'; import { computeAllV2Routes } from '../../../../src/routers/alpha-router/functions/compute-all-routes'; import { Pair } from '@uniswap/v2-sdk'; +import { BigNumber } from 'ethers'; const tokenIn = BULLET_WITHOUT_TAX const tokenOut = BLAST_WITHOUT_TAX @@ -18,38 +19,114 @@ const stEthCurrencyAmount = CurrencyAmount.fromRawAmount(STETH, JSBI.exponentiat const blastOriginalAmount = JSBI.BigInt(10) const blastCurrencyAmount = CurrencyAmount.fromRawAmount(BLAST, JSBI.exponentiate(blastOriginalAmount, JSBI.BigInt(BLAST.decimals))) -// split input amount by 10%, 40%, 50% -const inputBulletCurrencyAmounts: Array = [ +// split input amount by 10%, 20%, 30%, 40% +const inputBulletCurrencyAmounts: Array> = [ inputBulletCurrencyAmount.multiply(new Fraction(10, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(20, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(30, 100)), inputBulletCurrencyAmount.multiply(new Fraction(40, 100)), - inputBulletCurrencyAmount.multiply(new Fraction(50, 100)), ] const amountFactorForReserves = JSBI.BigInt(100) const bulletReserve = CurrencyAmount.fromRawAmount(BULLET, inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient) +const bulletWithoutTaxReserve = CurrencyAmount.fromRawAmount(BULLET_WITHOUT_TAX, inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient) const WETHReserve = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], wethCurrencyAmount.multiply(amountFactorForReserves).quotient) const bulletWETHPool = new Pair(bulletReserve, WETHReserve) +const bulletWithoutTaxWETHPool = new Pair(bulletWithoutTaxReserve, WETHReserve) const blastReserve = CurrencyAmount.fromRawAmount(BLAST, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) +const blastWithoutTaxReserve = CurrencyAmount.fromRawAmount(BLAST_WITHOUT_TAX, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) const WETHBlastPool = new Pair(WETHReserve, blastReserve) +const WETHBlastWithoutTaxPool = new Pair(WETHReserve, blastWithoutTaxReserve) const stETHReserve = CurrencyAmount.fromRawAmount(STETH, stEthCurrencyAmount.multiply(amountFactorForReserves).quotient) +const stETHWithoutTaxReserve = CurrencyAmount.fromRawAmount(STETH, stEthCurrencyAmount.multiply(amountFactorForReserves).quotient) const bulletSTETHPool = new Pair(bulletReserve, stETHReserve) +const bulletWithoutTaxSTETHWithoutTaxPool = new Pair(bulletWithoutTaxReserve, stETHWithoutTaxReserve) const stETHBlastPool = new Pair(stETHReserve, blastReserve) +const stETHWithoutTaxBlastWithoutTaxPool = new Pair(stETHWithoutTaxReserve, blastWithoutTaxReserve) -const pools: Pair[] = [bulletWETHPool, WETHBlastPool, bulletSTETHPool, stETHBlastPool] -const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, pools, 7) +const poolsWithTax: Pair[] = [bulletWETHPool, WETHBlastPool, bulletSTETHPool, stETHBlastPool] +const poolsWithoutTax: Pair[] = [bulletWithoutTaxWETHPool, WETHBlastWithoutTaxPool, bulletWithoutTaxSTETHWithoutTaxPool, stETHWithoutTaxBlastWithoutTaxPool] const quoteProvider = new V2QuoteProvider() describe('QuoteProvider', () => { + const enableFeeOnTransferFeeFetching = [undefined] - describe('fee-on-transfer flag enabled', () => { - const providerConfig: ProviderConfig = { enableFeeOnTransferFeeFetching: true } + enableFeeOnTransferFeeFetching.forEach((enableFeeOnTransferFeeFetching) => { + describe(`fee-on-transfer flag enableFeeOnTransferFeeFetching = ${enableFeeOnTransferFeeFetching}`, () => { + const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, enableFeeOnTransferFeeFetching ? poolsWithTax : poolsWithoutTax, 7) + const providerConfig: ProviderConfig = { enableFeeOnTransferFeeFetching: enableFeeOnTransferFeeFetching } - it('should return correct quote for exact in', async () => { - const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) - expect(routesWithQuotes.length).toEqual(2) - routesWithQuotes.forEach(([route, quote]) => { - route.path + // we are leaving exact out, since fot can't quote exact out + it('should return correct quote for exact in', async () => { + const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) + expect(routesWithQuotes.length).toEqual(2) + + console.log(JSON.stringify(routesWithQuotes)) + + routesWithQuotes.forEach(([route, quote]) => { + expect(quote.length).toEqual(inputBulletCurrencyAmounts.length) + expect(route.path.length).toEqual(3) + + inputBulletCurrencyAmounts.map((inputAmount, index) => { + let currentInputAmount = inputAmount + + for (let i = 0; i < route.path.length - 1; i++) { + const token = route.path[i]! + const nextToken = route.path[i + 1]! + const pair = route.pairs.find((pair) => pair.involvesToken(token) && pair.involvesToken(nextToken))! + + if (pair.reserve0.currency.equals(BULLET) || pair.reserve0.currency.equals(BLAST)) { + if (enableFeeOnTransferFeeFetching) { + expect(pair.reserve0.currency.sellFeeBps).toBeDefined() + expect(pair.reserve0.currency.buyFeeBps).toBeDefined() + } else { + expect(pair.reserve0.currency.sellFeeBps === undefined || pair.reserve0.currency.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() + expect(pair.reserve0.currency.buyFeeBps === undefined || pair.reserve0.currency.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() + } + } + + if (pair.reserve1.currency.equals(BULLET) || pair.reserve1.currency.equals(BLAST)) { + if (enableFeeOnTransferFeeFetching) { + expect(pair.reserve1.currency.sellFeeBps).toBeDefined() + expect(pair.reserve1.currency.buyFeeBps).toBeDefined() + } else { + expect(pair.reserve1.currency.sellFeeBps === undefined || pair.reserve1.currency.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() + expect(pair.reserve1.currency.buyFeeBps === undefined || pair.reserve1.currency.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() + } + } + + const [outputAmount] = pair.getOutputAmount(currentInputAmount) + currentInputAmount = outputAmount + + if (enableFeeOnTransferFeeFetching) { + if (nextToken.equals(tokenOut)) { + expect(nextToken.sellFeeBps).toBeDefined() + expect(nextToken.buyFeeBps).toBeDefined() + } + } else { + expect(nextToken.sellFeeBps === undefined || nextToken.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() + expect(nextToken.buyFeeBps === undefined || nextToken.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() + } + } + + // This is the raw input amount from tokenIn, no fot tax applied + // this is important to assert, since interface expects no fot tax applied + // for tokenIn, see https://www.notion.so/router-sdk-changes-for-fee-on-transfer-support-856392a72df64d628efb7b7a29ed9034?d=8d45715a31364360885eaa7e8bdd3370&pvs=4 + expect(inputAmount.toExact()).toEqual(quote[index]!.amount.toExact()) + + // we need to account for the round down/up during quote, + // 0.001 should be small enough rounding error + // this is the post fot tax quote amount + // this is the most important assertion, since interface & mobile + // uses this post fot tax quote amount to calculate the quote from each route + expect(CurrencyAmount.fromRawAmount(tokenOut, quote[index]!.quote!.toString()).subtract(currentInputAmount) + .lessThan(CurrencyAmount.fromFractionalAmount(tokenOut, 1, 1000))) + + expect(route.input.equals(tokenIn)).toBeTruthy() + expect(route.output.equals(tokenOut)).toBeTruthy() + }) + }) }) }) }) From dff167c1483a4f4249328209a1d158363e42d388 Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:37:00 -0700 Subject: [PATCH 06/10] revert the cached routes getting fot tax for tokenin and tokenout --- src/routers/alpha-router/alpha-router.ts | 34 ++---------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 069253574..0de3026cd 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -1178,42 +1178,12 @@ export class AlphaRouter cacheMode !== CacheMode.Darkmode && swapRouteFromChain ) { - const tokenPropertiesMap = await this.tokenPropertiesProvider.getTokensProperties([tokenIn, tokenOut], providerConfig); - - const tokenInWithFotTax = - (tokenPropertiesMap[tokenIn.address.toLowerCase()] - ?.tokenValidationResult === TokenValidationResult.FOT) ? - new Token( - tokenIn.chainId, - tokenIn.address, - tokenIn.decimals, - tokenIn.symbol, - tokenIn.name, - true, // at this point we know it's valid token address - tokenPropertiesMap[tokenIn.address.toLowerCase()]?.tokenFeeResult?.buyFeeBps, - tokenPropertiesMap[tokenIn.address.toLowerCase()]?.tokenFeeResult?.sellFeeBps - ) : tokenIn; - - const tokenOutWithFotTax = - (tokenPropertiesMap[tokenOut.address.toLowerCase()] - ?.tokenValidationResult === TokenValidationResult.FOT) ? - new Token( - tokenOut.chainId, - tokenOut.address, - tokenOut.decimals, - tokenOut.symbol, - tokenOut.name, - true, // at this point we know it's valid token address - tokenPropertiesMap[tokenOut.address.toLowerCase()]?.tokenFeeResult?.buyFeeBps, - tokenPropertiesMap[tokenOut.address.toLowerCase()]?.tokenFeeResult?.sellFeeBps - ) : tokenOut; - // Generate the object to be cached const routesToCache = CachedRoutes.fromRoutesWithValidQuotes( swapRouteFromChain.routes, this.chainId, - tokenInWithFotTax, - tokenOutWithFotTax, + tokenIn, + tokenOut, protocols.sort(), // sort it for consistency in the order of the protocols. await blockNumber, tradeType, From 58d821d38bf4c5d60a19b0e2b1de61dc992399a5 Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:40:03 -0700 Subject: [PATCH 07/10] fix pretty --- src/routers/alpha-router/alpha-router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 0de3026cd..b9bd02af7 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -35,7 +35,7 @@ import { StaticV2SubgraphProvider, StaticV3SubgraphProvider, SwapRouterProvider, - TokenPropertiesProvider, TokenValidationResult, + TokenPropertiesProvider, UniswapMulticallProvider, URISubgraphProvider, V2QuoteProvider, From 0043b3d76192b327141bc3bd762d6730675cb4a6 Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:41:46 -0700 Subject: [PATCH 08/10] prettier --- src/providers/v2/quote-provider.ts | 98 +++++++++++++++---- src/routers/alpha-router/quoters/v2-quoter.ts | 13 ++- 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/providers/v2/quote-provider.ts b/src/providers/v2/quote-provider.ts index a681978f5..e03d67906 100644 --- a/src/providers/v2/quote-provider.ts +++ b/src/providers/v2/quote-provider.ts @@ -51,7 +51,12 @@ export class V2QuoteProvider implements IV2QuoteProvider { routes: V2Route[], providerConfig: ProviderConfig ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }> { - return this.getQuotes(amountIns, routes, TradeType.EXACT_INPUT, providerConfig); + return this.getQuotes( + amountIns, + routes, + TradeType.EXACT_INPUT, + providerConfig + ); } public async getQuotesManyExactOut( @@ -59,14 +64,19 @@ export class V2QuoteProvider implements IV2QuoteProvider { routes: V2Route[], providerConfig: ProviderConfig ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }> { - return this.getQuotes(amountOuts, routes, TradeType.EXACT_OUTPUT, providerConfig); + return this.getQuotes( + amountOuts, + routes, + TradeType.EXACT_OUTPUT, + providerConfig + ); } private async getQuotes( amounts: CurrencyAmount[], routes: V2Route[], tradeType: TradeType, - providerConfig: ProviderConfig, + providerConfig: ProviderConfig ): Promise<{ routesWithQuotes: V2RouteWithQuotes[] }> { const routesWithQuotes: V2RouteWithQuotes[] = []; @@ -85,24 +95,50 @@ export class V2QuoteProvider implements IV2QuoteProvider { if (amount.wrapped.currency.sellFeeBps) { // this should never happen, but just in case it happens, // there is a bug in sor. We need to log this and investigate. - const error = new Error(`Sell fee bps should not exist on output amount + const error = + new Error(`Sell fee bps should not exist on output amount ${JSON.stringify(amount)} on amounts ${JSON.stringify(amounts)} - on routes ${JSON.stringify(routes)}`) + on routes ${JSON.stringify(routes)}`); // artificially create error object and pass in log.error so that // it also log the stack trace - log.error({ error }, 'Sell fee bps should not exist on output amount') - metric.putMetric('V2_QUOTE_PROVIDER_INCONSISTENT_SELL_FEE_BPS_VS_FEATURE_FLAG', 1, MetricLoggerUnit.Count) + log.error( + { error }, + 'Sell fee bps should not exist on output amount' + ); + metric.putMetric( + 'V2_QUOTE_PROVIDER_INCONSISTENT_SELL_FEE_BPS_VS_FEATURE_FLAG', + 1, + MetricLoggerUnit.Count + ); } if (providerConfig.enableFeeOnTransferFeeFetching) { - if (pair.token0.equals(outputAmount.currency) && pair.token0.sellFeeBps?.gt(BigNumber.from(0))) { - const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token0, outputAmount.quotient); - const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); + if ( + pair.token0.equals(outputAmount.currency) && + pair.token0.sellFeeBps?.gt(BigNumber.from(0)) + ) { + const outputAmountWithSellFeeBps = + CurrencyAmount.fromRawAmount( + pair.token0, + outputAmount.quotient + ); + const [outputAmountNew] = pair.getOutputAmount( + outputAmountWithSellFeeBps + ); outputAmount = outputAmountNew; - } else if (pair.token1.equals(outputAmount.currency) && pair.token1.sellFeeBps?.gt(BigNumber.from(0))) { - const outputAmountWithSellFeeBps = CurrencyAmount.fromRawAmount(pair.token1, outputAmount.quotient); - const [outputAmountNew] = pair.getOutputAmount(outputAmountWithSellFeeBps); + } else if ( + pair.token1.equals(outputAmount.currency) && + pair.token1.sellFeeBps?.gt(BigNumber.from(0)) + ) { + const outputAmountWithSellFeeBps = + CurrencyAmount.fromRawAmount( + pair.token1, + outputAmount.quotient + ); + const [outputAmountNew] = pair.getOutputAmount( + outputAmountWithSellFeeBps + ); outputAmount = outputAmountNew; } else { const [outputAmountNew] = pair.getOutputAmount(outputAmount); @@ -126,22 +162,42 @@ export class V2QuoteProvider implements IV2QuoteProvider { if (amount.wrapped.currency.buyFeeBps) { // this should never happen, but just in case it happens, // there is a bug in sor. We need to log this and investigate. - const error = new Error(`Buy fee bps should not exist on input amount + const error = + new Error(`Buy fee bps should not exist on input amount ${JSON.stringify(amount)} on amounts ${JSON.stringify(amounts)} - on routes ${JSON.stringify(routes)}`) + on routes ${JSON.stringify(routes)}`); // artificially create error object and pass in log.error so that // it also log the stack trace - log.error({ error }, 'Buy fee bps should not exist on input amount') - metric.putMetric('V2_QUOTE_PROVIDER_INCONSISTENT_BUY_FEE_BPS_VS_FEATURE_FLAG', 1, MetricLoggerUnit.Count) + log.error( + { error }, + 'Buy fee bps should not exist on input amount' + ); + metric.putMetric( + 'V2_QUOTE_PROVIDER_INCONSISTENT_BUY_FEE_BPS_VS_FEATURE_FLAG', + 1, + MetricLoggerUnit.Count + ); } if (providerConfig.enableFeeOnTransferFeeFetching) { - if (pair.token0.equals(inputAmount.currency) && pair.token0.buyFeeBps?.gt(BigNumber.from(0))) { - const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(pair.token0, inputAmount.quotient); + if ( + pair.token0.equals(inputAmount.currency) && + pair.token0.buyFeeBps?.gt(BigNumber.from(0)) + ) { + const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount( + pair.token0, + inputAmount.quotient + ); [inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps); - } else if (pair.token1.equals(inputAmount.currency) && pair.token1.buyFeeBps?.gt(BigNumber.from(0))) { - const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(pair.token1, inputAmount.quotient); + } else if ( + pair.token1.equals(inputAmount.currency) && + pair.token1.buyFeeBps?.gt(BigNumber.from(0)) + ) { + const inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount( + pair.token1, + inputAmount.quotient + ); [inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps); } else { [inputAmount] = pair.getInputAmount(inputAmount); diff --git a/src/routers/alpha-router/quoters/v2-quoter.ts b/src/routers/alpha-router/quoters/v2-quoter.ts index dce1a82a0..50ba548ae 100644 --- a/src/routers/alpha-router/quoters/v2-quoter.ts +++ b/src/routers/alpha-router/quoters/v2-quoter.ts @@ -12,12 +12,21 @@ import { IV2SubgraphProvider, TokenValidationResult, } from '../../../providers'; -import { CurrencyAmount, log, metric, MetricLoggerUnit, routeToString, } from '../../../util'; +import { + CurrencyAmount, + log, + metric, + MetricLoggerUnit, + routeToString, +} from '../../../util'; import { V2Route } from '../../router'; import { AlphaRouterConfig } from '../alpha-router'; import { V2RouteWithValidQuote } from '../entities'; import { computeAllV2Routes } from '../functions/compute-all-routes'; -import { CandidatePoolsBySelectionCriteria, V2CandidatePools, } from '../functions/get-candidate-pools'; +import { + CandidatePoolsBySelectionCriteria, + V2CandidatePools, +} from '../functions/get-candidate-pools'; import { IGasModel, IV2GasModelFactory } from '../gas-models'; import { BaseQuoter } from './base-quoter'; From 1d275d6a864e4b05cfe063cfbfbc47c063e0a486 Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 21:00:14 -0700 Subject: [PATCH 09/10] add enableFeeOnTransferFeeFetching missing values = true and false --- test/unit/providers/v2/quote-provider.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/unit/providers/v2/quote-provider.test.ts b/test/unit/providers/v2/quote-provider.test.ts index 6396104d8..b06124da6 100644 --- a/test/unit/providers/v2/quote-provider.test.ts +++ b/test/unit/providers/v2/quote-provider.test.ts @@ -50,7 +50,7 @@ const poolsWithoutTax: Pair[] = [bulletWithoutTaxWETHPool, WETHBlastWithoutTaxPo const quoteProvider = new V2QuoteProvider() describe('QuoteProvider', () => { - const enableFeeOnTransferFeeFetching = [undefined] + const enableFeeOnTransferFeeFetching = [true, false, undefined] enableFeeOnTransferFeeFetching.forEach((enableFeeOnTransferFeeFetching) => { describe(`fee-on-transfer flag enableFeeOnTransferFeeFetching = ${enableFeeOnTransferFeeFetching}`, () => { @@ -61,9 +61,7 @@ describe('QuoteProvider', () => { it('should return correct quote for exact in', async () => { const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) expect(routesWithQuotes.length).toEqual(2) - - console.log(JSON.stringify(routesWithQuotes)) - + routesWithQuotes.forEach(([route, quote]) => { expect(quote.length).toEqual(inputBulletCurrencyAmounts.length) expect(route.path.length).toEqual(3) From 0527a4c8ebf0f5623b8070c65c8353f3132e8093 Mon Sep 17 00:00:00 2001 From: Siyu Jiang <91580504+jsy1218@users.noreply.github.com> Date: Thu, 14 Sep 2023 21:02:05 -0700 Subject: [PATCH 10/10] prettier test/unit/providers/v2/quote-provider.test.ts --- test/unit/providers/v2/quote-provider.test.ts | 352 ++++++++++++------ 1 file changed, 229 insertions(+), 123 deletions(-) diff --git a/test/unit/providers/v2/quote-provider.test.ts b/test/unit/providers/v2/quote-provider.test.ts index b06124da6..28753726c 100644 --- a/test/unit/providers/v2/quote-provider.test.ts +++ b/test/unit/providers/v2/quote-provider.test.ts @@ -1,131 +1,237 @@ -import { V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; -import { ProviderConfig } from '../../../../src/providers/provider'; -import { BLAST, BLAST_WITHOUT_TAX, BULLET, BULLET_WITHOUT_TAX, STETH } from '../../../test-util/mock-data'; -import JSBI from 'jsbi'; import { ChainId, CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core'; -import { computeAllV2Routes } from '../../../../src/routers/alpha-router/functions/compute-all-routes'; import { Pair } from '@uniswap/v2-sdk'; import { BigNumber } from 'ethers'; - -const tokenIn = BULLET_WITHOUT_TAX -const tokenOut = BLAST_WITHOUT_TAX - -const inputBulletOriginalAmount = JSBI.BigInt(10) -const inputBulletCurrencyAmount = CurrencyAmount.fromRawAmount(tokenIn, JSBI.exponentiate(inputBulletOriginalAmount, JSBI.BigInt(tokenIn.decimals))) -const wethOriginalAmount = JSBI.BigInt(10) -const wethCurrencyAmount = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], JSBI.exponentiate(wethOriginalAmount, JSBI.BigInt(WETH9[ChainId.MAINNET].decimals))) -const stEthOriginalAmount = JSBI.BigInt(10) -const stEthCurrencyAmount = CurrencyAmount.fromRawAmount(STETH, JSBI.exponentiate(stEthOriginalAmount, JSBI.BigInt(STETH.decimals))) -const blastOriginalAmount = JSBI.BigInt(10) -const blastCurrencyAmount = CurrencyAmount.fromRawAmount(BLAST, JSBI.exponentiate(blastOriginalAmount, JSBI.BigInt(BLAST.decimals))) +import JSBI from 'jsbi'; +import { V2QuoteProvider, V2Route, WETH9 } from '../../../../src'; +import { ProviderConfig } from '../../../../src/providers/provider'; +import { computeAllV2Routes } from '../../../../src/routers/alpha-router/functions/compute-all-routes'; +import { + BLAST, + BLAST_WITHOUT_TAX, + BULLET, + BULLET_WITHOUT_TAX, + STETH, +} from '../../../test-util/mock-data'; + +const tokenIn = BULLET_WITHOUT_TAX; +const tokenOut = BLAST_WITHOUT_TAX; + +const inputBulletOriginalAmount = JSBI.BigInt(10); +const inputBulletCurrencyAmount = CurrencyAmount.fromRawAmount( + tokenIn, + JSBI.exponentiate(inputBulletOriginalAmount, JSBI.BigInt(tokenIn.decimals)) +); +const wethOriginalAmount = JSBI.BigInt(10); +const wethCurrencyAmount = CurrencyAmount.fromRawAmount( + WETH9[ChainId.MAINNET], + JSBI.exponentiate( + wethOriginalAmount, + JSBI.BigInt(WETH9[ChainId.MAINNET].decimals) + ) +); +const stEthOriginalAmount = JSBI.BigInt(10); +const stEthCurrencyAmount = CurrencyAmount.fromRawAmount( + STETH, + JSBI.exponentiate(stEthOriginalAmount, JSBI.BigInt(STETH.decimals)) +); +const blastOriginalAmount = JSBI.BigInt(10); +const blastCurrencyAmount = CurrencyAmount.fromRawAmount( + BLAST, + JSBI.exponentiate(blastOriginalAmount, JSBI.BigInt(BLAST.decimals)) +); // split input amount by 10%, 20%, 30%, 40% const inputBulletCurrencyAmounts: Array> = [ - inputBulletCurrencyAmount.multiply(new Fraction(10, 100)), - inputBulletCurrencyAmount.multiply(new Fraction(20, 100)), - inputBulletCurrencyAmount.multiply(new Fraction(30, 100)), - inputBulletCurrencyAmount.multiply(new Fraction(40, 100)), -] - -const amountFactorForReserves = JSBI.BigInt(100) -const bulletReserve = CurrencyAmount.fromRawAmount(BULLET, inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient) -const bulletWithoutTaxReserve = CurrencyAmount.fromRawAmount(BULLET_WITHOUT_TAX, inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient) -const WETHReserve = CurrencyAmount.fromRawAmount(WETH9[ChainId.MAINNET], wethCurrencyAmount.multiply(amountFactorForReserves).quotient) -const bulletWETHPool = new Pair(bulletReserve, WETHReserve) -const bulletWithoutTaxWETHPool = new Pair(bulletWithoutTaxReserve, WETHReserve) -const blastReserve = CurrencyAmount.fromRawAmount(BLAST, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) -const blastWithoutTaxReserve = CurrencyAmount.fromRawAmount(BLAST_WITHOUT_TAX, blastCurrencyAmount.multiply(amountFactorForReserves).quotient) -const WETHBlastPool = new Pair(WETHReserve, blastReserve) -const WETHBlastWithoutTaxPool = new Pair(WETHReserve, blastWithoutTaxReserve) -const stETHReserve = CurrencyAmount.fromRawAmount(STETH, stEthCurrencyAmount.multiply(amountFactorForReserves).quotient) -const stETHWithoutTaxReserve = CurrencyAmount.fromRawAmount(STETH, stEthCurrencyAmount.multiply(amountFactorForReserves).quotient) -const bulletSTETHPool = new Pair(bulletReserve, stETHReserve) -const bulletWithoutTaxSTETHWithoutTaxPool = new Pair(bulletWithoutTaxReserve, stETHWithoutTaxReserve) -const stETHBlastPool = new Pair(stETHReserve, blastReserve) -const stETHWithoutTaxBlastWithoutTaxPool = new Pair(stETHWithoutTaxReserve, blastWithoutTaxReserve) - -const poolsWithTax: Pair[] = [bulletWETHPool, WETHBlastPool, bulletSTETHPool, stETHBlastPool] -const poolsWithoutTax: Pair[] = [bulletWithoutTaxWETHPool, WETHBlastWithoutTaxPool, bulletWithoutTaxSTETHWithoutTaxPool, stETHWithoutTaxBlastWithoutTaxPool] - -const quoteProvider = new V2QuoteProvider() + inputBulletCurrencyAmount.multiply(new Fraction(10, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(20, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(30, 100)), + inputBulletCurrencyAmount.multiply(new Fraction(40, 100)), +]; + +const amountFactorForReserves = JSBI.BigInt(100); +const bulletReserve = CurrencyAmount.fromRawAmount( + BULLET, + inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient +); +const bulletWithoutTaxReserve = CurrencyAmount.fromRawAmount( + BULLET_WITHOUT_TAX, + inputBulletCurrencyAmount.multiply(amountFactorForReserves).quotient +); +const WETHReserve = CurrencyAmount.fromRawAmount( + WETH9[ChainId.MAINNET], + wethCurrencyAmount.multiply(amountFactorForReserves).quotient +); +const bulletWETHPool = new Pair(bulletReserve, WETHReserve); +const bulletWithoutTaxWETHPool = new Pair(bulletWithoutTaxReserve, WETHReserve); +const blastReserve = CurrencyAmount.fromRawAmount( + BLAST, + blastCurrencyAmount.multiply(amountFactorForReserves).quotient +); +const blastWithoutTaxReserve = CurrencyAmount.fromRawAmount( + BLAST_WITHOUT_TAX, + blastCurrencyAmount.multiply(amountFactorForReserves).quotient +); +const WETHBlastPool = new Pair(WETHReserve, blastReserve); +const WETHBlastWithoutTaxPool = new Pair(WETHReserve, blastWithoutTaxReserve); +const stETHReserve = CurrencyAmount.fromRawAmount( + STETH, + stEthCurrencyAmount.multiply(amountFactorForReserves).quotient +); +const stETHWithoutTaxReserve = CurrencyAmount.fromRawAmount( + STETH, + stEthCurrencyAmount.multiply(amountFactorForReserves).quotient +); +const bulletSTETHPool = new Pair(bulletReserve, stETHReserve); +const bulletWithoutTaxSTETHWithoutTaxPool = new Pair( + bulletWithoutTaxReserve, + stETHWithoutTaxReserve +); +const stETHBlastPool = new Pair(stETHReserve, blastReserve); +const stETHWithoutTaxBlastWithoutTaxPool = new Pair( + stETHWithoutTaxReserve, + blastWithoutTaxReserve +); + +const poolsWithTax: Pair[] = [ + bulletWETHPool, + WETHBlastPool, + bulletSTETHPool, + stETHBlastPool, +]; +const poolsWithoutTax: Pair[] = [ + bulletWithoutTaxWETHPool, + WETHBlastWithoutTaxPool, + bulletWithoutTaxSTETHWithoutTaxPool, + stETHWithoutTaxBlastWithoutTaxPool, +]; + +const quoteProvider = new V2QuoteProvider(); describe('QuoteProvider', () => { - const enableFeeOnTransferFeeFetching = [true, false, undefined] - - enableFeeOnTransferFeeFetching.forEach((enableFeeOnTransferFeeFetching) => { - describe(`fee-on-transfer flag enableFeeOnTransferFeeFetching = ${enableFeeOnTransferFeeFetching}`, () => { - const v2Routes: Array = computeAllV2Routes(tokenIn, tokenOut, enableFeeOnTransferFeeFetching ? poolsWithTax : poolsWithoutTax, 7) - const providerConfig: ProviderConfig = { enableFeeOnTransferFeeFetching: enableFeeOnTransferFeeFetching } - - // we are leaving exact out, since fot can't quote exact out - it('should return correct quote for exact in', async () => { - const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn(inputBulletCurrencyAmounts, v2Routes, providerConfig) - expect(routesWithQuotes.length).toEqual(2) - - routesWithQuotes.forEach(([route, quote]) => { - expect(quote.length).toEqual(inputBulletCurrencyAmounts.length) - expect(route.path.length).toEqual(3) - - inputBulletCurrencyAmounts.map((inputAmount, index) => { - let currentInputAmount = inputAmount - - for (let i = 0; i < route.path.length - 1; i++) { - const token = route.path[i]! - const nextToken = route.path[i + 1]! - const pair = route.pairs.find((pair) => pair.involvesToken(token) && pair.involvesToken(nextToken))! - - if (pair.reserve0.currency.equals(BULLET) || pair.reserve0.currency.equals(BLAST)) { - if (enableFeeOnTransferFeeFetching) { - expect(pair.reserve0.currency.sellFeeBps).toBeDefined() - expect(pair.reserve0.currency.buyFeeBps).toBeDefined() - } else { - expect(pair.reserve0.currency.sellFeeBps === undefined || pair.reserve0.currency.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() - expect(pair.reserve0.currency.buyFeeBps === undefined || pair.reserve0.currency.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() - } - } - - if (pair.reserve1.currency.equals(BULLET) || pair.reserve1.currency.equals(BLAST)) { - if (enableFeeOnTransferFeeFetching) { - expect(pair.reserve1.currency.sellFeeBps).toBeDefined() - expect(pair.reserve1.currency.buyFeeBps).toBeDefined() - } else { - expect(pair.reserve1.currency.sellFeeBps === undefined || pair.reserve1.currency.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() - expect(pair.reserve1.currency.buyFeeBps === undefined || pair.reserve1.currency.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() - } - } - - const [outputAmount] = pair.getOutputAmount(currentInputAmount) - currentInputAmount = outputAmount - - if (enableFeeOnTransferFeeFetching) { - if (nextToken.equals(tokenOut)) { - expect(nextToken.sellFeeBps).toBeDefined() - expect(nextToken.buyFeeBps).toBeDefined() - } - } else { - expect(nextToken.sellFeeBps === undefined || nextToken.sellFeeBps.eq(BigNumber.from(0))).toBeTruthy() - expect(nextToken.buyFeeBps === undefined || nextToken.buyFeeBps.eq(BigNumber.from(0))).toBeTruthy() - } - } - - // This is the raw input amount from tokenIn, no fot tax applied - // this is important to assert, since interface expects no fot tax applied - // for tokenIn, see https://www.notion.so/router-sdk-changes-for-fee-on-transfer-support-856392a72df64d628efb7b7a29ed9034?d=8d45715a31364360885eaa7e8bdd3370&pvs=4 - expect(inputAmount.toExact()).toEqual(quote[index]!.amount.toExact()) - - // we need to account for the round down/up during quote, - // 0.001 should be small enough rounding error - // this is the post fot tax quote amount - // this is the most important assertion, since interface & mobile - // uses this post fot tax quote amount to calculate the quote from each route - expect(CurrencyAmount.fromRawAmount(tokenOut, quote[index]!.quote!.toString()).subtract(currentInputAmount) - .lessThan(CurrencyAmount.fromFractionalAmount(tokenOut, 1, 1000))) - - expect(route.input.equals(tokenIn)).toBeTruthy() - expect(route.output.equals(tokenOut)).toBeTruthy() - }) - }) - }) - }) - }) -}) + const enableFeeOnTransferFeeFetching = [true, false, undefined]; + + enableFeeOnTransferFeeFetching.forEach((enableFeeOnTransferFeeFetching) => { + describe(`fee-on-transfer flag enableFeeOnTransferFeeFetching = ${enableFeeOnTransferFeeFetching}`, () => { + const v2Routes: Array = computeAllV2Routes( + tokenIn, + tokenOut, + enableFeeOnTransferFeeFetching ? poolsWithTax : poolsWithoutTax, + 7 + ); + const providerConfig: ProviderConfig = { + enableFeeOnTransferFeeFetching: enableFeeOnTransferFeeFetching, + }; + + // we are leaving exact out, since fot can't quote exact out + it('should return correct quote for exact in', async () => { + const { routesWithQuotes } = await quoteProvider.getQuotesManyExactIn( + inputBulletCurrencyAmounts, + v2Routes, + providerConfig + ); + expect(routesWithQuotes.length).toEqual(2); + + routesWithQuotes.forEach(([route, quote]) => { + expect(quote.length).toEqual(inputBulletCurrencyAmounts.length); + expect(route.path.length).toEqual(3); + + inputBulletCurrencyAmounts.map((inputAmount, index) => { + let currentInputAmount = inputAmount; + + for (let i = 0; i < route.path.length - 1; i++) { + const token = route.path[i]!; + const nextToken = route.path[i + 1]!; + const pair = route.pairs.find( + (pair) => + pair.involvesToken(token) && pair.involvesToken(nextToken) + )!; + + if ( + pair.reserve0.currency.equals(BULLET) || + pair.reserve0.currency.equals(BLAST) + ) { + if (enableFeeOnTransferFeeFetching) { + expect(pair.reserve0.currency.sellFeeBps).toBeDefined(); + expect(pair.reserve0.currency.buyFeeBps).toBeDefined(); + } else { + expect( + pair.reserve0.currency.sellFeeBps === undefined || + pair.reserve0.currency.sellFeeBps.eq(BigNumber.from(0)) + ).toBeTruthy(); + expect( + pair.reserve0.currency.buyFeeBps === undefined || + pair.reserve0.currency.buyFeeBps.eq(BigNumber.from(0)) + ).toBeTruthy(); + } + } + + if ( + pair.reserve1.currency.equals(BULLET) || + pair.reserve1.currency.equals(BLAST) + ) { + if (enableFeeOnTransferFeeFetching) { + expect(pair.reserve1.currency.sellFeeBps).toBeDefined(); + expect(pair.reserve1.currency.buyFeeBps).toBeDefined(); + } else { + expect( + pair.reserve1.currency.sellFeeBps === undefined || + pair.reserve1.currency.sellFeeBps.eq(BigNumber.from(0)) + ).toBeTruthy(); + expect( + pair.reserve1.currency.buyFeeBps === undefined || + pair.reserve1.currency.buyFeeBps.eq(BigNumber.from(0)) + ).toBeTruthy(); + } + } + + const [outputAmount] = pair.getOutputAmount(currentInputAmount); + currentInputAmount = outputAmount; + + if (enableFeeOnTransferFeeFetching) { + if (nextToken.equals(tokenOut)) { + expect(nextToken.sellFeeBps).toBeDefined(); + expect(nextToken.buyFeeBps).toBeDefined(); + } + } else { + expect( + nextToken.sellFeeBps === undefined || + nextToken.sellFeeBps.eq(BigNumber.from(0)) + ).toBeTruthy(); + expect( + nextToken.buyFeeBps === undefined || + nextToken.buyFeeBps.eq(BigNumber.from(0)) + ).toBeTruthy(); + } + } + + // This is the raw input amount from tokenIn, no fot tax applied + // this is important to assert, since interface expects no fot tax applied + // for tokenIn, see https://www.notion.so/router-sdk-changes-for-fee-on-transfer-support-856392a72df64d628efb7b7a29ed9034?d=8d45715a31364360885eaa7e8bdd3370&pvs=4 + expect(inputAmount.toExact()).toEqual( + quote[index]!.amount.toExact() + ); + + // we need to account for the round down/up during quote, + // 0.001 should be small enough rounding error + // this is the post fot tax quote amount + // this is the most important assertion, since interface & mobile + // uses this post fot tax quote amount to calculate the quote from each route + expect( + CurrencyAmount.fromRawAmount( + tokenOut, + quote[index]!.quote!.toString() + ) + .subtract(currentInputAmount) + .lessThan( + CurrencyAmount.fromFractionalAmount(tokenOut, 1, 1000) + ) + ); + + expect(route.input.equals(tokenIn)).toBeTruthy(); + expect(route.output.equals(tokenOut)).toBeTruthy(); + }); + }); + }); + }); + }); +});