From 725907dbd1b3497809ca39259b3f614018afbe16 Mon Sep 17 00:00:00 2001 From: jsy1218 <91580504+jsy1218@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:55:43 -0700 Subject: [PATCH] Feature: V2 routes quote with fee-on-transfer fees off the quote amount (#395) * sor quote with fot fees * prettier * remove comments in quote provider and alpha router pass in enable FOT flag * remove comments in quote provider and alpha router pass in enable FOT flag * address feedback * flatten instead of flatMap * fix integ-test after making tokenPropertiesProvider a required property in alpha router * fix token properties provider unit test * update sdk-core to use Token with fot tax fields * fix CLI command to include FOT fee fetching * fix cli for passing in enableFOT flag * pass enable FOT flag to v2 subgraph candidate pools loading * make tokenValidatorProvider before tokenPropertiesProvider due to dependencies * fix v2 quote provider to get the fot tax from the pool objects from subgraph * populate enable fot tax everywhere to v2 pool provider * fix quote provider copy paste error * get rid of explicit enableFeeOnTransferFeeFetching passing * Revert "fix quote provider copy paste error" This reverts commit 5587a4e2d4c8e13d6d2d71f1a294a5e8e4addced. * actual quote provider copy paste error fix * replace tokenIn and tokenOut with fox tax one after getting the candidate pools * amount distribution also adding fot tax * remove quote provider changes * v2 get routes from chain and cache use pools to re instantiate tokens with fox tax * get amount distribution add back black line * cached routes get pools for matched tokenIn and tokenOut only once * compute all v2 routes no longer need to get token with fot tax again * fix quote cli exact out to pass in debugRouting and enableFeeOnTransferFeeFetching as well * fix the input currency amount to not mutate during the swap methods * use currency for getSwapRouteFromCache v2 amountWithFotTax * amount to flash borror 10x for rebase tokens e.g. stETH * bump v2-sdk version * extract matched pools to for tokens to util functions * @mikeki offline feedback on no need to filter pools on getSwapRouteFromCache, because writing into Cached Routes can have FOT Token instances * 3.16.22 * fix cached routes side of bugs that caused fot quote to not work properly * remove custom pool reserve matching token logics --- cli/base-command.ts | 20 ++- cli/commands/quote.ts | 8 + package-lock.json | 44 ++--- package.json | 6 +- src/providers/provider.ts | 4 + src/providers/token-fee-fetcher.ts | 5 +- src/providers/token-properties-provider.ts | 9 +- src/providers/v2/pool-provider.ts | 84 ++++++++-- src/providers/v2/quote-provider.ts | 24 ++- src/routers/alpha-router/alpha-router.ts | 156 +++++++++++------- .../functions/get-candidate-pools.ts | 20 +-- .../alpha-router/gas-models/gas-model.ts | 1 + src/routers/alpha-router/quoters/v2-quoter.ts | 1 + .../alpha-router.integration.test.ts | 38 ++++- .../token-properties-provider.test.ts | 12 +- .../routers/alpha-router/alpha-router.test.ts | 7 + 16 files changed, 315 insertions(+), 124 deletions(-) diff --git a/cli/base-command.ts b/cli/base-command.ts index 978549108..13af14441 100644 --- a/cli/base-command.ts +++ b/cli/base-command.ts @@ -39,7 +39,9 @@ import { setGlobalMetric, SimulationStatus, TenderlySimulator, + TokenPropertiesProvider, TokenProvider, + TokenValidatorProvider, UniswapMulticallProvider, V2PoolProvider, V3PoolProvider, @@ -47,6 +49,7 @@ import { } from '../src'; import { LegacyGasPriceProvider } from '../src/providers/legacy-gas-price-provider'; import { OnChainGasPriceProvider } from '../src/providers/on-chain-gas-price-provider'; +import { OnChainTokenFeeFetcher } from '../src/providers/token-fee-fetcher'; export abstract class BaseCommand extends Command { static flags = { @@ -284,7 +287,22 @@ export abstract class BaseCommand extends Command { new V3PoolProvider(chainId, multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) ); - const v2PoolProvider = new V2PoolProvider(chainId, multicall2Provider); + const tokenValidatorProvider = new TokenValidatorProvider( + chainId, + multicall2Provider, + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) + ) + const tokenFeeFetcher = new OnChainTokenFeeFetcher( + chainId, + provider + ) + const tokenPropertiesProvider = new TokenPropertiesProvider( + chainId, + tokenValidatorProvider, + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })), + tokenFeeFetcher + ) + const v2PoolProvider = new V2PoolProvider(chainId, multicall2Provider, tokenPropertiesProvider); const tenderlySimulator = new TenderlySimulator( chainId, diff --git a/cli/commands/quote.ts b/cli/commands/quote.ts index 7ce77266e..20a57d73a 100644 --- a/cli/commands/quote.ts +++ b/cli/commands/quote.ts @@ -34,6 +34,8 @@ export class Quote extends BaseCommand { default: false, }), simulate: flags.boolean({ required: false, default: false }), + debugRouting: flags.boolean({ required: false, default: true }), + enableFeeOnTransferFeeFetching: flags.boolean({ required: false, default: true }), }; async run() { @@ -63,6 +65,8 @@ export class Quote extends BaseCommand { forceCrossProtocol, forceMixedRoutes, simulate, + debugRouting, + enableFeeOnTransferFeeFetching } = flags; const topNSecondHopForTokenAddress = new MapWithLowerCaseKey(); @@ -151,6 +155,8 @@ export class Quote extends BaseCommand { protocols, forceCrossProtocol, forceMixedRoutes, + debugRouting, + enableFeeOnTransferFeeFetching, } ); } else { @@ -186,6 +192,8 @@ export class Quote extends BaseCommand { protocols, forceCrossProtocol, forceMixedRoutes, + debugRouting, + enableFeeOnTransferFeeFetching, } ); } diff --git a/package-lock.json b/package-lock.json index dd29a2b9d..e469e2c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,23 @@ { "name": "@uniswap/smart-order-router", - "version": "3.16.21", + "version": "3.16.22", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@uniswap/smart-order-router", - "version": "3.16.21", + "version": "3.16.22", "license": "GPL", "dependencies": { "@uniswap/default-token-list": "^11.2.0", "@uniswap/permit2-sdk": "^1.2.0", "@uniswap/router-sdk": "^1.6.0", - "@uniswap/sdk-core": "^4.0.6", + "@uniswap/sdk-core": "^4.0.7", "@uniswap/swap-router-contracts": "^1.3.0", "@uniswap/token-lists": "^1.0.0-beta.31", "@uniswap/universal-router": "^1.0.1", "@uniswap/universal-router-sdk": "^1.5.7", - "@uniswap/v2-sdk": "^3.2.0", + "@uniswap/v2-sdk": "^3.2.1", "@uniswap/v3-sdk": "^3.10.0", "async-retry": "^1.3.1", "await-timeout": "^1.1.1", @@ -3119,7 +3119,7 @@ "@ethersproject/abi": "^5.5.0", "@uniswap/sdk-core": "^4", "@uniswap/swap-router-contracts": "1.1.0", - "@uniswap/v2-sdk": "^3.2.0", + "@uniswap/v2-sdk": "^3.2.1", "@uniswap/v3-sdk": "^3.10.0" } }, @@ -3171,9 +3171,9 @@ } }, "node_modules/@uniswap/sdk-core": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@uniswap/sdk-core/-/sdk-core-4.0.6.tgz", - "integrity": "sha512-6GzCVfnOiJtvo91zlF/VjnC2OEbBRThVclzrh7+Zmo8dBovXwSlXwqn3RkSWACn/XEOzAKH70TficfOWm6mWJA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@uniswap/sdk-core/-/sdk-core-4.0.7.tgz", + "integrity": "sha512-jscx7KUIWzQatcL5PHY6xy0gEL9IGQcL5h/obxzX9foP2KoNk9cq66Ia8I2Kvpa7zBcPOeW1hU0hJNBq6CzcIQ==", "dependencies": { "@ethersproject/address": "^5.0.2", "big.js": "^5.2.2", @@ -3253,7 +3253,7 @@ "@uniswap/router-sdk": "^1.6.0", "@uniswap/sdk-core": "^4.0.0", "@uniswap/universal-router": "1.4.3", - "@uniswap/v2-sdk": "^3.2.0", + "@uniswap/v2-sdk": "^3.2.1", "@uniswap/v3-sdk": "^3.10.0", "bignumber.js": "^9.0.2", "ethers": "^5.3.1" @@ -3306,13 +3306,13 @@ } }, "node_modules/@uniswap/v2-sdk": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@uniswap/v2-sdk/-/v2-sdk-3.2.0.tgz", - "integrity": "sha512-kBOJ6Iwtgb/2LckLMIzfbPM37/ll0F+33lzPmZlqoJwsT0F2hZdVfAhclufZcSb0Y9RdLXl6372CZJ+lhx8cUQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-sdk/-/v2-sdk-3.2.1.tgz", + "integrity": "sha512-YocFCG97t7qi0fZrBWjFz3dfOgW+rhVN70PlciwcioG+x3KmQlJH7pVfCA34U/BDVFfebHp/iS/De6ajvMmJBg==", "dependencies": { "@ethersproject/address": "^5.0.0", "@ethersproject/solidity": "^5.0.0", - "@uniswap/sdk-core": "^4.0.2", + "@uniswap/sdk-core": "^4.0.7", "tiny-invariant": "^1.1.0", "tiny-warning": "^1.0.3" }, @@ -14114,7 +14114,7 @@ "@ethersproject/abi": "^5.5.0", "@uniswap/sdk-core": "^4", "@uniswap/swap-router-contracts": "1.1.0", - "@uniswap/v2-sdk": "^3.2.0", + "@uniswap/v2-sdk": "^3.2.1", "@uniswap/v3-sdk": "^3.10.0" }, "dependencies": { @@ -14156,9 +14156,9 @@ } }, "@uniswap/sdk-core": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@uniswap/sdk-core/-/sdk-core-4.0.6.tgz", - "integrity": "sha512-6GzCVfnOiJtvo91zlF/VjnC2OEbBRThVclzrh7+Zmo8dBovXwSlXwqn3RkSWACn/XEOzAKH70TficfOWm6mWJA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@uniswap/sdk-core/-/sdk-core-4.0.7.tgz", + "integrity": "sha512-jscx7KUIWzQatcL5PHY6xy0gEL9IGQcL5h/obxzX9foP2KoNk9cq66Ia8I2Kvpa7zBcPOeW1hU0hJNBq6CzcIQ==", "requires": { "@ethersproject/address": "^5.0.2", "big.js": "^5.2.2", @@ -14234,7 +14234,7 @@ "@uniswap/router-sdk": "^1.6.0", "@uniswap/sdk-core": "^4.0.0", "@uniswap/universal-router": "1.4.3", - "@uniswap/v2-sdk": "^3.2.0", + "@uniswap/v2-sdk": "^3.2.1", "@uniswap/v3-sdk": "^3.10.0", "bignumber.js": "^9.0.2", "ethers": "^5.3.1" @@ -14264,13 +14264,13 @@ } }, "@uniswap/v2-sdk": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@uniswap/v2-sdk/-/v2-sdk-3.2.0.tgz", - "integrity": "sha512-kBOJ6Iwtgb/2LckLMIzfbPM37/ll0F+33lzPmZlqoJwsT0F2hZdVfAhclufZcSb0Y9RdLXl6372CZJ+lhx8cUQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@uniswap/v2-sdk/-/v2-sdk-3.2.1.tgz", + "integrity": "sha512-YocFCG97t7qi0fZrBWjFz3dfOgW+rhVN70PlciwcioG+x3KmQlJH7pVfCA34U/BDVFfebHp/iS/De6ajvMmJBg==", "requires": { "@ethersproject/address": "^5.0.0", "@ethersproject/solidity": "^5.0.0", - "@uniswap/sdk-core": "^4.0.2", + "@uniswap/sdk-core": "^4.0.7", "tiny-invariant": "^1.1.0", "tiny-warning": "^1.0.3" } diff --git a/package.json b/package.json index a68051b9c..b99d43a47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uniswap/smart-order-router", - "version": "3.16.21", + "version": "3.16.22", "description": "Uniswap Smart Order Router", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -38,9 +38,9 @@ "@uniswap/token-lists": "^1.0.0-beta.31", "@uniswap/universal-router": "^1.0.1", "@uniswap/universal-router-sdk": "^1.5.7", - "@uniswap/v2-sdk": "^3.2.0", + "@uniswap/v2-sdk": "^3.2.1", "@uniswap/v3-sdk": "^3.10.0", - "@uniswap/sdk-core": "^4.0.6", + "@uniswap/sdk-core": "^4.0.7", "async-retry": "^1.3.1", "await-timeout": "^1.1.1", "axios": "^0.21.1", diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 9bba79133..ea65ab5df 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -13,6 +13,10 @@ export type ProviderConfig = { * Debug flag to test some codepaths */ debugRouting?: boolean; + /** + * Flag for token properties provider to enable fetching fee-on-transfer tokens. + */ + enableFeeOnTransferFeeFetching?: boolean; }; export type LocalCacheEntry = { diff --git a/src/providers/token-fee-fetcher.ts b/src/providers/token-fee-fetcher.ts index 96b9897bc..029b279d3 100644 --- a/src/providers/token-fee-fetcher.ts +++ b/src/providers/token-fee-fetcher.ts @@ -36,8 +36,9 @@ const FEE_DETECTOR_ADDRESS = (chainId: ChainId) => { // Amount has to be big enough to avoid rounding errors, but small enough that // most v2 pools will have at least this many token units -// 10000 is the smallest number that avoids rounding errors in bps terms -const AMOUNT_TO_FLASH_BORROW = '10000'; +// 100000 is the smallest number that avoids rounding errors in bps terms +// 10000 was not sufficient due to rounding errors for rebase token (e.g. stETH) +const AMOUNT_TO_FLASH_BORROW = '100000'; // 1M gas limit per validate call, should cover most swap cases const GAS_LIMIT_PER_VALIDATE = 1_000_000; diff --git a/src/providers/token-properties-provider.ts b/src/providers/token-properties-provider.ts index 78e41bdc5..1b8027355 100644 --- a/src/providers/token-properties-provider.ts +++ b/src/providers/token-properties-provider.ts @@ -42,13 +42,19 @@ export class TokenPropertiesProvider implements ITokenPropertiesProvider { private tokenValidatorProvider: ITokenValidatorProvider, private tokenPropertiesCache: ICache, private tokenFeeFetcher: ITokenFeeFetcher, - private allowList = DEFAULT_ALLOWLIST + private allowList = DEFAULT_ALLOWLIST, ) {} public async getTokensProperties( tokens: Token[], providerConfig?: ProviderConfig ): Promise { + const tokenToResult: TokenPropertiesMap = {}; + + if (!providerConfig?.enableFeeOnTransferFeeFetching || this.chainId !== ChainId.MAINNET) { + return tokenToResult; + } + const nonAllowlistTokens = tokens.filter( (token) => !this.allowList.has(token.address.toLowerCase()) ); @@ -57,7 +63,6 @@ export class TokenPropertiesProvider implements ITokenPropertiesProvider { nonAllowlistTokens, providerConfig ); - const tokenToResult: TokenPropertiesMap = {}; tokens.forEach((token) => { if (this.allowList.has(token.address.toLowerCase())) { diff --git a/src/providers/v2/pool-provider.ts b/src/providers/v2/pool-provider.ts index dfc70b9f3..fcc0c0eda 100644 --- a/src/providers/v2/pool-provider.ts +++ b/src/providers/v2/pool-provider.ts @@ -5,11 +5,20 @@ import retry, { Options as RetryOptions } from 'async-retry'; import _ from 'lodash'; import { IUniswapV2Pair__factory } from '../../types/v2/factories/IUniswapV2Pair__factory'; -import { CurrencyAmount, ID_TO_NETWORK_NAME, metric, MetricLoggerUnit } from '../../util'; +import { + CurrencyAmount, + ID_TO_NETWORK_NAME, + metric, + MetricLoggerUnit, +} from '../../util'; import { log } from '../../util/log'; import { poolToString } from '../../util/routes'; import { IMulticallProvider, Result } from '../multicall-provider'; import { ProviderConfig } from '../provider'; +import { + ITokenPropertiesProvider, +} from '../token-properties-provider'; +import { TokenValidationResult } from '../token-validator-provider'; type IReserves = { reserve0: BigNumber; @@ -66,18 +75,19 @@ export class V2PoolProvider implements IV2PoolProvider { * Creates an instance of V2PoolProvider. * @param chainId The chain id to use. * @param multicall2Provider The multicall provider to use to get the pools. + * @param tokenPropertiesProvider The token properties provider to use to get token properties. * @param retryOptions The retry options for each call to the multicall. */ constructor( protected chainId: ChainId, protected multicall2Provider: IMulticallProvider, + protected tokenPropertiesProvider: ITokenPropertiesProvider, protected retryOptions: V2PoolRetryOptions = { retries: 2, minTimeout: 50, maxTimeout: 500, } - ) { - } + ) {} public async getPools( tokenPairs: [Token, Token][], @@ -109,18 +119,28 @@ export class V2PoolProvider implements IV2PoolProvider { ); metric.putMetric('V2_RPC_POOL_RPC_CALL', 1, MetricLoggerUnit.None); - metric.putMetric('V2GetReservesBatchSize', sortedPoolAddresses.length, MetricLoggerUnit.Count); + metric.putMetric( + 'V2GetReservesBatchSize', + sortedPoolAddresses.length, + MetricLoggerUnit.Count + ); metric.putMetric( `V2GetReservesBatchSize_${ID_TO_NETWORK_NAME(this.chainId)}`, sortedPoolAddresses.length, MetricLoggerUnit.Count ); - const reservesResults = await this.getPoolsData( - sortedPoolAddresses, - 'getReserves', - providerConfig - ); + const [reservesResults, tokenPropertiesMap] = await Promise.all([ + this.getPoolsData( + sortedPoolAddresses, + 'getReserves', + providerConfig + ), + this.tokenPropertiesProvider.getTokensProperties( + this.flatten(tokenPairs), + providerConfig + ), + ]); log.info( `Got reserves for ${poolAddressSet.size} pools ${ @@ -144,7 +164,39 @@ export class V2PoolProvider implements IV2PoolProvider { continue; } - const [token0, token1] = sortedTokenPairs[i]!; + let [token0, token1] = sortedTokenPairs[i]!; + if ( + tokenPropertiesMap[token0.address.toLowerCase()] + ?.tokenValidationResult === TokenValidationResult.FOT + ) { + token0 = new Token( + token0.chainId, + token0.address, + token0.decimals, + token0.symbol, + token0.name, + true, // at this point we know it's valid token address + tokenPropertiesMap[token0.address.toLowerCase()]?.tokenFeeResult?.buyFeeBps, + tokenPropertiesMap[token0.address.toLowerCase()]?.tokenFeeResult?.sellFeeBps + ); + } + + if ( + tokenPropertiesMap[token1.address.toLowerCase()] + ?.tokenValidationResult === TokenValidationResult.FOT + ) { + token1 = new Token( + token1.chainId, + token1.address, + token1.decimals, + token1.symbol, + token1.name, + true, // at this point we know it's valid token address + tokenPropertiesMap[token1.address.toLowerCase()]?.tokenFeeResult?.buyFeeBps, + tokenPropertiesMap[token1.address.toLowerCase()]?.tokenFeeResult?.sellFeeBps + ); + } + const { reserve0, reserve1 } = reservesResult.result; const pool = new Pair( @@ -228,4 +280,16 @@ export class V2PoolProvider implements IV2PoolProvider { return results; } + + // We are using ES2017. ES2019 has native flatMap support + private flatten(tokenPairs: Array<[Token, Token]>): Token[] { + const tokens = new Array(); + + for (const [tokenA, tokenB] of tokenPairs) { + tokens.push(tokenA); + tokens.push(tokenB); + } + + return tokens; + } } diff --git a/src/providers/v2/quote-provider.ts b/src/providers/v2/quote-provider.ts index bf2f2bdc4..b22ba9d51 100644 --- a/src/providers/v2/quote-provider.ts +++ b/src/providers/v2/quote-provider.ts @@ -75,8 +75,18 @@ export class V2QuoteProvider implements IV2QuoteProvider { let outputAmount = amount.wrapped; for (const pair of route.pairs) { - const [outputAmountNew] = pair.getOutputAmount(outputAmount); - outputAmount = outputAmountNew; + 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; + } } amountQuotes.push({ @@ -88,7 +98,15 @@ export class V2QuoteProvider implements IV2QuoteProvider { for (let i = route.pairs.length - 1; i >= 0; i--) { const pair = route.pairs[i]!; - [inputAmount] = pair.getInputAmount(inputAmount); + 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); + } } amountQuotes.push({ diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 0bbcf1702..069253574 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -34,7 +34,8 @@ import { Simulator, StaticV2SubgraphProvider, StaticV3SubgraphProvider, - SwapRouterProvider, TokenPropertiesProvider, + SwapRouterProvider, + TokenPropertiesProvider, TokenValidationResult, UniswapMulticallProvider, URISubgraphProvider, V2QuoteProvider, @@ -46,14 +47,14 @@ import { GasPrice, IGasPriceProvider } from '../../providers/gas-price-provider' import { ProviderConfig } from '../../providers/provider'; import { OnChainTokenFeeFetcher } from '../../providers/token-fee-fetcher'; import { ITokenProvider, TokenProvider } from '../../providers/token-provider'; -import { ITokenValidatorProvider, TokenValidatorProvider, } from '../../providers/token-validator-provider'; +import { ITokenValidatorProvider, TokenValidatorProvider } from '../../providers/token-validator-provider'; import { IV2PoolProvider, V2PoolProvider } from '../../providers/v2/pool-provider'; import { ArbitrumGasData, ArbitrumGasDataProvider, IL2GasDataProvider, OptimismGasData, - OptimismGasDataProvider, + OptimismGasDataProvider } from '../../providers/v3/gas-data-provider'; import { IV3PoolProvider, V3PoolProvider } from '../../providers/v3/pool-provider'; import { IV3SubgraphProvider } from '../../providers/v3/subgraph-provider'; @@ -79,14 +80,14 @@ import { SwapToRatioResponse, SwapToRatioStatus, V2Route, - V3Route, + V3Route } from '../router'; import { DEFAULT_ROUTING_CONFIG_BY_CHAIN, ETH_GAS_STATION_API_URL } from './config'; import { MixedRouteWithValidQuote, RouteWithValidQuote, - V3RouteWithValidQuote, + V3RouteWithValidQuote } from './entities/route-with-valid-quote'; import { BestSwapRoute, getBestSwapRoute } from './functions/best-swap-route'; import { calculateRatioAmountIn } from './functions/calculate-ratio-amount-in'; @@ -96,7 +97,7 @@ import { getV3CandidatePools, PoolId, V2CandidatePools, - V3CandidatePools, + V3CandidatePools } from './functions/get-candidate-pools'; import { IGasModel, @@ -110,6 +111,7 @@ import { NATIVE_OVERHEAD } from './gas-models/v3/gas-costs'; import { V3HeuristicGasModelFactory } from './gas-models/v3/v3-heuristic-gas-model'; import { GetQuotesResult, MixedQuoter, V2Quoter, V3Quoter } from './quoters'; + export type AlphaRouterParams = { /** * The chain id for this instance of the Alpha Router. @@ -351,7 +353,11 @@ export type AlphaRouterConfig = { /** * Flag that allow us to override the cache mode. */ - overwriteCacheMode?: CacheMode + overwriteCacheMode?: CacheMode; + /** + * Flag for token properties provider to enable fetching fee-on-transfer tokens. + */ + enableFeeOnTransferFeeFetching?: boolean; }; export class AlphaRouter @@ -382,7 +388,7 @@ export class AlphaRouter protected v3Quoter: V3Quoter; protected mixedQuoter: MixedQuoter; protected routeCachingProvider?: IRouteCachingProvider; - protected tokenPropertiesProvider?: ITokenPropertiesProvider; + protected tokenPropertiesProvider: ITokenPropertiesProvider; constructor({ chainId, @@ -571,11 +577,30 @@ export class AlphaRouter } } + if (tokenValidatorProvider) { + this.tokenValidatorProvider = tokenValidatorProvider; + } else if (this.chainId === ChainId.MAINNET) { + this.tokenValidatorProvider = new TokenValidatorProvider( + this.chainId, + this.multicall2Provider, + new NodeJSCache(new NodeCache({ stdTTL: 30000, useClones: false })) + ); + } + if (tokenPropertiesProvider) { + this.tokenPropertiesProvider = tokenPropertiesProvider; + } else { + this.tokenPropertiesProvider = new TokenPropertiesProvider( + this.chainId, + this.tokenValidatorProvider!, + new NodeJSCache(new NodeCache({ stdTTL: 86400, useClones: false })), + new OnChainTokenFeeFetcher(this.chainId, provider) + ) + } this.v2PoolProvider = v2PoolProvider ?? new CachingV2PoolProvider( chainId, - new V2PoolProvider(chainId, this.multicall2Provider), + new V2PoolProvider(chainId, this.multicall2Provider, this.tokenPropertiesProvider), new NodeJSCache(new NodeCache({ stdTTL: 60, useClones: false })) ); @@ -684,25 +709,6 @@ export class AlphaRouter arbitrumGasDataProvider ?? new ArbitrumGasDataProvider(chainId, this.provider); } - if (tokenValidatorProvider) { - this.tokenValidatorProvider = tokenValidatorProvider; - } else if (this.chainId === ChainId.MAINNET) { - this.tokenValidatorProvider = new TokenValidatorProvider( - this.chainId, - this.multicall2Provider, - new NodeJSCache(new NodeCache({ stdTTL: 30000, useClones: false })) - ); - } - if (tokenPropertiesProvider) { - this.tokenPropertiesProvider = tokenPropertiesProvider; - } else if (this.chainId === ChainId.MAINNET) { - this.tokenPropertiesProvider = new TokenPropertiesProvider( - this.chainId, - this.tokenValidatorProvider!, - new NodeJSCache(new NodeCache({ stdTTL: 86400, useClones: false })), - new OnChainTokenFeeFetcher(this.chainId, provider) - ) - } // Initialize the Quoters. // Quoters are an abstraction encapsulating the business logic of fetching routes and quotes. @@ -714,7 +720,7 @@ export class AlphaRouter this.tokenProvider, this.chainId, this.blockedTokenListProvider, - this.tokenValidatorProvider + this.tokenValidatorProvider, ); this.v3Quoter = new V3Quoter( @@ -978,12 +984,17 @@ export class AlphaRouter const gasPriceWei = await this.getGasPriceWei(); const quoteToken = quoteCurrency.wrapped; + const providerConfig: ProviderConfig = { + ...routingConfig, + blockNumber, + additionalGasOverhead: NATIVE_OVERHEAD(this.chainId, amount.currency, quoteCurrency), + }; const [v3GasModel, mixedRouteGasModel] = await this.getGasModels( gasPriceWei, amount.currency.wrapped, quoteToken, - { blockNumber, additionalGasOverhead: NATIVE_OVERHEAD(this.chainId, amount.currency, quoteCurrency) } + providerConfig ); // Create a Set to sanitize the protocols input, a Set of undefined becomes an empty set, @@ -1167,12 +1178,42 @@ 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, - tokenIn, - tokenOut, + tokenInWithFotTax, + tokenOutWithFotTax, protocols.sort(), // sort it for consistency in the order of the protocols. await blockNumber, tradeType, @@ -1445,10 +1486,7 @@ export class AlphaRouter // Generate our distribution of amounts, i.e. fractions of the input amount. // We will get quotes for fractions of the input amount for different routes, then // combine to generate split routes. - const [percents, amounts] = this.getAmountDistribution( - amount, - routingConfig - ); + const [percents, amounts] = this.getAmountDistribution(amount, routingConfig); const noProtocolsSpecified = protocols.length === 0; const v3ProtocolSpecified = protocols.includes(Protocol.V3); @@ -1549,29 +1587,29 @@ export class AlphaRouter const beforeGetRoutesThenQuotes = Date.now(); quotePromises.push( - v2CandidatePoolsPromise.then((v2CandidatePools) => - this.v2Quoter.getRoutesThenQuotes( - tokenIn, - tokenOut, - amount, - amounts, - percents, - quoteToken, - v2CandidatePools!, - tradeType, - routingConfig, - undefined, - gasPriceWei - ).then((result) => { - metric.putMetric( - `SwapRouteFromChain_V2_GetRoutesThenQuotes_Load`, - Date.now() - beforeGetRoutesThenQuotes, - MetricLoggerUnit.Milliseconds - ); - - return result; - }) - ) + v2CandidatePoolsPromise.then((v2CandidatePools) => + this.v2Quoter.getRoutesThenQuotes( + tokenIn, + tokenOut, + amount, + amounts, + percents, + quoteToken, + v2CandidatePools!, + tradeType, + routingConfig, + undefined, + gasPriceWei + ).then((result) => { + metric.putMetric( + `SwapRouteFromChain_V2_GetRoutesThenQuotes_Load`, + Date.now() - beforeGetRoutesThenQuotes, + MetricLoggerUnit.Milliseconds + ); + + return result; + }) + ) ); } diff --git a/src/routers/alpha-router/functions/get-candidate-pools.ts b/src/routers/alpha-router/functions/get-candidate-pools.ts index 5968beca2..e5599276f 100644 --- a/src/routers/alpha-router/functions/get-candidate-pools.ts +++ b/src/routers/alpha-router/functions/get-candidate-pools.ts @@ -628,7 +628,6 @@ export async function getV2CandidatePools({ topNWithEachBaseToken, topNWithBaseToken, }, - debugRouting, } = routingConfig; const tokenInAddress = tokenIn.address.toLowerCase(); const tokenOutAddress = tokenOut.address.toLowerCase(); @@ -1088,7 +1087,9 @@ export async function getV2CandidatePools({ const beforePoolsLoad = Date.now(); - const poolAccessor = await poolProvider.getPools(tokenPairs, { blockNumber, debugRouting }); + // this should be the only place to enable fee-on-transfer fee fetching, + // because this places loads pools (pairs of tokens with fot taxes) from the subgraph + const poolAccessor = await poolProvider.getPools(tokenPairs, routingConfig); metric.putMetric( 'V2PoolsLoad', @@ -1130,7 +1131,6 @@ export async function getMixedRouteCandidatePools({ v2poolProvider, }: MixedRouteGetCandidatePoolsParams): Promise { const beforeSubgraphPools = Date.now(); - const { blockNumber, debugRouting } = routingConfig; const [ { subgraphPools: V3subgraphPools, candidatePools: V3candidatePools }, { subgraphPools: V2subgraphPools, candidatePools: V2candidatePools } @@ -1223,9 +1223,7 @@ export async function getMixedRouteCandidatePools({ `Getting the ${tokenAddresses.length} tokens within the ${subgraphPools.length} pools we are considering` ); - const tokenAccessor = await tokenProvider.getTokens(tokenAddresses, { - blockNumber, - }); + const tokenAccessor = await tokenProvider.getTokens(tokenAddresses, routingConfig); const V3tokenPairsRaw = _.map< V3SubgraphPool, @@ -1288,14 +1286,8 @@ export async function getMixedRouteCandidatePools({ const beforePoolsLoad = Date.now(); const [V2poolAccessor, V3poolAccessor] = await Promise.all([ - v2poolProvider.getPools(V2tokenPairs, { - blockNumber, - debugRouting, - }), - v3poolProvider.getPools(V3tokenPairs, { - blockNumber, - debugRouting - }), + v2poolProvider.getPools(V2tokenPairs, routingConfig), + v3poolProvider.getPools(V3tokenPairs, routingConfig), ]); metric.putMetric( diff --git a/src/routers/alpha-router/gas-models/gas-model.ts b/src/routers/alpha-router/gas-models/gas-model.ts index aabfb68b3..ba3c71b50 100644 --- a/src/routers/alpha-router/gas-models/gas-model.ts +++ b/src/routers/alpha-router/gas-models/gas-model.ts @@ -175,6 +175,7 @@ export abstract class IOnChainGasModelFactory { quoteToken, v2poolProvider: V2poolProvider, l2GasDataProvider, + providerConfig, }: BuildOnChainGasModelFactoryType): Promise< IGasModel >; diff --git a/src/routers/alpha-router/quoters/v2-quoter.ts b/src/routers/alpha-router/quoters/v2-quoter.ts index 71c64778a..63695eb92 100644 --- a/src/routers/alpha-router/quoters/v2-quoter.ts +++ b/src/routers/alpha-router/quoters/v2-quoter.ts @@ -153,6 +153,7 @@ export class V2Quoter extends BaseQuoter { gasPriceWei, poolProvider: this.v2PoolProvider, token: quoteToken, + providerConfig: _routingConfig, // TODO: implement wrap overhead for v2 routes }); 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 6ae6e3dc4..f0bed7945 100644 --- a/test/integ/routers/alpha-router/alpha-router.integration.test.ts +++ b/test/integ/routers/alpha-router/alpha-router.integration.test.ts @@ -67,7 +67,10 @@ import { WBTC_MOONBEAM, WETH9, WNATIVE_ON, + TokenPropertiesProvider, + TokenValidatorProvider, } from '../../../../src'; +import { OnChainTokenFeeFetcher } from '../../../../src/providers/token-fee-fetcher'; import { DEFAULT_ROUTING_CONFIG_BY_CHAIN } from '../../../../src/routers/alpha-router/config'; import { Permit2__factory } from '../../../../src/types/other/factories/Permit2__factory'; import { getBalanceAndApprove } from '../../../test-util/getBalanceAndApprove'; @@ -480,9 +483,25 @@ describe('alpha router integration', () => { new V3PoolProvider(ChainId.MAINNET, multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) ); + const tokenValidatorProvider = new TokenValidatorProvider( + ChainId.MAINNET, + multicall2Provider, + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) + ) + const tokenFeeFetcher = new OnChainTokenFeeFetcher( + ChainId.MAINNET, + hardhat.provider + ) + const tokenPropertiesProvider = new TokenPropertiesProvider( + ChainId.MAINNET, + tokenValidatorProvider, + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })), + tokenFeeFetcher + ) const v2PoolProvider = new V2PoolProvider( ChainId.MAINNET, - multicall2Provider + multicall2Provider, + tokenPropertiesProvider ); const ethEstimateGasSimulator = new EthEstimateGasSimulator( @@ -2686,7 +2705,22 @@ describe('quote for other networks', () => { new V3PoolProvider(chain, multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) ); - const v2PoolProvider = new V2PoolProvider(chain, multicall2Provider); + const tokenValidatorProvider = new TokenValidatorProvider( + ChainId.MAINNET, + multicall2Provider, + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) + ) + const tokenFeeFetcher = new OnChainTokenFeeFetcher( + ChainId.MAINNET, + hardhat.provider + ) + const tokenPropertiesProvider = new TokenPropertiesProvider( + ChainId.MAINNET, + tokenValidatorProvider, + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })), + tokenFeeFetcher + ) + const v2PoolProvider = new V2PoolProvider(chain, multicall2Provider, tokenPropertiesProvider); const ethEstimateGasSimulator = new EthEstimateGasSimulator( chain, diff --git a/test/unit/providers/token-properties-provider.test.ts b/test/unit/providers/token-properties-provider.test.ts index 94ef21b34..62e6938eb 100644 --- a/test/unit/providers/token-properties-provider.test.ts +++ b/test/unit/providers/token-properties-provider.test.ts @@ -84,7 +84,7 @@ describe('TokenPropertiesProvider', () => { const token = USDC_MAINNET expect(await tokenPropertiesResultCache.get(CACHE_KEY(ChainId.MAINNET, token.address.toLowerCase()))).toBeUndefined(); - const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties([token]); + const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties([token], { enableFeeOnTransferFeeFetching: true }); expect(tokenPropertiesMap[token.address.toLowerCase()]).toBeDefined(); assertExpectedTokenProperties(tokenPropertiesMap[token.address.toLowerCase()], BigNumber.from(213), BigNumber.from(800), TokenValidationResult.FOT); @@ -97,7 +97,7 @@ describe('TokenPropertiesProvider', () => { const token = USDC_MAINNET expect(await tokenPropertiesResultCache.get(CACHE_KEY(ChainId.MAINNET, token.address.toLowerCase()))).toBeUndefined(); - const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties([token]); + const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties([token], { enableFeeOnTransferFeeFetching: true }); expect(tokenPropertiesMap[token.address.toLowerCase()]).toBeDefined(); assertExpectedTokenProperties(tokenPropertiesMap[token.address.toLowerCase()], BigNumber.from(213), BigNumber.from(800), TokenValidationResult.FOT); sinon.assert.calledOnce(mockTokenFeeFetcher.fetchFees) @@ -110,7 +110,7 @@ describe('TokenPropertiesProvider', () => { it('succeeds to get token allowlist with no on-chain calls nor caching', async function() { const allowListToken = new Token(1, '0x777E2ae845272a2F540ebf6a3D03734A5a8f618e', 18); - const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties([allowListToken]); + const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties([allowListToken], { enableFeeOnTransferFeeFetching: true }); expect(tokenPropertiesMap[allowListToken.address.toLowerCase()]).toBeDefined(); expect(tokenPropertiesMap[allowListToken.address.toLowerCase()]?.tokenFeeResult).toBeUndefined(); @@ -138,7 +138,7 @@ describe('TokenPropertiesProvider', () => { return tokenToResult }); - const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties(tokens); + const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties(tokens, { enableFeeOnTransferFeeFetching: true }); for (const token of tokens) { const address = token.address.toLowerCase() @@ -185,7 +185,7 @@ describe('TokenPropertiesProvider', () => { }; }) - const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties(tokens); + const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties(tokens, { enableFeeOnTransferFeeFetching: true }); for (const token of tokens) { const address = token.address.toLowerCase() @@ -207,7 +207,7 @@ describe('TokenPropertiesProvider', () => { mockTokenFeeFetcher.fetchFees.withArgs(tokens.map(token => token.address)).throws(new Error('Failed to fetch fees for token 1')); - const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties(tokens); + const tokenPropertiesMap = await tokenPropertiesProvider.getTokensProperties(tokens, { enableFeeOnTransferFeeFetching: true }); for (const token of tokens) { const address = token.address.toLowerCase() diff --git a/test/unit/routers/alpha-router/alpha-router.test.ts b/test/unit/routers/alpha-router/alpha-router.test.ts index a6eef7be8..0790e0e57 100644 --- a/test/unit/routers/alpha-router/alpha-router.test.ts +++ b/test/unit/routers/alpha-router/alpha-router.test.ts @@ -515,6 +515,7 @@ describe('alpha router', () => { gasPriceWei: mockGasPriceWeiBN, poolProvider: sinon.match.any, token: WRAPPED_NATIVE_CURRENCY[1], + providerConfig: sinon.match.any, }) ).toBeTruthy(); expect( @@ -918,6 +919,7 @@ describe('alpha router', () => { gasPriceWei: mockGasPriceWeiBN, poolProvider: sinon.match.any, token: WRAPPED_NATIVE_CURRENCY[1], + providerConfig: sinon.match.any, }) ).toBeTruthy(); @@ -1103,6 +1105,7 @@ describe('alpha router', () => { gasPriceWei: mockGasPriceWeiBN, poolProvider: sinon.match.any, token: WRAPPED_NATIVE_CURRENCY[1], + providerConfig: sinon.match.any, }) ).toBeTruthy(); @@ -1512,6 +1515,7 @@ describe('alpha router', () => { gasPriceWei: mockGasPriceWeiBN, poolProvider: sinon.match.any, token: WRAPPED_NATIVE_CURRENCY[1], + providerConfig: sinon.match.any, }) ).toBeTruthy(); @@ -1884,6 +1888,7 @@ describe('alpha router', () => { gasPriceWei: mockGasPriceWeiBN, poolProvider: sinon.match.any, token: USDC, + providerConfig: sinon.match.any, }) ).toBeTruthy(); expect( @@ -2046,6 +2051,7 @@ describe('alpha router', () => { gasPriceWei: mockGasPriceWeiBN, poolProvider: sinon.match.any, token: USDC, + providerConfig: sinon.match.any, }) ).toBeTruthy(); expect( @@ -2125,6 +2131,7 @@ describe('alpha router', () => { gasPriceWei: mockGasPriceWeiBN, poolProvider: sinon.match.any, token: USDC, + providerConfig: sinon.match.any, }) ).toBeTruthy(); expect(