Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v4-sdk): add permit2 forwarder support and other permit funcs #104

Merged
merged 39 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
352c198
save state
zhongeric Sep 5, 2024
9dfa044
fix buikld issues and flesh out most of posm
zhongeric Sep 5, 2024
eae746b
fix posm tests
zhongeric Sep 6, 2024
054af72
fix lint
zhongeric Sep 6, 2024
6cd416f
re-order
zhongeric Sep 6, 2024
e3fdf5e
Merge branch 'main' into zhongeric/v4-sdk/actions-router
zhongeric Sep 6, 2024
371774d
Merge branch 'main' into zhongeric/v4-sdk/actions-router
zhongeric Sep 12, 2024
4226aeb
remove all unused files for imports
zhongeric Sep 12, 2024
f4e0125
Update with latest changes from main
zhongeric Sep 12, 2024
75adb9d
feat(sdk-core): add mixed quoter v2 property (#95)
jsy1218 Sep 13, 2024
598988f
feat(router-sdk): support mixed route with v4 route (#94)
jsy1218 Sep 13, 2024
b14cc85
fix linter
snreynolds Sep 16, 2024
32ac636
add test for createCallParameters
snreynolds Sep 16, 2024
cd55856
Merge branch 'main' of https://github.com/Uniswap/sdks into sra/posit…
snreynolds Sep 16, 2024
67269a5
fix: linter
snreynolds Sep 16, 2024
ec8b861
checkpoint, addCallParameters, tests not passing
snreynolds Sep 17, 2024
bd6dbd7
fix addCallParameter tests, add new tests, fix sweeping
snreynolds Sep 18, 2024
9fa445a
Merge branch 'main' into sra/position-createCallParameters
snreynolds Sep 18, 2024
47d4a36
do not use wrapped
snreynolds Sep 18, 2024
b6a79dc
Merge branch 'main' into sra/position-createCallParameters
snreynolds Sep 19, 2024
bb125cc
throw no sqrtPrice is createPool is set
snreynolds Sep 19, 2024
22f064c
rename to calldataList & run linter
snreynolds Sep 19, 2024
f8c8fcf
add constants
snreynolds Sep 19, 2024
46e9fe2
use not symbol
snreynolds Sep 19, 2024
bf00dbb
change back v3 sdk
snreynolds Sep 19, 2024
48afa5e
merge main, fix conf
snreynolds Sep 20, 2024
894238d
add removeCallParameters
snreynolds Sep 20, 2024
0742142
Add forwarder permit and permitBatch support
zhongeric Sep 23, 2024
66a8a2e
lint fix
zhongeric Sep 23, 2024
758b9d4
Add nft permit to removeCallparams
zhongeric Sep 23, 2024
3adf11e
Add burn with permit test
zhongeric Sep 23, 2024
d78c790
Add ERC2612 permit helper
zhongeric Sep 23, 2024
6ce9732
Merge branch 'main' into zhongeric/v4-sdk/add-permit-forwarder
zhongeric Sep 23, 2024
1f62485
remove posm.ts
zhongeric Sep 23, 2024
d5dac92
Merge branch 'main' into zhongeric/v4-sdk/add-permit-forwarder
zhongeric Sep 23, 2024
34ee8de
remove unused permit funcs
zhongeric Sep 23, 2024
abf098e
Merge branch 'main' into zhongeric/v4-sdk/add-permit-forwarder
zhongeric Sep 23, 2024
05a73cd
remove 2612 permit
zhongeric Sep 25, 2024
d25eacc
fix test
zhongeric Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
zhongeric marked this conversation as resolved.
Show resolved Hide resolved
} 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
)
)
}
Comment on lines +227 to +235
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we don't need to support permit2 permit for either token0 OR token1. We can just do a batchPermit of 1 element in that case, simplifying the options interface

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder how much more gas it is. But yeah we can always do this as a follow-up? Maybe worth opening an issue or something just in case its way more gas

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep just made an issue #107


// 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
Loading