Skip to content
This repository has been archived by the owner on Apr 25, 2024. It is now read-only.

feat: Handle fee-on-transfer token taxes in price impact calculations #50

Merged
merged 6 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"license": "MIT",
"dependencies": {
"@ethersproject/abi": "^5.5.0",
"@uniswap/sdk-core": "^4",
"@uniswap/sdk-core": "^4.0.7",
"@uniswap/swap-router-contracts": "^1.1.0",
"@uniswap/v2-sdk": "^3.2.0",
"@uniswap/v3-sdk": "^3.10.0"
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Percent } from '@uniswap/sdk-core'
import JSBI from 'jsbi'

export const MSG_SENDER = '0x0000000000000000000000000000000000000001'
Expand All @@ -8,3 +9,5 @@ export const ONE = JSBI.BigInt(1)

// = 1 << 23 or 100000000000000000000000
export const V2_FEE_PATH_PLACEHOLDER = 8388608

export const ZERO_PERCENT = new Percent(ZERO)
75 changes: 75 additions & 0 deletions src/entities/trade.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { sqrt, Token, CurrencyAmount, TradeType, WETH9, Ether, Percent, Price } from '@uniswap/sdk-core'
import { BigNumber } from '@ethersproject/bignumber'
import JSBI from 'jsbi'
import { MixedRoute, RouteV2, RouteV3 } from './route'
import { Trade } from './trade'
Expand All @@ -21,6 +22,26 @@ describe('Trade', () => {
const token1 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 't1', 'token1')
const token2 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 't2', 'token2')
const token3 = new Token(1, '0x0000000000000000000000000000000000000004', 18, 't3', 'token3')
const token4WithTax = new Token(
1,
'0x0000000000000000000000000000000000000005',
18,
't4',
'token4',
false,
BigNumber.from(100),
BigNumber.from(100)
)
const token5WithTax = new Token(
1,
'0x0000000000000000000000000000000000000005',
18,
't5',
'token5',
false,
BigNumber.from(500),
BigNumber.from(500)
)

function v2StylePool(
reserve0: CurrencyAmount<Token>,
Expand Down Expand Up @@ -101,6 +122,16 @@ describe('Trade', () => {
CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(10000))
)

const pair_tax_output = new Pair(
CurrencyAmount.fromRawAmount(weth, JSBI.BigInt(100000)),
CurrencyAmount.fromRawAmount(token4WithTax, JSBI.BigInt(100000))
)

const pair_tax_input = new Pair(
CurrencyAmount.fromRawAmount(token5WithTax, JSBI.BigInt(100000)),
CurrencyAmount.fromRawAmount(weth, JSBI.BigInt(100000))
)

const pool_weth_0 = v2StylePool(
CurrencyAmount.fromRawAmount(weth, JSBI.BigInt(100000)),
CurrencyAmount.fromRawAmount(token0, JSBI.BigInt(100000))
Expand Down Expand Up @@ -1160,6 +1191,50 @@ describe('Trade', () => {
})
// v3 sdk price impact tests
describe('#priceImpact', () => {
tinaszheng marked this conversation as resolved.
Show resolved Hide resolved
describe('with FOT sell fees', () => {
Copy link
Member

@jsy1218 jsy1218 Sep 19, 2023

Choose a reason for hiding this comment

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

nit - technically we only support FOT that goes through v2-pools, not v3-pools. But I think it's fine to write unit test coverage against v3 routes, since Trade object is being used by v2, v3 and mixed routes, as a common object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point, updated to v2 routes since it should match our real world use case :D

const routev2 = new V2RouteSDK([pair_tax_output], weth, token4WithTax)
const trade = new Trade({
v2Routes: [
{
routev2,
inputAmount: CurrencyAmount.fromRawAmount(weth, 100),
outputAmount: CurrencyAmount.fromRawAmount(token4WithTax, 69),
},
],
v3Routes: [],
tradeType: TradeType.EXACT_INPUT,
})

it('is cached', () => {
expect(trade.priceImpact === trade.priceImpact).toStrictEqual(true)
})
it('is correct', () => {
expect(trade.priceImpact.toSignificant(3)).toEqual('30.3')
})
})

describe('with FOT buy fees', () => {
const routev2 = new V2RouteSDK([pair_tax_input], token5WithTax, weth)
const trade = new Trade({
v2Routes: [
{
routev2,
inputAmount: CurrencyAmount.fromRawAmount(token5WithTax, 100),
outputAmount: CurrencyAmount.fromRawAmount(weth, 69),
},
],
v3Routes: [],
tradeType: TradeType.EXACT_INPUT,
})

it('is cached', () => {
expect(trade.priceImpact === trade.priceImpact).toStrictEqual(true)
})
it('is correct', () => {
expect(trade.priceImpact.toSignificant(3)).toEqual('27.4')
})
})

describe('tradeType = EXACT_INPUT', () => {
const routev3 = new V3RouteSDK([pool_0_1, pool_1_2], token0, token2)
const mixedRoute = new MixedRouteSDK([pool_0_1, pool_1_2], token0, token2)
Expand Down
32 changes: 28 additions & 4 deletions src/entities/trade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Currency, CurrencyAmount, Fraction, Percent, Price, TradeType } from '@
import { Pair, Route as V2RouteSDK, Trade as V2TradeSDK } from '@uniswap/v2-sdk'
import { Pool, Route as V3RouteSDK, Trade as V3TradeSDK } from '@uniswap/v3-sdk'
import invariant from 'tiny-invariant'
import { ONE, ZERO } from '../constants'
import { ONE, ZERO, ZERO_PERCENT } from '../constants'
import { MixedRouteSDK } from './mixedRoute/route'
import { MixedRouteTrade as MixedRouteTradeSDK } from './mixedRoute/trade'
import { IRoute, MixedRoute, RouteV2, RouteV3 } from './route'
Expand Down Expand Up @@ -158,13 +158,35 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
)
}

/**
* Returns the sell tax of the input token
*/
public get inputTax(): Percent {
const inputCurrency = this.inputAmount.currency
if (inputCurrency.isNative || !inputCurrency.wrapped.sellFeeBps) return ZERO_PERCENT

return new Percent(inputCurrency.wrapped.sellFeeBps.toNumber(), 10000)
}

/**
* Returns the buy tax of the output token
*/
public get outputTax(): Percent {
const outputCurrency = this.outputAmount.currency
if (outputCurrency.isNative || !outputCurrency.wrapped.buyFeeBps) return ZERO_PERCENT

return new Percent(outputCurrency.wrapped.buyFeeBps.toNumber(), 10000)
}

/**
* The cached result of the price impact computation
* @private
*/
private _priceImpact: Percent | undefined
/**
* Returns the percent difference between the route's mid price and the price impact
* Returns the percent difference between the route's mid price and the expected execution price
* In order to exclude token taxes from the price impact calculation, the spot price is calculated
* using a ratio of values that go into the pools, which are the post-tax input amount and pre-tax output amount.
*/
public get priceImpact(): Percent {
if (this._priceImpact) {
Expand All @@ -174,10 +196,12 @@ export class Trade<TInput extends Currency, TOutput extends Currency, TTradeType
let spotOutputAmount = CurrencyAmount.fromRawAmount(this.outputAmount.currency, 0)
for (const { route, inputAmount } of this.swaps) {
const midPrice = route.midPrice
spotOutputAmount = spotOutputAmount.add(midPrice.quote(inputAmount))
const postTaxInputAmount = inputAmount.multiply(new Fraction(ONE).subtract(this.inputTax))
spotOutputAmount = spotOutputAmount.add(midPrice.quote(postTaxInputAmount))
}

const priceImpact = spotOutputAmount.subtract(this.outputAmount).divide(spotOutputAmount)
const preTaxOutputAmount = this.outputAmount.divide(new Fraction(ONE).subtract(this.outputTax))
const priceImpact = spotOutputAmount.subtract(preTaxOutputAmount).divide(spotOutputAmount)
this._priceImpact = new Percent(priceImpact.numerator, priceImpact.denominator)

return this._priceImpact
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1575,10 +1575,10 @@
resolved "https://registry.npmjs.org/@uniswap/lib/-/lib-4.0.1-alpha.tgz"
integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==

"@uniswap/sdk-core@^4", "@uniswap/sdk-core@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-4.0.2.tgz#2eca2b5bf00bad74519aef918465c19218285b4b"
integrity sha512-rR5xobsAAP4yMYC7C+0+duVx0pFoDn2lV9kTWpoKgH1WJuw7hD1uDEvuevU2dL89TuixVgGvnYd0QxmrMtsIlg==
"@uniswap/sdk-core@^4", "@uniswap/sdk-core@^4.0.2", "@uniswap/sdk-core@^4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-4.0.7.tgz#90dfd070d7e44494234618af398da158363ae827"
integrity sha512-jscx7KUIWzQatcL5PHY6xy0gEL9IGQcL5h/obxzX9foP2KoNk9cq66Ia8I2Kvpa7zBcPOeW1hU0hJNBq6CzcIQ==
dependencies:
"@ethersproject/address" "^5.0.2"
big.js "^5.2.2"
Expand Down