Skip to content

Commit

Permalink
feat(v3-sdk): export v3Swap logic (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
ewilz authored May 30, 2024
1 parent f028944 commit 1bb5b88
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 115 deletions.
2 changes: 1 addition & 1 deletion sdks/v2-sdk/src/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
123 changes: 13 additions & 110 deletions sdks/v3-sdk/src/entities/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<StepComputations> = {}
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 {
Expand Down
1 change: 1 addition & 0 deletions sdks/v3-sdk/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
9 changes: 5 additions & 4 deletions sdks/v3-sdk/src/utils/swapMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
)
}

Expand Down
127 changes: 127 additions & 0 deletions sdks/v3-sdk/src/utils/v3swap.ts
Original file line number Diff line number Diff line change
@@ -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<StepComputations> = {}
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,
}
}

0 comments on commit 1bb5b88

Please sign in to comment.