Skip to content

Commit

Permalink
feat(v4-sdk): support removeCallParameters (#102)
Browse files Browse the repository at this point in the history
Co-authored-by: Eric Zhong <[email protected]>
Co-authored-by: Siyu Jiang (See-You John) <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent db85bdd commit 9d3e8bc
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 14 deletions.
81 changes: 73 additions & 8 deletions sdks/v4-sdk/src/PositionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ import { Interface } from '@ethersproject/abi'
import { PoolKey } from './entities'
import { Multicall } from './multicall'
import invariant from 'tiny-invariant'
import { NO_NATIVE, PositionFunctions, ZERO } from './internalConstants'
import {
EMPTY_BYTES,
CANNOT_BURN,
NO_NATIVE,
NO_SQRT_PRICE,
ONE,
PositionFunctions,
ZERO,
ZERO_LIQUIDITY,
} from './internalConstants'
import { V4PositionPlanner } from './utils'
import { abi } from './utils/abi'

Expand Down Expand Up @@ -96,11 +105,6 @@ export interface RemoveLiquiditySpecificOptions {
* The optional permit of the token ID being exited, in case the exit transaction is being sent by an account that does not own the NFT
*/
permit?: NFTPermitOptions

/**
* Parameters to be passed on to collect
*/
collectOptions: Omit<CollectSpecificOptions, 'tokenId'>
}

export interface CollectSpecificOptions {
Expand Down Expand Up @@ -165,7 +169,7 @@ function isMint(options: AddLiquidityOptions): options is MintOptions {

function shouldCreatePool(options: MintOptions): boolean {
if (options.createPool) {
invariant(options.sqrtPriceX96 !== undefined, 'NO_SQRT_PRICE')
invariant(options.sqrtPriceX96 !== undefined, NO_SQRT_PRICE)
return true
}
return false
Expand All @@ -189,6 +193,7 @@ export abstract class V4PositionManager {
}
}

// TODO: Add Support for permit2 batch forwarding
public static addCallParameters(position: Position, options: AddLiquidityOptions): MethodParameters {
/**
* Cases:
Expand All @@ -197,7 +202,7 @@ export abstract class V4PositionManager {
* - if is mint, encode MINT_POSITION. If it is on a NATIVE pool, encode a SWEEP. Finally encode a SETTLE_PAIR
* - else, encode INCREASE_LIQUIDITY. If it is on a NATIVE pool, encode a SWEEP. Finally encode a SETTLE_PAIR
*/
invariant(JSBI.greaterThan(position.liquidity, ZERO), 'ZERO_LIQUIDITY')
invariant(JSBI.greaterThan(position.liquidity, ZERO), ZERO_LIQUIDITY)

const calldataList: string[] = []
const planner = new V4PositionPlanner()
Expand Down Expand Up @@ -257,6 +262,66 @@ export abstract class V4PositionManager {
}
}

/**
* Produces the calldata for completely or partially exiting a position
* @param position The position to exit
* @param options Additional information necessary for generating the calldata
* @returns The call parameters
*/
public static removeCallParameters(position: Position, options: RemoveLiquidityOptions): MethodParameters {
/**
* cases:
* - if liquidityPercentage is 100%, encode BURN_POSITION and then TAKE_PAIR
* - else, encode DECREASE_LIQUIDITY and then TAKE_PAIR
*/
const calldataList: string[] = []
const planner = new V4PositionPlanner()

const tokenId = toHex(options.tokenId)

if (options.burnToken) {
// if burnToken is true, the specified liquidity percentage must be 100%
invariant(options.liquidityPercentage.equalTo(ONE), CANNOT_BURN)

// slippage-adjusted amounts derived from current position liquidity
const { amount0: amount0Min, amount1: amount1Min } = position.burnAmountsWithSlippage(options.slippageTolerance)
planner.addBurn(tokenId, amount0Min, amount1Min, options.hookData)
} else {
// construct a partial position with a percentage of liquidity
const partialPosition = new Position({
pool: position.pool,
liquidity: options.liquidityPercentage.multiply(position.liquidity).quotient,
tickLower: position.tickLower,
tickUpper: position.tickUpper,
})

// If the partial position has liquidity=0, this is a collect call and collectCallParameters should be used
invariant(JSBI.greaterThan(partialPosition.liquidity, ZERO), ZERO_LIQUIDITY)

// slippage-adjusted underlying amounts
const { amount0: amount0Min, amount1: amount1Min } = partialPosition.burnAmountsWithSlippage(
options.slippageTolerance
)

planner.addDecrease(
tokenId,
partialPosition.liquidity.toString(),
amount0Min.toString(),
amount1Min.toString(),
options.hookData ?? EMPTY_BYTES
)
}

planner.addTakePair(position.pool.currency0, position.pool.currency1, MSG_SENDER)

calldataList.push(V4PositionManager.encodeModifyLiquidities(planner.finalize(), options.deadline))

return {
calldata: Multicall.encodeMulticall(calldataList),
value: toHex(0),
}
}

// Initialize a pool
private static encodeInitializePool(poolKey: PoolKey, sqrtPriceX96: BigintIsh, hookData?: string): string {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.INITIALIZE_POOL, [
Expand Down
8 changes: 4 additions & 4 deletions sdks/v4-sdk/src/entities/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ export class Position {

// construct counterfactual pools
const poolLower = new Pool(
this.pool.token0,
this.pool.token1,
this.pool.currency0,
this.pool.currency1,
this.pool.fee,
this.pool.tickSpacing,
this.pool.hooks,
Expand All @@ -235,8 +235,8 @@ export class Position {
TickMath.getTickAtSqrtRatio(sqrtRatioX96Lower)
)
const poolUpper = new Pool(
this.pool.token0,
this.pool.token1,
this.pool.currency0,
this.pool.currency1,
this.pool.fee,
this.pool.tickSpacing,
this.pool.hooks,
Expand Down
3 changes: 3 additions & 0 deletions sdks/v4-sdk/src/internalConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export const EMPTY_HOOK = '0x0000000000000000000000000000000000000000'

// error constants
export const NO_NATIVE = 'NO_NATIVE'
export const ZERO_LIQUIDITY = 'ZERO_LIQUIDITY'
export const NO_SQRT_PRICE = 'NO_SQRT_PRICE'
export const CANNOT_BURN = 'CANNOT_BURN'

/**
* Function fragments that exist on the PositionManager contract.
Expand Down
114 changes: 112 additions & 2 deletions sdks/v4-sdk/src/posm.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { Ether, Percent, Token } from '@uniswap/sdk-core'
import { EMPTY_BYTES, EMPTY_HOOK, FeeAmount, NO_NATIVE, SQRT_PRICE_1_1, TICK_SPACINGS } from './internalConstants'
import {
EMPTY_BYTES,
EMPTY_HOOK,
FeeAmount,
CANNOT_BURN,
NO_NATIVE,
SQRT_PRICE_1_1,
TICK_SPACINGS,
ZERO_LIQUIDITY,
} from './internalConstants'
import { Pool } from './entities/pool'
import { Position } from './entities/position'
import { V4PositionManager } from './PositionManager'
import { RemoveLiquidityOptions, V4PositionManager } from './PositionManager'
import { Multicall } from './multicall'
import { Actions, toHex, V4Planner } from './utils'
import { PoolKey } from './entities/pool'
import { toAddress } from './utils/currencyMap'
import { MSG_SENDER } from './actionConstants'
import { V4PositionPlanner } from './utils'

describe('POSM', () => {
const currency0 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 't0', 'currency0')
Expand Down Expand Up @@ -265,4 +275,104 @@ describe('POSM', () => {
expect(value).toEqual(toHex(amount0Max))
})
})

describe('#removeCallParameters', () => {
const position = new Position({
pool: pool_0_1,
tickLower: -TICK_SPACINGS[FeeAmount.MEDIUM],
tickUpper: TICK_SPACINGS[FeeAmount.MEDIUM],
liquidity: 100,
})

const removeLiqOptions: RemoveLiquidityOptions = {
tokenId,
liquidityPercentage: new Percent(1),
slippageTolerance,
deadline,
}

const partialRemoveOptions: RemoveLiquidityOptions = {
tokenId,
liquidityPercentage: new Percent(1, 100),
slippageTolerance,
deadline,
}

const burnLiqOptions: RemoveLiquidityOptions = {
burnToken: true,
...removeLiqOptions,
}

it('throws for 0 liquidity', () => {
const zeroLiquidityPosition = new Position({
...position,
liquidity: 0,
})

expect(() => V4PositionManager.removeCallParameters(zeroLiquidityPosition, removeLiqOptions)).toThrow(
ZERO_LIQUIDITY
)
})

it('throws when burn is true but liquidityPercentage is not 100%', () => {
const fullLiquidityPosition = new Position({
...position,
liquidity: 999,
})

let invalidBurnLiqOptions = {
burnToken: true,
liquidityPercentage: new Percent(1, 100),
tokenId,
slippageTolerance,
deadline,
}

expect(() => V4PositionManager.removeCallParameters(fullLiquidityPosition, invalidBurnLiqOptions)).toThrow(
CANNOT_BURN
)
})

it('succeeds for burn', () => {
const { calldata, value } = V4PositionManager.removeCallParameters(position, burnLiqOptions)

const { amount0: amount0Min, amount1: amount1Min } = position.burnAmountsWithSlippage(slippageTolerance)

const planner = new V4PositionPlanner()

planner.addAction(Actions.BURN_POSITION, [
tokenId.toString(),
amount0Min.toString(),
amount1Min.toString(),
EMPTY_BYTES,
])
planner.addAction(Actions.TAKE_PAIR, [toAddress(currency0), toAddress(currency1), MSG_SENDER])

expect(calldata).toEqual(V4PositionManager.encodeModifyLiquidities(planner.finalize(), burnLiqOptions.deadline))
expect(value).toEqual('0x00')
})

it('succeeds for remove partial liquidity', () => {
// remove 1% of 100, 1
let amountToRemove = '1'
const { calldata, value } = V4PositionManager.removeCallParameters(position, partialRemoveOptions)
const { amount0: amount0Min, amount1: amount1Min } = position.burnAmountsWithSlippage(slippageTolerance)

const planner = new V4Planner()

planner.addAction(Actions.DECREASE_LIQUIDITY, [
tokenId.toString(),
amountToRemove,
amount0Min.toString(),
amount1Min.toString(),
EMPTY_BYTES,
])
planner.addAction(Actions.TAKE_PAIR, [toAddress(currency0), toAddress(currency1), MSG_SENDER])

expect(calldata).toEqual(
V4PositionManager.encodeModifyLiquidities(planner.finalize(), partialRemoveOptions.deadline)
)
expect(value).toEqual('0x00')
})
})
})

0 comments on commit 9d3e8bc

Please sign in to comment.