From 53e3b8e93a1b46c2a679d275f6d2ffc473b9c1e9 Mon Sep 17 00:00:00 2001 From: Miguel Cervera Date: Fri, 29 Sep 2023 14:32:39 -0700 Subject: [PATCH] Receive an option to decide if we calculate FOT tax or not (#146) * Receive an option to decide if we calculate FOT tax or not * Fix code style issues with Prettier --------- Co-authored-by: Lint Action --- .gitignore | 1 + src/entities/pair.test.ts | 182 +++++++++++++++++++++++--------------- src/entities/pair.ts | 18 ++-- 3 files changed, 125 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index de4d1f007..a284d1362 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist node_modules +.idea diff --git a/src/entities/pair.test.ts b/src/entities/pair.test.ts index 607400ef3..a819dbffc 100644 --- a/src/entities/pair.test.ts +++ b/src/entities/pair.test.ts @@ -199,78 +199,120 @@ describe('Pair', () => { 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) + let calculateFotFees: boolean = false + + describe('when calculating FOT fees', () => { + beforeEach(() => { + calculateFotFees = true + }) + + 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, calculateFotFees) + + // 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, calculateFotFees) + + // 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('when NOT calculating FOT fees', () => { + beforeEach(() => { + calculateFotFees = false + }) + + 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, calculateFotFees) + + const expectedOutputBlastAmount = '0.000000000000000098' + 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, calculateFotFees) - 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) + const expectedInputBlasterAmount = '0.000000093' + expect(inputBlasterAmount.toExact()).toEqual(expectedInputBlasterAmount) + }) }) }) }) diff --git a/src/entities/pair.ts b/src/entities/pair.ts index ef89c882e..4bac5e43c 100644 --- a/src/entities/pair.ts +++ b/src/entities/pair.ts @@ -178,7 +178,10 @@ export class Pair { * * @param inputAmount */ - public getOutputAmount(inputAmount: CurrencyAmount): [CurrencyAmount, Pair] { + public getOutputAmount( + inputAmount: CurrencyAmount, + calculateFotFees: boolean = false + ): [CurrencyAmount, Pair] { invariant(this.involvesToken(inputAmount.currency), 'TOKEN') if (JSBI.equal(this.reserve0.quotient, ZERO) || JSBI.equal(this.reserve1.quotient, ZERO)) { throw new InsufficientReservesError() @@ -186,7 +189,7 @@ export class Pair { const inputReserve = this.reserveOf(inputAmount.currency) const outputReserve = this.reserveOf(inputAmount.currency.equals(this.token0) ? this.token1 : this.token0) - const percentAfterSellFees = this.derivePercentAfterSellFees(inputAmount) + const percentAfterSellFees = calculateFotFees ? this.derivePercentAfterSellFees(inputAmount) : ZERO_PERCENT const inputAmountAfterTax = percentAfterSellFees.greaterThan(ZERO_PERCENT) ? CurrencyAmount.fromRawAmount( inputAmount.currency, @@ -206,7 +209,7 @@ export class Pair { throw new InsufficientInputAmountError() } - const percentAfterBuyFees = this.derivePercentAfterBuyFees(outputAmount) + const percentAfterBuyFees = calculateFotFees ? this.derivePercentAfterBuyFees(outputAmount) : ZERO_PERCENT const outputAmountAfterTax = percentAfterBuyFees.greaterThan(ZERO_PERCENT) ? CurrencyAmount.fromRawAmount( outputAmount.currency, @@ -265,9 +268,12 @@ export class Pair { * * @param outputAmount */ - public getInputAmount(outputAmount: CurrencyAmount): [CurrencyAmount, Pair] { + public getInputAmount( + outputAmount: CurrencyAmount, + calculateFotFees: boolean = false + ): [CurrencyAmount, Pair] { invariant(this.involvesToken(outputAmount.currency), 'TOKEN') - const percentAfterBuyFees = this.derivePercentAfterBuyFees(outputAmount) + const percentAfterBuyFees = calculateFotFees ? this.derivePercentAfterBuyFees(outputAmount) : ZERO_PERCENT const outputAmountBeforeTax = percentAfterBuyFees.greaterThan(ZERO_PERCENT) ? CurrencyAmount.fromRawAmount( outputAmount.currency, @@ -294,7 +300,7 @@ export class Pair { 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 ) - const percentAfterSellFees = this.derivePercentAfterSellFees(inputAmount) + const percentAfterSellFees = calculateFotFees ? this.derivePercentAfterSellFees(inputAmount) : ZERO_PERCENT const inputAmountBeforeTax = percentAfterSellFees.greaterThan(ZERO_PERCENT) ? CurrencyAmount.fromRawAmount( inputAmount.currency,