From 1bb5b881dcdc477a8433eded5f8ed9f0760a7f17 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Thu, 30 May 2024 17:02:19 -0400 Subject: [PATCH] feat(v3-sdk): export v3Swap logic (#30) --- sdks/v2-sdk/src/router.test.ts | 2 +- sdks/v3-sdk/src/entities/pool.ts | 123 +++-------------------------- sdks/v3-sdk/src/utils/index.ts | 1 + sdks/v3-sdk/src/utils/swapMath.ts | 9 ++- sdks/v3-sdk/src/utils/v3swap.ts | 127 ++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 115 deletions(-) create mode 100644 sdks/v3-sdk/src/utils/v3swap.ts diff --git a/sdks/v2-sdk/src/router.test.ts b/sdks/v2-sdk/src/router.test.ts index 4df510ff7..720641241 100644 --- a/sdks/v2-sdk/src/router.test.ts +++ b/sdks/v2-sdk/src/router.test.ts @@ -28,7 +28,7 @@ describe('Router', () => { describe('#swapCallParameters', () => { describe('exact in', () => { - it.only('ether to token1', () => { + it('ether to token1', () => { const result = Router.swapCallParameters( Trade.exactIn( new Route([pair_weth_0, pair_0_1], ETHER, token1), diff --git a/sdks/v3-sdk/src/entities/pool.ts b/sdks/v3-sdk/src/entities/pool.ts index 77762e9b1..851ce3373 100644 --- a/sdks/v3-sdk/src/entities/pool.ts +++ b/sdks/v3-sdk/src/entities/pool.ts @@ -2,25 +2,14 @@ import { BigintIsh, CurrencyAmount, Price, Token } from '@uniswap/sdk-core' import JSBI from 'jsbi' import invariant from 'tiny-invariant' import { FACTORY_ADDRESS, FeeAmount, TICK_SPACINGS } from '../constants' -import { NEGATIVE_ONE, ONE, Q192, ZERO } from '../internalConstants' +import { NEGATIVE_ONE, Q192 } from '../internalConstants' import { computePoolAddress } from '../utils/computePoolAddress' -import { LiquidityMath } from '../utils/liquidityMath' -import { SwapMath } from '../utils/swapMath' +import { v3Swap } from '../utils/v3swap' import { TickMath } from '../utils/tickMath' import { Tick, TickConstructorArgs } from './tick' import { NoTickDataProvider, TickDataProvider } from './tickDataProvider' import { TickListDataProvider } from './tickListDataProvider' -interface StepComputations { - sqrtPriceStartX96: JSBI - tickNext: number - initialized: boolean - sqrtPriceNextX96: JSBI - amountIn: JSBI - amountOut: JSBI - feeAmount: JSBI -} - /** * By default, pools will not allow operations that require ticks. */ @@ -219,103 +208,17 @@ export class Pool { amountSpecified: JSBI, sqrtPriceLimitX96?: JSBI ): Promise<{ amountCalculated: JSBI; sqrtRatioX96: JSBI; liquidity: JSBI; tickCurrent: number }> { - if (!sqrtPriceLimitX96) - sqrtPriceLimitX96 = zeroForOne - ? JSBI.add(TickMath.MIN_SQRT_RATIO, ONE) - : JSBI.subtract(TickMath.MAX_SQRT_RATIO, ONE) - - if (zeroForOne) { - invariant(JSBI.greaterThan(sqrtPriceLimitX96, TickMath.MIN_SQRT_RATIO), 'RATIO_MIN') - invariant(JSBI.lessThan(sqrtPriceLimitX96, this.sqrtRatioX96), 'RATIO_CURRENT') - } else { - invariant(JSBI.lessThan(sqrtPriceLimitX96, TickMath.MAX_SQRT_RATIO), 'RATIO_MAX') - invariant(JSBI.greaterThan(sqrtPriceLimitX96, this.sqrtRatioX96), 'RATIO_CURRENT') - } - - const exactInput = JSBI.greaterThanOrEqual(amountSpecified, ZERO) - - // keep track of swap state - - const state = { - amountSpecifiedRemaining: amountSpecified, - amountCalculated: ZERO, - sqrtPriceX96: this.sqrtRatioX96, - tick: this.tickCurrent, - liquidity: this.liquidity, - } - - // start swap while loop - while (JSBI.notEqual(state.amountSpecifiedRemaining, ZERO) && state.sqrtPriceX96 !== sqrtPriceLimitX96) { - let step: Partial = {} - step.sqrtPriceStartX96 = state.sqrtPriceX96 - - // because each iteration of the while loop rounds, we can't optimize this code (relative to the smart contract) - // by simply traversing to the next available tick, we instead need to exactly replicate - // tickBitmap.nextInitializedTickWithinOneWord - ;[step.tickNext, step.initialized] = await this.tickDataProvider.nextInitializedTickWithinOneWord( - state.tick, - zeroForOne, - this.tickSpacing - ) - - if (step.tickNext < TickMath.MIN_TICK) { - step.tickNext = TickMath.MIN_TICK - } else if (step.tickNext > TickMath.MAX_TICK) { - step.tickNext = TickMath.MAX_TICK - } - - step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext) - ;[state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount] = SwapMath.computeSwapStep( - state.sqrtPriceX96, - ( - zeroForOne - ? JSBI.lessThan(step.sqrtPriceNextX96, sqrtPriceLimitX96) - : JSBI.greaterThan(step.sqrtPriceNextX96, sqrtPriceLimitX96) - ) - ? sqrtPriceLimitX96 - : step.sqrtPriceNextX96, - state.liquidity, - state.amountSpecifiedRemaining, - this.fee - ) - - if (exactInput) { - state.amountSpecifiedRemaining = JSBI.subtract( - state.amountSpecifiedRemaining, - JSBI.add(step.amountIn, step.feeAmount) - ) - state.amountCalculated = JSBI.subtract(state.amountCalculated, step.amountOut) - } else { - state.amountSpecifiedRemaining = JSBI.add(state.amountSpecifiedRemaining, step.amountOut) - state.amountCalculated = JSBI.add(state.amountCalculated, JSBI.add(step.amountIn, step.feeAmount)) - } - - // TODO - if (JSBI.equal(state.sqrtPriceX96, step.sqrtPriceNextX96)) { - // if the tick is initialized, run the tick transition - if (step.initialized) { - let liquidityNet = JSBI.BigInt((await this.tickDataProvider.getTick(step.tickNext)).liquidityNet) - // if we're moving leftward, we interpret liquidityNet as the opposite sign - // safe because liquidityNet cannot be type(int128).min - if (zeroForOne) liquidityNet = JSBI.multiply(liquidityNet, NEGATIVE_ONE) - - state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet) - } - - state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext - } else if (JSBI.notEqual(state.sqrtPriceX96, step.sqrtPriceStartX96)) { - // updated comparison function - // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved - state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96) - } - } - - return { - amountCalculated: state.amountCalculated, - sqrtRatioX96: state.sqrtPriceX96, - liquidity: state.liquidity, - tickCurrent: state.tick, - } + return v3Swap( + JSBI.BigInt(this.fee), + this.sqrtRatioX96, + this.tickCurrent, + this.liquidity, + this.tickSpacing, + this.tickDataProvider, + zeroForOne, + amountSpecified, + sqrtPriceLimitX96 + ) } public get tickSpacing(): number { diff --git a/sdks/v3-sdk/src/utils/index.ts b/sdks/v3-sdk/src/utils/index.ts index e3b7724e4..97775d07f 100644 --- a/sdks/v3-sdk/src/utils/index.ts +++ b/sdks/v3-sdk/src/utils/index.ts @@ -11,6 +11,7 @@ export * from './nearestUsableTick' export * from './position' export * from './priceTickConversions' export * from './sqrtPriceMath' +export * from './v3swap' export * from './swapMath' export * from './tickLibrary' export * from './tickList' diff --git a/sdks/v3-sdk/src/utils/swapMath.ts b/sdks/v3-sdk/src/utils/swapMath.ts index 8189ee171..54f72f162 100644 --- a/sdks/v3-sdk/src/utils/swapMath.ts +++ b/sdks/v3-sdk/src/utils/swapMath.ts @@ -17,7 +17,7 @@ export abstract class SwapMath { sqrtRatioTargetX96: JSBI, liquidity: JSBI, amountRemaining: JSBI, - feePips: FeeAmount + feePips: JSBI | FeeAmount ): [JSBI, JSBI, JSBI, JSBI] { const returnValues: Partial<{ sqrtRatioNextX96: JSBI @@ -26,12 +26,13 @@ export abstract class SwapMath { feeAmount: JSBI }> = {} + feePips = JSBI.BigInt(feePips) const zeroForOne = JSBI.greaterThanOrEqual(sqrtRatioCurrentX96, sqrtRatioTargetX96) const exactIn = JSBI.greaterThanOrEqual(amountRemaining, ZERO) if (exactIn) { const amountRemainingLessFee = JSBI.divide( - JSBI.multiply(amountRemaining, JSBI.subtract(MAX_FEE, JSBI.BigInt(feePips))), + JSBI.multiply(amountRemaining, JSBI.subtract(MAX_FEE, feePips)), MAX_FEE ) returnValues.amountIn = zeroForOne @@ -95,8 +96,8 @@ export abstract class SwapMath { } else { returnValues.feeAmount = FullMath.mulDivRoundingUp( returnValues.amountIn!, - JSBI.BigInt(feePips), - JSBI.subtract(MAX_FEE, JSBI.BigInt(feePips)) + feePips, + JSBI.subtract(MAX_FEE, feePips) ) } diff --git a/sdks/v3-sdk/src/utils/v3swap.ts b/sdks/v3-sdk/src/utils/v3swap.ts new file mode 100644 index 000000000..16a46d7f7 --- /dev/null +++ b/sdks/v3-sdk/src/utils/v3swap.ts @@ -0,0 +1,127 @@ +import { SwapMath } from './swapMath' +import { LiquidityMath } from './liquidityMath' +import JSBI from 'jsbi' +import invariant from 'tiny-invariant' +import { TickMath } from './tickMath' +import { NEGATIVE_ONE, ONE, ZERO } from '../internalConstants' +import { TickDataProvider } from '../entities/tickDataProvider' + +interface StepComputations { + sqrtPriceStartX96: JSBI + tickNext: number + initialized: boolean + sqrtPriceNextX96: JSBI + amountIn: JSBI + amountOut: JSBI + feeAmount: JSBI +} + +export async function v3Swap( + fee: JSBI, + sqrtRatioX96: JSBI, + tickCurrent: number, + liquidity: JSBI, + tickSpacing: number, + tickDataProvider: TickDataProvider, + zeroForOne: boolean, + amountSpecified: JSBI, + sqrtPriceLimitX96?: JSBI +): Promise<{ amountCalculated: JSBI; sqrtRatioX96: JSBI; liquidity: JSBI; tickCurrent: number }> { + if (!sqrtPriceLimitX96) + sqrtPriceLimitX96 = zeroForOne + ? JSBI.add(TickMath.MIN_SQRT_RATIO, ONE) + : JSBI.subtract(TickMath.MAX_SQRT_RATIO, ONE) + + if (zeroForOne) { + invariant(JSBI.greaterThan(sqrtPriceLimitX96, TickMath.MIN_SQRT_RATIO), 'RATIO_MIN') + invariant(JSBI.lessThan(sqrtPriceLimitX96, sqrtRatioX96), 'RATIO_CURRENT') + } else { + invariant(JSBI.lessThan(sqrtPriceLimitX96, TickMath.MAX_SQRT_RATIO), 'RATIO_MAX') + invariant(JSBI.greaterThan(sqrtPriceLimitX96, sqrtRatioX96), 'RATIO_CURRENT') + } + + const exactInput = JSBI.greaterThanOrEqual(amountSpecified, ZERO) + + // keep track of swap state + + const state = { + amountSpecifiedRemaining: amountSpecified, + amountCalculated: ZERO, + sqrtPriceX96: sqrtRatioX96, + tick: tickCurrent, + liquidity: liquidity, + } + + // start swap while loop + while (JSBI.notEqual(state.amountSpecifiedRemaining, ZERO) && state.sqrtPriceX96 !== sqrtPriceLimitX96) { + let step: Partial = {} + step.sqrtPriceStartX96 = state.sqrtPriceX96 + + // because each iteration of the while loop rounds, we can't optimize this code (relative to the smart contract) + // by simply traversing to the next available tick, we instead need to exactly replicate + // tickBitmap.nextInitializedTickWithinOneWord + ;[step.tickNext, step.initialized] = await tickDataProvider.nextInitializedTickWithinOneWord( + state.tick, + zeroForOne, + tickSpacing + ) + + if (step.tickNext < TickMath.MIN_TICK) { + step.tickNext = TickMath.MIN_TICK + } else if (step.tickNext > TickMath.MAX_TICK) { + step.tickNext = TickMath.MAX_TICK + } + + step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext) + ;[state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount] = SwapMath.computeSwapStep( + state.sqrtPriceX96, + ( + zeroForOne + ? JSBI.lessThan(step.sqrtPriceNextX96, sqrtPriceLimitX96) + : JSBI.greaterThan(step.sqrtPriceNextX96, sqrtPriceLimitX96) + ) + ? sqrtPriceLimitX96 + : step.sqrtPriceNextX96, + state.liquidity, + state.amountSpecifiedRemaining, + fee + ) + + if (exactInput) { + state.amountSpecifiedRemaining = JSBI.subtract( + state.amountSpecifiedRemaining, + JSBI.add(step.amountIn, step.feeAmount) + ) + state.amountCalculated = JSBI.subtract(state.amountCalculated, step.amountOut) + } else { + state.amountSpecifiedRemaining = JSBI.add(state.amountSpecifiedRemaining, step.amountOut) + state.amountCalculated = JSBI.add(state.amountCalculated, JSBI.add(step.amountIn, step.feeAmount)) + } + + // TODO + if (JSBI.equal(state.sqrtPriceX96, step.sqrtPriceNextX96)) { + // if the tick is initialized, run the tick transition + if (step.initialized) { + let liquidityNet = JSBI.BigInt((await tickDataProvider.getTick(step.tickNext)).liquidityNet) + // if we're moving leftward, we interpret liquidityNet as the opposite sign + // safe because liquidityNet cannot be type(int128).min + if (zeroForOne) liquidityNet = JSBI.multiply(liquidityNet, NEGATIVE_ONE) + + state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet) + } + + state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext + } else if (JSBI.notEqual(state.sqrtPriceX96, step.sqrtPriceStartX96)) { + // updated comparison function + // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved + state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96) + } + } + + return { + amountCalculated: state.amountCalculated, + sqrtRatioX96: state.sqrtPriceX96, + liquidity: state.liquidity, + tickCurrent: state.tick, + } +}