Skip to content

Commit

Permalink
feat(v4-sdk): add permit2 forwarder support and other permit funcs (#104
Browse files Browse the repository at this point in the history
)

Co-authored-by: Siyu Jiang (See-You John) <[email protected]>
Co-authored-by: Sara Reynolds <[email protected]>
Co-authored-by: Sara Reynolds <[email protected]>
  • Loading branch information
4 people authored Sep 26, 2024
1 parent dd4d598 commit 582a43d
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 15 deletions.
108 changes: 105 additions & 3 deletions sdks/v4-sdk/src/PositionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import {
SQRT_PRICE_1_1,
TICK_SPACINGS,
ZERO_LIQUIDITY,
PositionFunctions,
} from './internalConstants'
import { Pool } from './entities/pool'
import { Position } from './entities/position'
import { CollectOptions, RemoveLiquidityOptions, V4PositionManager } from './PositionManager'
import { BatchPermitOptions, CollectOptions, RemoveLiquidityOptions, V4PositionManager } from './PositionManager'
import { Multicall } from './multicall'
import { Actions, toHex, V4Planner } from './utils'
import { PoolKey } from './entities/pool'
Expand Down Expand Up @@ -43,12 +44,15 @@ describe('PositionManager', () => {
[]
)

const recipient = '0x0000000000000000000000000000000000000003'

const tokenId = 1
const slippageTolerance = new Percent(1, 100)
const deadline = 123

const mockOwner = '0x000000000000000000000000000000000000000a'
const mockSpender = '0x000000000000000000000000000000000000000b'
const recipient = '0x000000000000000000000000000000000000000c'
const mockBytes32 = '0x0000000000000000000000000000000000000000000000000000000000000000'

let planner: V4Planner

beforeEach(() => {
Expand Down Expand Up @@ -274,6 +278,59 @@ describe('PositionManager', () => {

expect(value).toEqual(toHex(amount0Max))
})

it('succeeds for batchPermit', () => {
const position: Position = new Position({
pool: pool_0_1,
tickLower: -TICK_SPACINGS[FeeAmount.MEDIUM],
tickUpper: TICK_SPACINGS[FeeAmount.MEDIUM],
liquidity: 1,
})

const batchPermit: BatchPermitOptions = {
owner: mockOwner,
permitBatch: {
details: [],
sigDeadline: deadline,
spender: mockSpender,
},
signature: mockBytes32,
}

const { calldata, value } = V4PositionManager.addCallParameters(position, {
recipient,
slippageTolerance,
deadline,
batchPermit,
})

const calldataList = Multicall.decodeMulticall(calldata)
// Expect permitBatch to be called correctly
expect(calldataList[0]).toEqual(
V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.PERMIT_BATCH, [
batchPermit.owner,
batchPermit.permitBatch,
batchPermit.signature,
])
)

const planner = new V4Planner()
const { amount0: amount0Max, amount1: amount1Max } = position.mintAmountsWithSlippage(slippageTolerance)

planner.addAction(Actions.MINT_POSITION, [
pool_0_1.poolKey,
-TICK_SPACINGS[FeeAmount.MEDIUM],
TICK_SPACINGS[FeeAmount.MEDIUM],
1,
toHex(amount0Max),
toHex(amount1Max),
recipient,
EMPTY_BYTES,
])
planner.addAction(Actions.SETTLE_PAIR, [toAddress(pool_0_1.currency0), toAddress(pool_0_1.currency1)])
expect(calldataList[1]).toEqual(V4PositionManager.encodeModifyLiquidities(planner.finalize(), deadline))
expect(value).toEqual('0x00')
})
})

describe('#removeCallParameters', () => {
Expand Down Expand Up @@ -303,6 +360,17 @@ describe('PositionManager', () => {
...removeLiqOptions,
}

const burnLiqWithPermitOptions: RemoveLiquidityOptions = {
...burnLiqOptions,
permit: {
spender: mockSpender,
tokenId,
deadline,
nonce: 1,
signature: '0x00',
},
}

it('throws for 0 liquidity', () => {
const zeroLiquidityPosition = new Position({
...position,
Expand Down Expand Up @@ -374,6 +442,40 @@ describe('PositionManager', () => {
)
expect(value).toEqual('0x00')
})

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

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])

// The resulting calldata should be multicall with two calls: ERC721Permit_Permit and modifyLiquidities
const calldataList = Multicall.decodeMulticall(calldata)
// Expect ERC721Permit_Permit to be called correctly
expect(calldataList[0]).toEqual(
V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.ERC721PERMIT_PERMIT, [
burnLiqWithPermitOptions.permit!.spender,
tokenId.toString(),
burnLiqWithPermitOptions.permit!.deadline,
burnLiqWithPermitOptions.permit!.nonce,
burnLiqWithPermitOptions.permit!.signature,
])
)
// Expect modifyLiquidities to be called correctly
expect(calldataList[1]).toEqual(
V4PositionManager.encodeModifyLiquidities(planner.finalize(), burnLiqOptions.deadline)
)
expect(value).toEqual('0x00')
})
})

