Skip to content

Commit

Permalink
feat(universal-router-sdk): v3 to v4 migrator (#114)
Browse files Browse the repository at this point in the history
Co-authored-by: dianakocsis <[email protected]>
Co-authored-by: Sara Reynolds <[email protected]>
  • Loading branch information
3 people authored Oct 3, 2024
1 parent 3c14aee commit 1372b07
Show file tree
Hide file tree
Showing 6 changed files with 642 additions and 193 deletions.
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
*/
public static migrateV3ToV4CallParameters(options: MigrateV3ToV4Options): MethodParameters {
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

0 comments on commit 1372b07

Please sign in to comment.