diff --git a/package.json b/package.json index 2158dc0d0..ce40288c1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "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" }, diff --git a/src/entities/pair.ts b/src/entities/pair.ts index d76e34f54..6bea0a7c3 100644 --- a/src/entities/pair.ts +++ b/src/entities/pair.ts @@ -106,6 +106,61 @@ export class Pair { 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 fees, intuitively it's just: + * inputAmountWithFeeAndTax = 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn + * = (1 - amountIn.sellFeesBips / 10000) * amountInWithFee + * 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 * 997 * (1 - amountIn.sellFeesBips / 10000) * amountIn + * / + * (1000 * A + 997 * (1 - amountIn.sellFeesBips / 10000) * 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): [CurrencyAmount, Pair] { invariant(this.involvesToken(inputAmount.currency), 'TOKEN') if (JSBI.equal(this.reserve0.quotient, ZERO) || JSBI.equal(this.reserve1.quotient, ZERO)) { @@ -114,8 +169,12 @@ export class Pair { const inputReserve = this.reserveOf(inputAmount.currency) 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 denominator = JSBI.add(JSBI.multiply(inputReserve.quotient, _1000), inputAmountWithFee) + + const inputAmountWithFeeAndTax = this.deriveInputAmountWithTax( + CurrencyAmount.fromRawAmount(inputAmount.currency, inputAmountWithFee)); + + const numerator = JSBI.multiply(inputAmountWithFeeAndTax.quotient, outputReserve.quotient) + const denominator = JSBI.add(JSBI.multiply(inputReserve.quotient, _1000), inputAmountWithFeeAndTax.quotient) const outputAmount = CurrencyAmount.fromRawAmount( inputAmount.currency.equals(this.token0) ? this.token1 : this.token0, JSBI.divide(numerator, denominator) @@ -123,9 +182,54 @@ export class Pair { if (JSBI.equal(outputAmount.quotient, ZERO)) { throw new InsufficientInputAmountError() } - return [outputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] + + const outputAmountWithTax = this.deriveOutputAmountWithTax(outputAmount) + return [outputAmountWithTax, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] } + /** + * 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 = 0.997 * (1 - amountIn.sellFeesBips / 10000) * amountIn + * = (1 - amountIn.sellFeesBips / 10000) * amountInWithFee + * + * 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 * amountOut * 1000 * (1 - amountOut.buyFeesBips / 10000)) / (B - amountOut * (1 - amountOut.buyFeesBips / 10000))) + * / + * (997 * (1 - amountIn.sellFeesBips / 10000)) + * + * outputAmountWithTax = amountOut * (1 - amountOut.buyFeesBips / 10000) + * inputAmountWithTax = (1 - amountIn.sellFeesBips / 10000) * amountIn + * = (A * outputAmountWithTax * 1000) / ((B - outputAmountWithTax) * 997) + * + * @param outputAmount + */ public getInputAmount(outputAmount: CurrencyAmount): [CurrencyAmount, Pair] { invariant(this.involvesToken(outputAmount.currency), 'TOKEN') if ( @@ -138,13 +242,16 @@ export class Pair { const outputReserve = this.reserveOf(outputAmount.currency) 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 outputAmountWithTax = this.deriveOutputAmountWithTax(outputAmount) + const numerator = JSBI.multiply(JSBI.multiply(inputReserve.quotient, outputAmountWithTax.quotient), _1000) + const denominator = JSBI.multiply(JSBI.subtract(outputReserve.quotient, outputAmountWithTax.quotient), _997) const inputAmount = CurrencyAmount.fromRawAmount( outputAmount.currency.equals(this.token0) ? this.token1 : this.token0, JSBI.add(JSBI.divide(numerator, denominator), ONE) ) - return [inputAmount, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] + const inputAmountWithTax = this.deriveInputAmountWithTax(inputAmount) + return [inputAmountWithTax, new Pair(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))] } public getLiquidityMinted( @@ -214,4 +321,32 @@ export class Pair { JSBI.divide(JSBI.multiply(liquidity.quotient, this.reserveOf(token).quotient), totalSupplyAdjusted.quotient) ) } + + private deriveInputAmountWithTax(inputAmount: CurrencyAmount): CurrencyAmount { + const sellFeeBips = inputAmount.currency.sellFeeBps + if (sellFeeBips) { + const sellFeePercentInDecimal = JSBI.divide(JSBI.BigInt(inputAmount.currency.sellFeeBps), JSBI.BigInt(10000)) + const taxAmount = JSBI.multiply(inputAmount.quotient, sellFeePercentInDecimal) + return CurrencyAmount.fromRawAmount( + inputAmount.currency, + JSBI.subtract(inputAmount.quotient, taxAmount) + ); + } else { + return inputAmount + } + } + + private deriveOutputAmountWithTax(outputAmount: CurrencyAmount): CurrencyAmount { + const buyFeeBps = outputAmount.currency.buyFeeBps + if (buyFeeBps) { + const buyFeePercentInDecimal = JSBI.divide(JSBI.BigInt(outputAmount.currency.buyFeeBps), JSBI.BigInt(10000)) + const taxAmount = JSBI.multiply(outputAmount.quotient, buyFeePercentInDecimal) + return CurrencyAmount.fromRawAmount( + outputAmount.currency, + JSBI.subtract(outputAmount.quotient, taxAmount) + ) + } else { + return outputAmount + } + } } diff --git a/yarn.lock b/yarn.lock index 66e2e97a7..7f339d5c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1706,10 +1706,10 @@ semver "^7.3.2" tsutils "^3.17.1" -"@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.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"