Skip to content

Commit

Permalink
Fix: quote provider has the enable fot flag awareness (Uniswap#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsy1218 authored Sep 15, 2023
1 parent 12f21e1 commit 17d6310
Show file tree
Hide file tree
Showing 5 changed files with 426 additions and 57 deletions.
140 changes: 119 additions & 21 deletions src/providers/v2/quote-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ 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';

// Quotes can be null (e.g. pool did not have enough liquidity).
export type V2AmountQuote = {
Expand All @@ -21,12 +23,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[] }>;
}

Expand All @@ -44,22 +48,35 @@ 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[] = [];

Expand All @@ -75,14 +92,58 @@ 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 (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(amount)} 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
);
}

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;
}
} else {
const [outputAmountNew] = pair.getOutputAmount(outputAmount);
outputAmount = outputAmountNew;
Expand All @@ -98,12 +159,49 @@ 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 (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(amount)} 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
);
}

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 inputAmountWithBuyFeeBps = CurrencyAmount.fromRawAmount(
pair.token1,
inputAmount.quotient
);
[inputAmount] = pair.getInputAmount(inputAmountWithBuyFeeBps);
} else {
[inputAmount] = pair.getInputAmount(inputAmount);
}
} else {
[inputAmount] = pair.getInputAmount(inputAmount);
}
Expand Down
36 changes: 3 additions & 33 deletions src/routers/alpha-router/alpha-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
StaticV2SubgraphProvider,
StaticV3SubgraphProvider,
SwapRouterProvider,
TokenPropertiesProvider, TokenValidationResult,
TokenPropertiesProvider,
UniswapMulticallProvider,
URISubgraphProvider,
V2QuoteProvider,
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 12 additions & 3 deletions src/routers/alpha-router/quoters/v2-quoter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -146,7 +155,7 @@ export class V2Quoter extends BaseQuoter<V2CandidatePools, V2Route> {
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,
Expand Down
55 changes: 55 additions & 0 deletions test/test-util/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,58 @@ 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)
)
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,
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
18,
'stETH',
'stETH',
false,
BigNumber.from(0),
BigNumber.from(0)
)
Loading

0 comments on commit 17d6310

Please sign in to comment.