describe('#collectCallParameters', () => {
Expand Down
91 changes: 81 additions & 10 deletions sdks/v4-sdk/src/PositionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,9 @@ export interface CommonAddLiquidityOptions {
useNative?: NativeCurrency

/**
* The optional permit parameters for spending token0
* The optional permit2 batch permit parameters for spending token0 and token1
*/
token0Permit?: any // TODO: add permit2 permit type here

/**
* The optional permit parameters for spending token1
*/
token1Permit?: any // TODO: add permit2 permit type here
batchPermit?: BatchPermitOptions
}

/**
Expand Down Expand Up @@ -129,11 +124,37 @@ export interface TransferOptions {
tokenId: BigintIsh
}

export interface NFTPermitOptions {
export interface PermitDetails {
token: string
amount: BigintIsh
expiration: BigintIsh
nonce: BigintIsh
}

export interface AllowanceTransferPermitSingle {
details: PermitDetails
spender: string
sigDeadline: BigintIsh
}

export interface AllowanceTransferPermitBatch {
details: PermitDetails[]
spender: string
sigDeadline: BigintIsh
}

export interface BatchPermitOptions {
owner: string
permitBatch: AllowanceTransferPermitBatch
signature: string
deadline: BigintIsh
}

export interface NFTPermitOptions {
spender: string
tokenId: BigintIsh
deadline: BigintIsh
nonce: BigintIsh
signature: string
}

export type MintOptions = CommonOptions & CommonAddLiquidityOptions & MintSpecificOptions
Expand Down Expand Up @@ -176,7 +197,6 @@ export abstract class V4PositionManager {
}
}

// TODO: Add Support for permit2 batch forwarding
public static addCallParameters(position: Position, options: AddLiquidityOptions): MethodParameters {
/**
* Cases:
Expand All @@ -203,6 +223,17 @@ export abstract class V4PositionManager {
const amount0Max = toHex(maximumAmounts.amount0)
const amount1Max = toHex(maximumAmounts.amount1)

// We use permit2 to approve tokens to the position manager
if (options.batchPermit) {
calldataList.push(
V4PositionManager.encodePermitBatch(
options.batchPermit.owner,
options.batchPermit.permitBatch,
options.batchPermit.signature
)
)
}

// mint
if (isMint(options)) {
const recipient: string = validateAndParseAddress(options.recipient)
Expand Down Expand Up @@ -264,6 +295,19 @@ export abstract class V4PositionManager {
// if burnToken is true, the specified liquidity percentage must be 100%
invariant(options.liquidityPercentage.equalTo(ONE), CANNOT_BURN)

// if there is a permit, encode the ERC721Permit permit call
if (options.permit) {
calldataList.push(
V4PositionManager.encodeERC721Permit(
options.permit.spender,
options.permit.tokenId,
options.permit.deadline,
options.permit.nonce,
options.permit.signature
)
)
}

// slippage-adjusted amounts derived from current position liquidity
const { amount0: amount0Min, amount1: amount1Min } = position.burnAmountsWithSlippage(options.slippageTolerance)
planner.addBurn(tokenId, amount0Min, amount1Min, options.hookData)
Expand Down Expand Up @@ -343,7 +387,34 @@ export abstract class V4PositionManager {
])
}

// Encode a modify liquidities call
public static encodeModifyLiquidities(unlockData: string, deadline: BigintIsh): string {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.MODIFY_LIQUIDITIES, [unlockData, deadline])
}

// Encode a permit batch call
public static encodePermitBatch(owner: string, permitBatch: AllowanceTransferPermitBatch, signature: string): string {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.PERMIT_BATCH, [
owner,
permitBatch,
signature,
])
}

// Encode a ERC721Permit permit call
public static encodeERC721Permit(
spender: string,
tokenId: BigintIsh,
deadline: BigintIsh,
nonce: BigintIsh,
signature: string
): string {
return V4PositionManager.INTERFACE.encodeFunctionData(PositionFunctions.ERC721PERMIT_PERMIT, [
spender,
tokenId,
deadline,
nonce,
signature,
])
}
}
35 changes: 35 additions & 0 deletions sdks/v4-sdk/src/entities/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Pool } from './pool'
import { encodeSqrtRatioX96, maxLiquidityForAmounts, SqrtPriceMath, TickMath } from '@uniswap/v3-sdk'
import { ZERO } from '../internalConstants'
import { tickToPrice } from '../utils/priceTickConversions'
import { AllowanceTransferPermitBatch } from '../PositionManager'

interface PositionConstructorArgs {
pool: Pool
Expand Down Expand Up @@ -310,6 +311,40 @@ export class Position {
return this._mintAmounts
}

/**
* Returns the AllowanceTransferPermitBatch for adding liquidity to a position
* @param slippageTolerance The amount by which the price can 'slip' before the transaction will revert
* @param spender The spender of the permit (should usually be the PositionManager)
* @param nonce A valid permit2 nonce
* @param deadline The deadline for the permit
*/
public permitBatchData(
slippageTolerance: Percent,
spender: string,
nonce: BigintIsh,
deadline: BigintIsh
): AllowanceTransferPermitBatch {
const { amount0, amount1 } = this.mintAmountsWithSlippage(slippageTolerance)
return {
details: [
{
token: this.pool.currency0.wrapped.address,
amount: amount0,
expiration: deadline,
nonce: nonce,
},
{
token: this.pool.currency1.wrapped.address,
amount: amount1,
expiration: deadline,
nonce: nonce,
},
],
spender,
sigDeadline: deadline,
}
}

/**
* Computes the maximum amount of liquidity received for a given amount of token0, token1,
* and the prices at the tick boundaries.
Expand Down
4 changes: 4 additions & 0 deletions sdks/v4-sdk/src/internalConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export const CANNOT_BURN = 'CANNOT_BURN'
export enum PositionFunctions {
INITIALIZE_POOL = 'initializePool',
MODIFY_LIQUIDITIES = 'modifyLiquidities',
// Inherited from PermitForwarder
PERMIT_BATCH = '0x002a3e3a', // "permitBatch(address,((address,uint160,uint48,uint48)[],address,uint256),bytes)"
// Inherited from ERC721Permit
ERC721PERMIT_PERMIT = '0x0f5730f1', // "permit(address,uint256,uint256,uint256,bytes)"
}

/**
Expand Down
3 changes: 1 addition & 2 deletions sdks/v4-sdk/src/utils/v4PositionPlanner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { V4Planner } from './v4Planner'
import { Actions, V4Planner } from './v4Planner'
import { Pool } from '../entities'
import { Actions } from '../utils'
import { BigintIsh, Currency } from '@uniswap/sdk-core'
import { toAddress } from '../utils/currencyMap'
import { EMPTY_BYTES } from '../internalConstants'
Expand Down

0 comments on commit 582a43d

Please sign in to comment.