diff --git a/src/providers/portion-provider.ts b/src/providers/portion-provider.ts index 766f4ad1e..a8214f43f 100644 --- a/src/providers/portion-provider.ts +++ b/src/providers/portion-provider.ts @@ -22,6 +22,7 @@ export interface IPortionProvider { getPortionAmount( tokenOutAmount: CurrencyAmount, tradeType: TradeType, + tokenOutHasFot?: boolean, swapConfig?: SwapOptions ): CurrencyAmount | undefined; @@ -116,9 +117,10 @@ export class PortionProvider implements IPortionProvider { getPortionAmount( tokenOutAmount: CurrencyAmount, tradeType: TradeType, + tokenOutHasFot?: boolean, swapConfig?: SwapOptions ): CurrencyAmount | undefined { - if (swapConfig?.type !== SwapType.UNIVERSAL_ROUTER) { + if (tokenOutHasFot || swapConfig?.type !== SwapType.UNIVERSAL_ROUTER) { return undefined; } @@ -202,9 +204,13 @@ export class PortionProvider implements IPortionProvider { } return routeWithQuotes.map((routeWithQuote) => { + const tokenOut = + routeWithQuote.tokenPath[routeWithQuote.tokenPath.length - 1]; + const tokenOutHasFot = tokenOut && tokenOut.buyFeeBps?.gt(0); const portionAmount = this.getPortionAmount( routeWithQuote.quote, tradeType, + tokenOutHasFot, swapConfig ); diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 86e64439b..4aca46243 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -1039,10 +1039,30 @@ export class AlphaRouter partialRoutingConfig: Partial = {} ): Promise { const originalAmount = amount; + + const { currencyIn, currencyOut } = + this.determineCurrencyInOutFromTradeType( + tradeType, + amount, + quoteCurrency + ); + + const tokenIn = currencyIn.wrapped; + const tokenOut = currencyOut.wrapped; + + const tokenOutProperties = + await this.tokenPropertiesProvider.getTokensProperties( + [tokenOut], + partialRoutingConfig + ); + const buyFeeBps = tokenOutProperties[tokenOut.address.toLowerCase()]?.tokenFeeResult?.buyFeeBps; + const tokenOutHasFot = buyFeeBps && buyFeeBps.gt(0); + if (tradeType === TradeType.EXACT_OUTPUT) { const portionAmount = this.portionProvider.getPortionAmount( amount, tradeType, + tokenOutHasFot, swapConfig ); if (portionAmount && portionAmount.greaterThan(ZERO)) { @@ -1056,16 +1076,6 @@ export class AlphaRouter } } - const { currencyIn, currencyOut } = - this.determineCurrencyInOutFromTradeType( - tradeType, - amount, - quoteCurrency - ); - - const tokenIn = currencyIn.wrapped; - const tokenOut = currencyOut.wrapped; - metric.setProperty('chainId', this.chainId); metric.setProperty('pair', `${tokenIn.symbol}/${tokenOut.symbol}`); metric.setProperty('tokenIn', tokenIn.address); @@ -1455,6 +1465,7 @@ export class AlphaRouter const portionAmount = this.portionProvider.getPortionAmount( tokenOutAmount, tradeType, + tokenOutHasFot, swapConfig ); const portionQuoteAmount = this.portionProvider.getPortionQuoteAmount( diff --git a/test/integ/routers/alpha-router/alpha-router.integration.test.ts b/test/integ/routers/alpha-router/alpha-router.integration.test.ts index 857e103d5..19cad6619 100644 --- a/test/integ/routers/alpha-router/alpha-router.integration.test.ts +++ b/test/integ/routers/alpha-router/alpha-router.integration.test.ts @@ -2815,6 +2815,10 @@ describe('alpha router integration', () => { slippageTolerance: LARGE_SLIPPAGE, deadlineOrPreviousBlockhash: parseDeadline(360), simulate: { fromAddress: WHALES(tokenIn!) }, + fee: { + fee: new Percent(FLAT_PORTION.bips, 10_000), + recipient: FLAT_PORTION.recipient + } }, { ...ROUTING_CONFIG, @@ -2830,6 +2834,12 @@ describe('alpha router integration', () => { expect(swap!.methodParameters).toBeDefined(); expect(swap!.methodParameters!.to).toBeDefined(); + if (enableFeeOnTransferFeeFetching && tokenOut?.address === BULLET_WITHOUT_TAX.address) { + expect(swap?.portionAmount?.quotient).toBeUndefined(); + } else { + expect(swap?.portionAmount?.quotient?.toString()).not.toEqual("0"); + } + return { enableFeeOnTransferFeeFetching, ...swap! } }) ) diff --git a/test/unit/providers/portion-provider.test.ts b/test/unit/providers/portion-provider.test.ts index 7b80f0ffe..b7e42c6fb 100644 --- a/test/unit/providers/portion-provider.test.ts +++ b/test/unit/providers/portion-provider.test.ts @@ -1,13 +1,13 @@ import { BigNumber } from '@ethersproject/bignumber'; +import { Currency, CurrencyAmount, Fraction, Percent, Token, TradeType, } from '@uniswap/sdk-core'; import { - Currency, - CurrencyAmount, - Fraction, - Percent, - Token, - TradeType, -} from '@uniswap/sdk-core'; -import { parseAmount, RouteWithValidQuote, SwapOptions, SwapType, V2RouteWithValidQuote, V3RouteWithValidQuote } from '../../../src'; + parseAmount, + RouteWithValidQuote, + SwapOptions, + SwapType, + V2RouteWithValidQuote, + V3RouteWithValidQuote +} from '../../../src'; import { PortionProvider } from '../../../src/providers/portion-provider'; import { FLAT_PORTION, GREENLIST_TOKEN_PAIRS } from '../../test-util/mock-data'; import { @@ -20,7 +20,7 @@ describe('portion provider', () => { const expectedRequestAmount = '1.01'; const expectedQuote = '1605.56'; const expectedGas = '2.35'; - const expectedPortion = FLAT_PORTION + const expectedPortion = FLAT_PORTION; const portionProvider = new PortionProvider(); describe('getPortion test', () => { @@ -34,13 +34,19 @@ describe('portion provider', () => { const tokenAddress1 = token1.wrapped.address; const tokenAddress2 = token2.wrapped.address; - it(`token address ${tokenAddress1} to token address ${tokenAddress2} within the list, should have portion`, async () => { - await exactInGetPortionAndAssert(token2); - }); + it( + `token address ${tokenAddress1} to token address ${tokenAddress2} within the list, should have portion`, + async () => { + await exactInGetPortionAndAssert(token2); + } + ); - it(`token symbol ${tokenSymbol1} to token symbol ${tokenSymbol2} within the list, should have portion`, async () => { - await exactInGetPortionAndAssert(token2); - }); + it( + `token symbol ${tokenSymbol1} to token symbol ${tokenSymbol2} within the list, should have portion`, + async () => { + await exactInGetPortionAndAssert(token2); + } + ); }); async function exactInGetPortionAndAssert( @@ -57,13 +63,18 @@ describe('portion provider', () => { fee: new Percent(expectedPortion.bips, 10_000), recipient: expectedPortion.recipient, } - } + }; const portionAmount = portionProvider.getPortionAmount( quoteAmount, TradeType.EXACT_INPUT, + undefined, swapConfig ); - const portionAdjustedQuote = portionProvider.getQuoteGasAndPortionAdjusted(TradeType.EXACT_INPUT, quoteGasAdjustedAmount, portionAmount); + const portionAdjustedQuote = portionProvider.getQuoteGasAndPortionAdjusted( + TradeType.EXACT_INPUT, + quoteGasAdjustedAmount, + portionAmount + ); // 1605.56 * 10^8 * 5 / 10000 = 80278000 const expectedPortionAmount = quoteAmount.multiply(new Fraction(expectedPortion.bips, 10_000)); @@ -89,15 +100,21 @@ describe('portion provider', () => { const tokenAddress1 = token1.wrapped.address; const tokenAddress2 = token2.wrapped.address; - it(`token address ${tokenAddress1} to token address ${tokenAddress2} within the list, should have portion`, async () => { - const amount = parseAmount(expectedRequestAmount, token2); - await exactOutGetPortionAndAssert(amount, token1); - }); + it( + `token address ${tokenAddress1} to token address ${tokenAddress2} within the list, should have portion`, + async () => { + const amount = parseAmount(expectedRequestAmount, token2); + await exactOutGetPortionAndAssert(amount, token1); + } + ); - it(`token symbol ${tokenSymbol1} to token symbol ${tokenSymbol2} within the list, should have portion`, async () => { - const amount = parseAmount(expectedRequestAmount, token2); - await exactOutGetPortionAndAssert(amount, token1); - }); + it( + `token symbol ${tokenSymbol1} to token symbol ${tokenSymbol2} within the list, should have portion`, + async () => { + const amount = parseAmount(expectedRequestAmount, token2); + await exactOutGetPortionAndAssert(amount, token1); + } + ); }); async function exactOutGetPortionAndAssert( @@ -116,8 +133,8 @@ describe('portion provider', () => { amount: expectedPortionAmount.quotient.toString(), recipient: expectedPortion.recipient, } - } - const portionAmount = portionProvider.getPortionAmount(amount, TradeType.EXACT_OUTPUT, swapConfig); + }; + const portionAmount = portionProvider.getPortionAmount(amount, TradeType.EXACT_OUTPUT, false, swapConfig); expect(portionAmount).toBeDefined(); // 1.01 * 10^8 * 12 / 10000 = 121200 @@ -132,24 +149,73 @@ describe('portion provider', () => { ); expect(actualPortionQuoteAmount).toBeDefined(); - const expectedPortionQuoteAmount = portionAmount!.divide(portionAmount!.add(amount)).multiply(quoteAmount) + const expectedPortionQuoteAmount = portionAmount!.divide(portionAmount!.add(amount)).multiply(quoteAmount); expect(actualPortionQuoteAmount!.quotient.toString()).toBe(expectedPortionQuoteAmount.quotient.toString()); - const actualCorrectedQuoteAmount = portionProvider.getQuote(TradeType.EXACT_OUTPUT, quoteAmount, actualPortionQuoteAmount); + const actualCorrectedQuoteAmount = portionProvider.getQuote( + TradeType.EXACT_OUTPUT, + quoteAmount, + actualPortionQuoteAmount + ); const expectedCorrectedQuoteAmount = quoteAmount.subtract(actualPortionQuoteAmount!); expect(actualCorrectedQuoteAmount?.quotient.toString()).toBe(expectedCorrectedQuoteAmount.quotient.toString()); - const actualCorrectedQuoteGasAdjustedAmount = portionProvider.getQuoteGasAdjusted(TradeType.EXACT_OUTPUT, quoteGasAdjustedAmount, actualPortionQuoteAmount); + const actualCorrectedQuoteGasAdjustedAmount = portionProvider.getQuoteGasAdjusted( + TradeType.EXACT_OUTPUT, + quoteGasAdjustedAmount, + actualPortionQuoteAmount + ); const expectedCorrectedQuoteGasAdjustedAmount = quoteGasAdjustedAmount.subtract(actualPortionQuoteAmount!); - expect(actualCorrectedQuoteGasAdjustedAmount?.quotient.toString()).toBe(expectedCorrectedQuoteGasAdjustedAmount.quotient.toString()) + expect(actualCorrectedQuoteGasAdjustedAmount?.quotient.toString()) + .toBe(expectedCorrectedQuoteGasAdjustedAmount.quotient.toString()); - const actualCorrectedQuoteGasAndPortionAdjustedAmount = portionProvider.getQuoteGasAndPortionAdjusted(TradeType.EXACT_OUTPUT, actualCorrectedQuoteGasAdjustedAmount, portionAmount); + const actualCorrectedQuoteGasAndPortionAdjustedAmount = portionProvider.getQuoteGasAndPortionAdjusted( + TradeType.EXACT_OUTPUT, + actualCorrectedQuoteGasAdjustedAmount, + portionAmount + ); // 1605.56 * 10^18 + 121200 / (1.01 * 10^8 + 121200) * 1605.56 * 10^18 = 1.6074867e+21 // (exact in quote gas adjusted amount) * (ETH decimal scale) + (portion amount) / (exact out requested amount + portion amount) * (exact in quote amount) * (ETH decimal scale) // = (quote gas and portion adjusted amount) - expect(actualCorrectedQuoteGasAndPortionAdjustedAmount?.quotient.toString()).toBe(actualCorrectedQuoteGasAdjustedAmount.quotient.toString()); + expect(actualCorrectedQuoteGasAndPortionAdjustedAmount?.quotient.toString()) + .toBe(actualCorrectedQuoteGasAdjustedAmount.quotient.toString()); } }); + + describe('TokenOutHasFot flag is true', () => { + + GREENLIST_TOKEN_PAIRS.forEach((pair) => { + const token1: Currency | Token = pair[0].isNative ? (pair[0] as Currency) : pair[0].wrapped; + const token2: Currency | Token = pair[1].isNative ? (pair[1] as Currency) : pair[1].wrapped; + const tokenAddress1 = token1.wrapped.address; + const tokenAddress2 = token2.wrapped.address; + + it( + `token address ${tokenAddress1} to token address ${tokenAddress2} within the list, but tokenOut has FOT, should not have portion`, + async () => { + const quoteAmount = parseAmount(expectedQuote, token2); + + const swapConfig: SwapOptions = { + type: SwapType.UNIVERSAL_ROUTER, + slippageTolerance: new Percent(5), + recipient: '0x123', + fee: { + fee: new Percent(expectedPortion.bips, 10_000), + recipient: expectedPortion.recipient, + } + }; + const portionAmount = portionProvider.getPortionAmount( + quoteAmount, + TradeType.EXACT_INPUT, + true, + swapConfig + ); + + expect(portionAmount).toBeUndefined(); + } + ); + }); + }); }); describe('getRouteWithQuotePortionAdjusted test', () => { @@ -170,7 +236,7 @@ describe('portion provider', () => { v2RouteWithQuote, v3RouteWithQuote, mixedRouteWithQuote - ] + ]; const swapParams: SwapOptions = { type: SwapType.UNIVERSAL_ROUTER, deadlineOrPreviousBlockhash: undefined, @@ -180,24 +246,40 @@ describe('portion provider', () => { fee: new Percent(FLAT_PORTION.bips, 10_000), recipient: FLAT_PORTION.recipient } - } + }; const oneHundredPercent = new Percent(1); - const routesWithQuotePortionAdjusted = portionProvider.getRouteWithQuotePortionAdjusted(TradeType.EXACT_INPUT, routesWithValidQuotes, swapParams); + const routesWithQuotePortionAdjusted = portionProvider.getRouteWithQuotePortionAdjusted( + TradeType.EXACT_INPUT, + routesWithValidQuotes, + swapParams + ); routesWithQuotePortionAdjusted.forEach((routeWithQuotePortionAdjusted) => { if (routeWithQuotePortionAdjusted instanceof V2RouteWithValidQuote) { - expect(routeWithQuotePortionAdjusted.quote.quotient.toString()).toEqual(oneHundredPercent.subtract(new Percent(FLAT_PORTION.bips, 10_000)).multiply(20).quotient.toString()) + expect(routeWithQuotePortionAdjusted.quote.quotient.toString()) + .toEqual(oneHundredPercent.subtract(new Percent(FLAT_PORTION.bips, 10_000)) + .multiply(20) + .quotient + .toString()); } if (routeWithQuotePortionAdjusted instanceof V3RouteWithValidQuote) { - expect(routeWithQuotePortionAdjusted.quote.toExact()).toEqual(oneHundredPercent.subtract(new Percent(FLAT_PORTION.bips, 10_000)).multiply(50).quotient.toString()) + expect(routeWithQuotePortionAdjusted.quote.toExact()) + .toEqual(oneHundredPercent.subtract(new Percent(FLAT_PORTION.bips, 10_000)) + .multiply(50) + .quotient + .toString()); } if (routeWithQuotePortionAdjusted instanceof V3RouteWithValidQuote) { - expect(routeWithQuotePortionAdjusted.quote.toExact()).toEqual(oneHundredPercent.subtract(new Percent(FLAT_PORTION.bips, 10_000)).multiply(60).quotient.toString()) + expect(routeWithQuotePortionAdjusted.quote.toExact()) + .toEqual(oneHundredPercent.subtract(new Percent(FLAT_PORTION.bips, 10_000)) + .multiply(60) + .quotient + .toString()); } - }) + }); }); it('exact out test', () => { @@ -217,7 +299,7 @@ describe('portion provider', () => { v2RouteWithQuote, v3RouteWithQuote, mixedRouteWithQuote - ] + ]; const swapParams: SwapOptions = { type: SwapType.UNIVERSAL_ROUTER, deadlineOrPreviousBlockhash: undefined, @@ -227,23 +309,27 @@ describe('portion provider', () => { fee: new Percent(FLAT_PORTION.bips, 10_000), recipient: FLAT_PORTION.recipient } - } + }; - const routesWithQuotePortionAdjusted = portionProvider.getRouteWithQuotePortionAdjusted(TradeType.EXACT_OUTPUT, routesWithValidQuotes, swapParams); + const routesWithQuotePortionAdjusted = portionProvider.getRouteWithQuotePortionAdjusted( + TradeType.EXACT_OUTPUT, + routesWithValidQuotes, + swapParams + ); routesWithQuotePortionAdjusted.forEach((routeWithQuotePortionAdjusted) => { if (routeWithQuotePortionAdjusted instanceof V2RouteWithValidQuote) { - expect(routeWithQuotePortionAdjusted.quote.quotient.toString()).toEqual('20') + expect(routeWithQuotePortionAdjusted.quote.quotient.toString()).toEqual('20'); } if (routeWithQuotePortionAdjusted instanceof V3RouteWithValidQuote) { - expect(routeWithQuotePortionAdjusted.quote.quotient.toString()).toEqual('50') + expect(routeWithQuotePortionAdjusted.quote.quotient.toString()).toEqual('50'); } if (routeWithQuotePortionAdjusted instanceof V3RouteWithValidQuote) { - expect(routeWithQuotePortionAdjusted.quote.quotient.toString()).toEqual('30') + expect(routeWithQuotePortionAdjusted.quote.quotient.toString()).toEqual('30'); } - }) + }); }); }); });