Commit ff80edf0 authored by jsy1218's avatar jsy1218 Committed by GitHub

v2 quote with fee-on-transfer tax considerations (#144)

* v2 quote with fot tax considerations

* Fix code style issues with Prettier

* fix tax amount check

* fix the quote fot taxation calculation and correct test assertions

* getAmountIn tax calculation bug fix

* Fix code style issues with Prettier

* update the getAmountIn formula derivation to make it clearer

* Fix code style issues with Prettier

* update the reserve as well as getAmountIn amountInAfterTax calculation order

* address feedbacks

* Fix code style issues with Prettier

---------
Co-authored-by: default avatarLint Action <lint-action@samuelmeuli.com>
parent 629a46be
import { Percent } from '@uniswap/sdk-core'
import JSBI from 'jsbi' import JSBI from 'jsbi'
export const FACTORY_ADDRESS = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f' export const FACTORY_ADDRESS = '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f'
...@@ -12,3 +13,7 @@ export const ONE = JSBI.BigInt(1) ...@@ -12,3 +13,7 @@ export const ONE = JSBI.BigInt(1)
export const FIVE = JSBI.BigInt(5) export const FIVE = JSBI.BigInt(5)
export const _997 = JSBI.BigInt(997) export const _997 = JSBI.BigInt(997)
export const _1000 = JSBI.BigInt(1000) export const _1000 = JSBI.BigInt(1000)
export const BASIS_POINTS = JSBI.BigInt(10000)
export const ZERO_PERCENT = new Percent(ZERO)
export const ONE_HUNDRED_PERCENT = new Percent(ONE)
import { Token, WETH9, Price, CurrencyAmount } from '@uniswap/sdk-core' import { ChainId, CurrencyAmount, Price, Token, WETH9 } from '@uniswap/sdk-core'
import { InsufficientInputAmountError } from '../errors' import { InsufficientInputAmountError } from '../errors'
import { computePairAddress, Pair } from './pair' import { computePairAddress, Pair } from './pair'
import { BigNumber } from '@ethersproject/bignumber'
describe('computePairAddress', () => { describe('computePairAddress', () => {
it('should correctly compute the pool address', () => { it('should correctly compute the pool address', () => {
...@@ -172,6 +173,107 @@ describe('Pair', () => { ...@@ -172,6 +173,107 @@ describe('Pair', () => {
) )
).toEqual(false) ).toEqual(false)
}) })
describe('getInputAmount and getOutputAmount', () => {
const BLASTBuyFeeBps = BigNumber.from(400)
const BLASTSellFeeBps = BigNumber.from(10000)
const BLAST = new Token(
ChainId.MAINNET,
'0x3ed643e9032230f01c6c36060e305ab53ad3b482',
18,
'BLAST',
'BLAST',
false,
BLASTBuyFeeBps,
BLASTSellFeeBps
)
const BLASTERSBuyFeeBps = BigNumber.from(300)
const BLASTERSSellFeeBps = BigNumber.from(350)
const BLASTERS = new Token(
ChainId.MAINNET,
'0xab98093C7232E98A47D7270CE0c1c2106f61C73b',
9,
'BLAST',
'BLASTERS',
false,
BLASTERSBuyFeeBps,
BLASTERSSellFeeBps
)
describe('getOutputAmount', () => {
it('getOutputAmount for input token BLASTERS and output token BLAST', () => {
const reserveBlasterAmount = CurrencyAmount.fromRawAmount(BLASTERS, '10000')
const reserveBlastAmount = CurrencyAmount.fromRawAmount(BLAST, '10000')
const pair = new Pair(reserveBlasterAmount, reserveBlastAmount)
const inputBlastersAmount = CurrencyAmount.fromRawAmount(BLASTERS, '100')
const [outputBlastAmount] = pair.getOutputAmount(inputBlastersAmount)
// Theoretical amount out:
// (10000 * 997 * 100 * (1 - 3.5%) / (10000 * 1000 + 997 * 100 * (1 - 3.5%))) * (1 - 4%)
// = 91.48
//
// However in practice, we have round down of precisions in multiple steps
// hence the amount out will be slightly less than 91.48:
//
// inputAmount = 100
// percentAfterSellFeesInDecimal = fraction(9650, 10000)
// inputAmountAfterTax = 100 * fraction(9650, 10000) = 96.5 = 96 (rounded down)
// inputAmountWithFeeAndAfterTax = 96 * 997 = 95712
// numerator = 95712 * 10000 = 957120000
// denominator = 10000 * 1000 + 95712 = 10095712
// outputAmount = 957120000 / 10095712 = 94.8046061536 = 94 (rounded down)
// buyFeePercentInDecimal = fraction(400, 10000)
// percentAfterBuyFeesInDecimal = fraction(9600, 10000)
// outputAmountAfterTax = 94 * fraction(9600, 10000)
// = 94 * 0.96
// = 90.24
// = 90 (rounded down)
const expectedOutputBlastAmount = '0.00000000000000009'
expect(outputBlastAmount.toExact()).toEqual(expectedOutputBlastAmount)
})
it('getInputAmount for input token BLASTERS and output token BLAST', () => {
const reserveBlasterAmount = CurrencyAmount.fromRawAmount(BLASTERS, '10000')
const reserveBlastAmount = CurrencyAmount.fromRawAmount(BLAST, '10000')
const pair = new Pair(reserveBlasterAmount, reserveBlastAmount)
const outputBlastAmount = CurrencyAmount.fromRawAmount(BLAST, '91')
const [inputBlasterAmount] = pair.getInputAmount(outputBlastAmount)
// Theoretical amount in:
// 10000 * 100 * (1 - 4%) * 1000 / ((10000 - 100 * (1 - 4%)) * 997) / (1 - 3.5%)
// = 100.7483934892
//
// However in practice, we have round up of precisions in multiple steps
// hence the amount out will be slightly more than 100.7483934892:
//
// buyFeePercentInDecimal = fraction(400, 10000)
// percentAfterBuyFeesInDecimal = 1 - fraction(400, 10000) = fraction(9600, 10000)
// outputAmountBeforeTax = 91 / fraction(960000, 10000) + 1
// = 91 / 0.96 + 1
// = 94.7916666667 + 1
// = 94 (rounded down) + 1
// = 95 (rounded up)
// numerator = 10000 * 95 * 1000 = 950000000
// denominator = (10000 - 95) * 997 = 9875285
// inputAmount = 950000000 / 9875285 + 1
// = 96.1997552476 + 1
// = 96 (rounded down) + 1
// = 97 (rounded up)
// sellFeePercentInDecimal = fraction(350, 10000)
// percentAfterSellFeesInDecimal = 1 - fraction(350, 10000) = fraction(9650, 10000)
// inputAmountBeforeTax = (97 / fraction(9650, 10000)) + 1
// = (97 / 0.965) + 1
// = 100.518134715 + 1
// = 100 (rounded down) + 1
// = 101
const expectedInputBlasterAmount = '0.000000101'
expect(inputBlasterAmount.toExact()).toEqual(expectedInputBlasterAmount)
})
})
})
describe('miscellaneous', () => { describe('miscellaneous', () => {
it('getLiquidityMinted:0', async () => { it('getLiquidityMinted:0', async () => {
const tokenA = new Token(3, '0x0000000000000000000000000000000000000001', 18) const tokenA = new Token(3, '0x0000000000000000000000000000000000000001', 18)
......
import { BigintIsh, Price, sqrt, Token, CurrencyAmount } from '@uniswap/sdk-core' import { BigintIsh, Price, sqrt, Token, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { pack, keccak256 } from '@ethersproject/solidity' import { pack, keccak256 } from '@ethersproject/solidity'
import { getCreate2Address } from '@ethersproject/address' import { getCreate2Address } from '@ethersproject/address'
import { BigNumber } from '@ethersproject/bignumber'
import { FACTORY_ADDRESS, INIT_CODE_HASH, MINIMUM_LIQUIDITY, FIVE, _997, _1000, ONE, ZERO } from '../constants' import {
FACTORY_ADDRESS,
INIT_CODE_HASH,
MINIMUM_LIQUIDITY,
FIVE,
_997,
_1000,
ONE,
ZERO,
BASIS_POINTS,
ONE_HUNDRED_PERCENT,
ZERO_PERCENT
} from '../constants'
import { InsufficientReservesError, InsufficientInputAmountError } from '../errors' import { InsufficientReservesError, InsufficientInputAmountError } from '../errors'
export const computePairAddress = ({ export const computePairAddress = ({
...@@ -106,6 +119,65 @@ export class Pair { ...@@ -106,6 +119,65 @@ export class Pair {
return token.equals(this.token0) ? this.reserve0 : this.reserve1 return token.equals(this.token0) ? this.reserve0 : this.reserve1
} }
/**
* getAmountOut is the linear algebra of reserve ratio against amountIn:amountOut.
* https://ethereum.stackexchange.com/questions/101629/what-is-math-for-uniswap-calculates-the-amountout-and-amountin-why-997-and-1000
* has the math deduction for the reserve calculation without fee-on-transfer fees.
*
* With fee-on-transfer tax, intuitively it's just:
* inputAmountWithFeeAndTax = 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn
* = (1 - amountIn.sellFeesBips / 10000) * amountInWithFee
* where amountInWithFee is the amountIn after taking out the LP fees
* outputAmountWithTax = amountOut * (1 - amountOut.buyFeesBips / 10000)
*
* But we are illustrating the math deduction below to ensure that's the case.
*
* before swap A * B = K where A = reserveIn B = reserveOut
*
* after swap A' * B' = K where only k is a constant value
*
* getAmountOut
*
* A' = A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn # here 0.3% is deducted
* B' = B - amountOut * (1 - amountOut.buyFeesBips / 10000)
* amountOut = (B - B') / (1 - amountOut.buyFeesBips / 10000) # where A' * B' still is k
* = (B - K/(A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn))
* /
* (1 - amountOut.buyFeesBips / 10000)
* = (B - AB/(A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn))
* /
* (1 - amountOut.buyFeesBips / 10000)
* = ((BA + B * 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn - AB)/(A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn))
* /
* (1 - amountOut.buyFeesBips / 10000)
* = (B * 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn / (A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn)
* /
* (1 - amountOut.buyFeesBips / 10000)
* amountOut * (1 - amountOut.buyFeesBips / 10000) = (B * 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn
* /
* (A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn)
*
* outputAmountWithTax = (B * 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn
* /
* (A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn)
* = (B * 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn * 1000
* /
* ((A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn) * 1000)
* = (B * (1 - amountIn.sellFeesBips / 10000) 997 * * amountIn
* /
* (1000 * A + (1 - amountIn.sellFeesBips / 10000) * 997 * amountIn)
* = (B * (1 - amountIn.sellFeesBips / 10000) * inputAmountWithFee)
* /
* (1000 * A + (1 - amountIn.sellFeesBips / 10000) * inputAmountWithFee)
* = (B * inputAmountWithFeeAndTax)
* /
* (1000 * A + inputAmountWithFeeAndTax)
*
* inputAmountWithFeeAndTax = (1 - amountIn.sellFeesBips / 10000) * inputAmountWithFee
* outputAmountWithTax = amountOut * (1 - amountOut.buyFeesBips / 10000)
*
* @param inputAmount
*/
public getOutputAmount(inputAmount: CurrencyAmount<Token>): [CurrencyAmount<Token>, Pair] { public getOutputAmount(inputAmount: CurrencyAmount<Token>): [CurrencyAmount<Token>, Pair] {
invariant(this.involvesToken(inputAmount.currency), 'TOKEN') invariant(this.involvesToken(inputAmount.currency), 'TOKEN')
if (JSBI.equal(this.reserve0.quotient, ZERO) || JSBI.equal(this.reserve1.quotient, ZERO)) { if (JSBI.equal(this.reserve0.quotient, ZERO) || JSBI.equal(this.reserve1.quotient, ZERO)) {
...@@ -113,38 +185,123 @@ export class Pair { ...@@ -113,38 +185,123 @@ export class Pair {
} }
const inputReserve = this.reserveOf(inputAmount.currency) const inputReserve = this.reserveOf(inputAmount.currency)
const outputReserve = this.reserveOf(inputAmount.currency.equals(this.token0) ? this.token1 : this.token0) const outputReserve = this.reserveOf(inputAmount.currency.equals(this.token0) ? this.token1 : this.token0)
const inputAmountWithFee = JSBI.multiply(inputAmount.quotient, _997)
const numerator = JSBI.multiply(inputAmountWithFee, outputReserve.quotient) const percentAfterSellFees = this.derivePercentAfterSellFees(inputAmount)
const denominator = JSBI.add(JSBI.multiply(inputReserve.quotient, _1000), inputAmountWithFee) const inputAmountAfterTax = percentAfterSellFees.greaterThan(ZERO_PERCENT)
? CurrencyAmount.fromRawAmount(
inputAmount.currency,
percentAfterSellFees.multiply(inputAmount).quotient // fraction.quotient will round down by itself, which is desired
)
: inputAmount
const inputAmountWithFeeAndAfterTax = JSBI.multiply(inputAmountAfterTax.quotient, _997)
const numerator = JSBI.multiply(inputAmountWithFeeAndAfterTax, outputReserve.quotient)
const denominator = JSBI.add(JSBI.multiply(inputReserve.quotient, _1000), inputAmountWithFeeAndAfterTax)
const outputAmount = CurrencyAmount.fromRawAmount( const outputAmount = CurrencyAmount.fromRawAmount(
inputAmount.currency.equals(this.token0) ? this.token1 : this.token0, inputAmount.currency.equals(this.token0) ? this.token1 : this.token0,
JSBI.divide(numerator, denominator) JSBI.divide(numerator, denominator) // JSBI.divide will round down by itself, which is desired
) )
if (JSBI.equal(outputAmount.quotient, ZERO)) { if (JSBI.equal(outputAmount.quotient, ZERO)) {
throw new InsufficientInputAmountError() throw new InsufficientInputAmountError()
} }
return [outputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))]
const percentAfterBuyFees = this.derivePercentAfterBuyFees(outputAmount)
const outputAmountAfterTax = percentAfterBuyFees.greaterThan(ZERO_PERCENT)
? CurrencyAmount.fromRawAmount(
outputAmount.currency,
outputAmount.multiply(percentAfterBuyFees).quotient // fraction.quotient will round down by itself, which is desired
)
: outputAmount
if (JSBI.equal(outputAmountAfterTax.quotient, ZERO)) {
throw new InsufficientInputAmountError()
}
return [
outputAmountAfterTax,
new Pair(inputReserve.add(inputAmountAfterTax), outputReserve.subtract(outputAmountAfterTax))
]
} }
/**
* getAmountIn is the linear algebra of reserve ratio against amountIn:amountOut.
* https://ethereum.stackexchange.com/questions/101629/what-is-math-for-uniswap-calculates-the-amountout-and-amountin-why-997-and-1000
* has the math deduction for the reserve calculation without fee-on-transfer fees.
*
* With fee-on-transfer fees, intuitively it's just:
* outputAmountWithTax = amountOut / (1 - amountOut.buyFeesBips / 10000)
* inputAmountWithTax = amountIn / (1 - amountIn.sellFeesBips / 10000) / 0.997
*
* But we are illustrating the math deduction below to ensure that's the case.
*
* before swap A * B = K where A = reserveIn B = reserveOut
*
* after swap A' * B' = K where only k is a constant value
*
* getAmountIn
*
* B' = B - amountOut * (1 - amountOut.buyFeesBips / 10000)
* A' = A + 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn # here 0.3% is deducted
* amountIn = (A' - A) / (0.997 * (1 - amountIn.sellFeesBips / 10000))
* = (K / (B - amountOut / (1 - amountOut.buyFeesBips / 10000)) - A)
* /
* (0.997 * (1 - amountIn.sellFeesBips / 10000))
* = (AB / (B - amountOut / (1 - amountOut.buyFeesBips / 10000)) - A)
* /
* (0.997 * (1 - amountIn.sellFeesBips / 10000))
* = ((AB - AB + A * amountOut / (1 - amountOut.buyFeesBips / 10000)) / (B - amountOut / (1 - amountOut.buyFeesBips / 10000)))
* /
* (0.997 * (1 - amountIn.sellFeesBips / 10000))
* = ((A * amountOut / (1 - amountOut.buyFeesBips / 10000)) / (B - amountOut / (1 - amountOut.buyFeesBips / 10000)))
* /
* (0.997 * (1 - amountIn.sellFeesBips / 10000))
* = ((A * 1000 * amountOut / (1 - amountOut.buyFeesBips / 10000)) / (B - amountOut / (1 - amountOut.buyFeesBips / 10000)))
* /
* (997 * (1 - amountIn.sellFeesBips / 10000))
*
* outputAmountWithTax = amountOut / (1 - amountOut.buyFeesBips / 10000)
* inputAmountWithTax = amountIn / (997 * (1 - amountIn.sellFeesBips / 10000))
* = (A * outputAmountWithTax * 1000) / ((B - outputAmountWithTax) * 997)
*
* @param outputAmount
*/
public getInputAmount(outputAmount: CurrencyAmount<Token>): [CurrencyAmount<Token>, Pair] { public getInputAmount(outputAmount: CurrencyAmount<Token>): [CurrencyAmount<Token>, Pair] {
invariant(this.involvesToken(outputAmount.currency), 'TOKEN') invariant(this.involvesToken(outputAmount.currency), 'TOKEN')
const percentAfterBuyFees = this.derivePercentAfterBuyFees(outputAmount)
const outputAmountBeforeTax = percentAfterBuyFees.greaterThan(ZERO_PERCENT)
? CurrencyAmount.fromRawAmount(
outputAmount.currency,
JSBI.add(outputAmount.divide(percentAfterBuyFees).quotient, ONE) // add 1 for rounding up
)
: outputAmount
if ( if (
JSBI.equal(this.reserve0.quotient, ZERO) || JSBI.equal(this.reserve0.quotient, ZERO) ||
JSBI.equal(this.reserve1.quotient, ZERO) || JSBI.equal(this.reserve1.quotient, ZERO) ||
JSBI.greaterThanOrEqual(outputAmount.quotient, this.reserveOf(outputAmount.currency).quotient) JSBI.greaterThanOrEqual(outputAmount.quotient, this.reserveOf(outputAmount.currency).quotient) ||
JSBI.greaterThanOrEqual(outputAmountBeforeTax.quotient, this.reserveOf(outputAmount.currency).quotient)
) { ) {
throw new InsufficientReservesError() throw new InsufficientReservesError()
} }
const outputReserve = this.reserveOf(outputAmount.currency) const outputReserve = this.reserveOf(outputAmount.currency)
const inputReserve = this.reserveOf(outputAmount.currency.equals(this.token0) ? this.token1 : this.token0) const inputReserve = this.reserveOf(outputAmount.currency.equals(this.token0) ? this.token1 : this.token0)
const numerator = JSBI.multiply(JSBI.multiply(inputReserve.quotient, outputAmount.quotient), _1000)
const denominator = JSBI.multiply(JSBI.subtract(outputReserve.quotient, outputAmount.quotient), _997) const numerator = JSBI.multiply(JSBI.multiply(inputReserve.quotient, outputAmountBeforeTax.quotient), _1000)
const denominator = JSBI.multiply(JSBI.subtract(outputReserve.quotient, outputAmountBeforeTax.quotient), _997)
const inputAmount = CurrencyAmount.fromRawAmount( const inputAmount = CurrencyAmount.fromRawAmount(
outputAmount.currency.equals(this.token0) ? this.token1 : this.token0, outputAmount.currency.equals(this.token0) ? this.token1 : this.token0,
JSBI.add(JSBI.divide(numerator, denominator), ONE) JSBI.add(JSBI.divide(numerator, denominator), ONE) // add 1 here is part of the formula, no rounding needed here, since there will not be decimal at this point
) )
return [inputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))]
const percentAfterSellFees = this.derivePercentAfterSellFees(inputAmount)
const inputAmountBeforeTax = percentAfterSellFees.greaterThan(ZERO_PERCENT)
? CurrencyAmount.fromRawAmount(
inputAmount.currency,
JSBI.add(inputAmount.divide(percentAfterSellFees).quotient, ONE) // add 1 for rounding up
)
: inputAmount
return [inputAmountBeforeTax, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))]
} }
public getLiquidityMinted( public getLiquidityMinted(
...@@ -214,4 +371,22 @@ export class Pair { ...@@ -214,4 +371,22 @@ export class Pair {
JSBI.divide(JSBI.multiply(liquidity.quotient, this.reserveOf(token).quotient), totalSupplyAdjusted.quotient) JSBI.divide(JSBI.multiply(liquidity.quotient, this.reserveOf(token).quotient), totalSupplyAdjusted.quotient)
) )
} }
private derivePercentAfterSellFees(inputAmount: CurrencyAmount<Token>): Percent {
const sellFeeBips = inputAmount.currency.sellFeeBps
if (sellFeeBips?.gt(BigNumber.from(0))) {
return ONE_HUNDRED_PERCENT.subtract(new Percent(JSBI.BigInt(sellFeeBips)).divide(BASIS_POINTS))
} else {
return ZERO_PERCENT
}
}
private derivePercentAfterBuyFees(outputAmount: CurrencyAmount<Token>): Percent {
const buyFeeBps = outputAmount.currency.buyFeeBps
if (buyFeeBps?.gt(BigNumber.from(0))) {
return ONE_HUNDRED_PERCENT.subtract(new Percent(JSBI.BigInt(buyFeeBps)).divide(BASIS_POINTS))
} else {
return ZERO_PERCENT
}
}
} }
...@@ -1706,10 +1706,10 @@ ...@@ -1706,10 +1706,10 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@uniswap/sdk-core@^4.0.2": "@uniswap/sdk-core@^4.0.7":
version "4.0.2" version "4.0.7"
resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-4.0.2.tgz#2eca2b5bf00bad74519aef918465c19218285b4b" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-4.0.7.tgz#90dfd070d7e44494234618af398da158363ae827"
integrity sha512-rR5xobsAAP4yMYC7C+0+duVx0pFoDn2lV9kTWpoKgH1WJuw7hD1uDEvuevU2dL89TuixVgGvnYd0QxmrMtsIlg== integrity sha512-jscx7KUIWzQatcL5PHY6xy0gEL9IGQcL5h/obxzX9foP2KoNk9cq66Ia8I2Kvpa7zBcPOeW1hU0hJNBq6CzcIQ==
dependencies: dependencies:
"@ethersproject/address" "^5.0.2" "@ethersproject/address" "^5.0.2"
big.js "^5.2.2" big.js "^5.2.2"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment