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(universal-router-sdk): v3 to v4 migrator #114

Merged
merged 25 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
89 changes: 85 additions & 4 deletions sdks/universal-router-sdk/src/swapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,41 @@ import invariant from 'tiny-invariant'
import { abi } from '@uniswap/universal-router/artifacts/contracts/UniversalRouter.sol/UniversalRouter.json'
import { Interface } from '@ethersproject/abi'
import { BigNumber, BigNumberish } from 'ethers'
import { MethodParameters } from '@uniswap/v3-sdk'
import {
MethodParameters,
Position as V3Position,
NonfungiblePositionManager as V3PositionManager,
RemoveLiquidityOptions as V3RemoveLiquidityOptions,
} from '@uniswap/v3-sdk'
import {
Position as V4Position,
V4PositionManager,
AddLiquidityOptions as V4AddLiquidityOptions,
MintOptions,
} from '@uniswap/v4-sdk'
import { Trade as RouterTrade } from '@uniswap/router-sdk'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { Currency, TradeType, Percent, CHAIN_TO_ADDRESSES_MAP, SupportedChainsType } from '@uniswap/sdk-core'
import { UniswapTrade, SwapOptions } from './entities/actions/uniswap'
import { RoutePlanner } from './utils/routerCommands'
import { encodePermit } from './utils/inputTokens'
import { RoutePlanner, CommandType } from './utils/routerCommands'
import { encodePermit, encodeV3PositionPermit } from './utils/inputTokens'
import { UNIVERSAL_ROUTER_ADDRESS, UniversalRouterVersion } from './utils/constants'

export type SwapRouterConfig = {
sender?: string // address
deadline?: BigNumberish
}

export interface MigrateV3ToV4Options {
inputPosition: V3Position
outputPosition: V4Position
v3RemoveLiquidityOptions: V3RemoveLiquidityOptions
v4AddLiquidityOptions: V4AddLiquidityOptions
}

function isMint(options: V4AddLiquidityOptions): options is MintOptions {
return Object.keys(options).some((k) => k === 'recipient')
}

export abstract class SwapRouter {
public static INTERFACE: Interface = new Interface(abi)

Expand Down Expand Up @@ -43,6 +66,64 @@ export abstract class SwapRouter {
})
}

/**
* Builds the call parameters for a migration from a V3 position to a V4 position.
* Some requirements of the parameters:
* - v3RemoveLiquidityOptions.collectOptions.recipient must equal v4PositionManager
* - v3RemoveLiquidityOptions.liquidityPercentage must be 100%
* - input pool and output pool must have the same tokens
* - V3 NFT must be approved, or valid inputV3NFTPermit must be provided with UR as spender
Comment on lines +72 to +75
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

was considering not taking V3RemoveOptions and V4RemoveOptions and rather taking minimal deduplicated params and handling these automatically.. decided for this approach to give more configurability to options for each stage

*/
public static migrateV3ToV4CallParameters(options: MigrateV3ToV4Options): MethodParameters {
Copy link
Contributor

Choose a reason for hiding this comment

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

shoudl it check that the migrate param added to v4-sdk is true?

Copy link
Member

Choose a reason for hiding this comment

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

it should imo +

const token0 = options.inputPosition.pool.token0
const token1 = options.inputPosition.pool.token1
const v4PositionManagerAddress =
CHAIN_TO_ADDRESSES_MAP[options.outputPosition.pool.chainId as SupportedChainsType].v4PositionManagerAddress

invariant(token0 === options.outputPosition.pool.token0, 'TOKEN0_MISMATCH')
invariant(token1 === options.outputPosition.pool.token1, 'TOKEN1_MISMATCH')
invariant(
options.v3RemoveLiquidityOptions.liquidityPercentage.equalTo(new Percent(100, 100)),
'FULL_REMOVAL_REQUIRED'
)
invariant(options.v3RemoveLiquidityOptions.burnToken == true, 'BURN_TOKEN_REQUIRED')
invariant(
options.v3RemoveLiquidityOptions.collectOptions.recipient === v4PositionManagerAddress,
'RECIPIENT_NOT_POSITION_MANAGER'
)

invariant(isMint(options.v4AddLiquidityOptions), 'MINT_REQUIRED')
invariant(options.v4AddLiquidityOptions.migrate, 'MIGRATE_REQUIRED')

const planner = new RoutePlanner()

if (options.v3RemoveLiquidityOptions.permit) {
// permit spender should be UR
const universalRouterAddress = UNIVERSAL_ROUTER_ADDRESS(
UniversalRouterVersion.V2_0,
options.inputPosition.pool.chainId as SupportedChainsType
)
invariant(universalRouterAddress == options.v3RemoveLiquidityOptions.permit.spender, 'INVALID_SPENDER')
// don't need to transfer it because v3posm uses isApprovedOrOwner()
encodeV3PositionPermit(planner, options.v3RemoveLiquidityOptions.permit, options.v3RemoveLiquidityOptions.tokenId)
}

// encode v3 withdraw
const v3RemoveParams = V3PositionManager.removeCallParameters(
options.inputPosition,
options.v3RemoveLiquidityOptions
)
planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [v3RemoveParams.calldata])

// encode v4 mint
const v4AddParams = V4PositionManager.addCallParameters(options.outputPosition, options.v4AddLiquidityOptions)
planner.addCommand(CommandType.V4_POSITION_CALL, [v4AddParams.calldata])

return SwapRouter.encodePlan(planner, BigNumber.from(0), {
deadline: BigNumber.from(options.v4AddLiquidityOptions.deadline),
})
}

/**
* Encodes a planned route into a method name and parameters for the Router contract.
* @param planner the planned route
Expand Down
15 changes: 15 additions & 0 deletions sdks/universal-router-sdk/src/utils/inputTokens.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import invariant from 'tiny-invariant'
import { ethers } from 'ethers'
import { validateAndParseAddress, BigintIsh } from '@uniswap/sdk-core'
import { NFTPermitOptions, NonfungiblePositionManager } from '@uniswap/v3-sdk'
import { PermitSingle } from '@uniswap/permit2-sdk'
import { CommandType, RoutePlanner } from './routerCommands'
import { ROUTER_AS_RECIPIENT } from './constants'
Expand Down Expand Up @@ -40,6 +42,19 @@ export function encodePermit(planner: RoutePlanner, permit2: Permit2Permit): voi
planner.addCommand(CommandType.PERMIT2_PERMIT, [permit2, signature])
}

export function encodeV3PositionPermit(planner: RoutePlanner, permit: NFTPermitOptions, tokenId: BigintIsh): void {
const calldata = NonfungiblePositionManager.INTERFACE.encodeFunctionData('permit', [
validateAndParseAddress(permit.spender),
tokenId,
permit.deadline,
permit.v,
permit.r,
permit.s,
])

planner.addCommand(CommandType.V3_POSITION_MANAGER_PERMIT, [calldata])
}

// Handles the encoding of commands needed to gather input tokens for a trade
// Approval: The router approving another address to take tokens.
// note: Only seaport and sudoswap support this action. Approvals are left open.
Expand Down
Loading
Loading