Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
41 changes: 25 additions & 16 deletions sdks/universal-router-sdk/src/swapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import {
NonfungiblePositionManager as V3PositionManager,
RemoveLiquidityOptions as V3RemoveLiquidityOptions,
} from '@uniswap/v3-sdk'
import {
Position as V4Position,
V4PositionManager,
AddLiquidityOptions as V4AddLiquidityOptions,
MintOptions,
Pool as V4Pool,
PoolKey,
} from '@uniswap/v4-sdk'
import { Position as V4Position, V4PositionManager, MigrateOptions, Pool as V4Pool, PoolKey } from '@uniswap/v4-sdk'
import { Trade as RouterTrade } from '@uniswap/router-sdk'
import { Currency, TradeType, Percent, CHAIN_TO_ADDRESSES_MAP, SupportedChainsType } from '@uniswap/sdk-core'
import { UniswapTrade, SwapOptions } from './entities/actions/uniswap'
Expand All @@ -33,10 +26,10 @@ export interface MigrateV3ToV4Options {
inputPosition: V3Position
outputPosition: V4Position
v3RemoveLiquidityOptions: V3RemoveLiquidityOptions
v4AddLiquidityOptions: V4AddLiquidityOptions
migrateOptions: MigrateOptions
}

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

Expand Down Expand Up @@ -99,13 +92,14 @@ export abstract class SwapRouter {
options.v3RemoveLiquidityOptions.collectOptions.recipient === v4PositionManagerAddress,
'RECIPIENT_NOT_POSITION_MANAGER'
)
invariant(isMint(options.v4AddLiquidityOptions), 'MINT_REQUIRED')
invariant(options.v4AddLiquidityOptions.migrate, 'MIGRATE_REQUIRED')
// Migration must be a mint operation, not an increase because the UR should not have permission to increase liquidity on a v4 position
invariant(isMint(options.migrateOptions), 'MINT_REQUIRED')
invariant(options.migrateOptions.migrate, 'MIGRATE_REQUIRED')

const planner = new RoutePlanner()

// to prevent reentrancy by the pool hook, we initialize the v4 pool before moving funds
if (options.v4AddLiquidityOptions.createPool) {
if (options.migrateOptions.createPool) {
const poolKey: PoolKey = V4Pool.getPoolKey(
v4Pool.currency0,
v4Pool.currency1,
Expand All @@ -115,7 +109,7 @@ export abstract class SwapRouter {
)
planner.addCommand(CommandType.V4_INITIALIZE_POOL, [poolKey, v4Pool.sqrtRatioX96.toString()])
// remove createPool setting, so that it doesnt get encoded again later
delete options.v4AddLiquidityOptions.createPool
delete options.migrateOptions.createPool
}

// add position permit to the universal router planner
Expand Down Expand Up @@ -151,16 +145,31 @@ export abstract class SwapRouter {
planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [v3Call])
}

// if migrate options has a currency, require a batch permit
if (options.migrateOptions.additionalTransfer) {
invariant(options.migrateOptions.batchPermit, 'PERMIT_REQUIRED')
planner.addCommand(CommandType.PERMIT2_PERMIT_BATCH, [
options.migrateOptions.batchPermit.permitBatch,
options.migrateOptions.batchPermit.signature,
])
planner.addCommand(CommandType.PERMIT2_TRANSFER_FROM, [
options.migrateOptions.additionalTransfer.neededCurrency,
options.v3RemoveLiquidityOptions.collectOptions.recipient,
options.migrateOptions.additionalTransfer.neededAmount,
])
delete options.migrateOptions.batchPermit
}

// encode v4 mint
const v4AddParams = V4PositionManager.addCallParameters(options.outputPosition, options.v4AddLiquidityOptions)
const v4AddParams = V4PositionManager.addCallParameters(options.outputPosition, options.migrateOptions)
// only modifyLiquidities can be called by the UniversalRouter
const selector = v4AddParams.calldata.slice(0, 10)
invariant(selector == V4PositionManager.INTERFACE.getSighash('modifyLiquidities'), 'INVALID_V4_CALL: ' + selector)

planner.addCommand(CommandType.V4_POSITION_MANAGER_CALL, [v4AddParams.calldata])

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

Expand Down
208 changes: 205 additions & 3 deletions sdks/universal-router-sdk/test/forge/MigratorCallParameters.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18);
// in range v3 position
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 200040, 300000);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// approve the UniversalRouter to access the position (instead of permit)
Expand Down Expand Up @@ -64,7 +65,8 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18);
// in range v3 position
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 200040, 300000);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

assertEq(params.value, 0);
Expand All @@ -89,7 +91,7 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18);
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 200040, 300000);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

assertEq(params.value, 0);
Expand All @@ -107,4 +109,204 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
assertEq(v4PositionManager.balanceOf(RECIPIENT), 1, "V4 NOT MINTED");
}

function test_migrate_outOfRange0_inRange() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_OUT_OF_RANGE0_TO_IN_RANGE");

// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
// one sided v3 position in USDC
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 205320, 300000);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// approve the UniversalRouter to access the position (instead of permit)
vm.startPrank(from);
INonfungiblePositionManager(V3_POSITION_MANAGER).setApprovalForAll(MAINNET_ROUTER, true);
vm.stopPrank();

assertEq(params.value, 0);
WETH.transfer(from, WETH.balanceOf(address(this)));
// approve permit2 to spend WETH
vm.startPrank(from);
WETH.approve(MAINNET_PERMIT2, WETH.balanceOf(from));

(bool success,) = address(router).call(params.data);
require(success, "call failed");

// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);

// old position burned, new position minted
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
assertEq(v4PositionManager.balanceOf(RECIPIENT), 1, "V4 NOT MINTED");
}

function test_migrate_outOfRange0_outOfRange1() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_OUT_OF_RANGE0_TO_OUT_OF_RANGE1");

// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
// one sided v3 position in USDC
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 205320, 300000);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// approve the UniversalRouter to access the position (instead of permit)
vm.startPrank(from);
INonfungiblePositionManager(V3_POSITION_MANAGER).setApprovalForAll(MAINNET_ROUTER, true);
vm.stopPrank();

assertEq(params.value, 0);
WETH.transfer(from, WETH.balanceOf(address(this)));
// approve permit2 to spend WETH
vm.startPrank(from);
WETH.approve(MAINNET_PERMIT2, WETH.balanceOf(from));

(bool success,) = address(router).call(params.data);
require(success, "call failed");

// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);

// old position burned, new position minted
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
assertEq(v4PositionManager.balanceOf(RECIPIENT), 1, "V4 NOT MINTED");
}

function test_migrate_outOfRange0_outOfRange0() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_OUT_OF_RANGE0_TO_OUT_OF_RANGE0");

// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
// one sided v3 position in USDC
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 205320, 300000);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// approve the UniversalRouter to access the position (instead of permit)
vm.startPrank(from);
INonfungiblePositionManager(V3_POSITION_MANAGER).setApprovalForAll(MAINNET_ROUTER, true);

assertEq(params.value, 0);

(bool success,) = address(router).call(params.data);
require(success, "call failed");

// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);

// old position burned, new position minted
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
assertEq(v4PositionManager.balanceOf(RECIPIENT), 1, "V4 NOT MINTED");
}

function test_migrate_outOfRange1_inRange() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_OUT_OF_RANGE1_TO_IN_RANGE");

// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
// one sided v3 position in USDC
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 204720, 204960);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// approve the UniversalRouter to access the position (instead of permit)
vm.startPrank(from);
INonfungiblePositionManager(V3_POSITION_MANAGER).setApprovalForAll(MAINNET_ROUTER, true);
vm.stopPrank();

assertEq(params.value, 0);
USDC.transfer(from, USDC.balanceOf(address(this)));
// approve the universal router on permit2 to spend USDC
vm.startPrank(from);
USDC.approve(MAINNET_PERMIT2, USDC.balanceOf(from));

(bool success,) = address(router).call(params.data);
require(success, "call failed");

// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);

// old position burned, new position minted
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
assertEq(v4PositionManager.balanceOf(RECIPIENT), 1, "V4 NOT MINTED");
}

function test_migrate_outOfRange1_outOfRange0() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_OUT_OF_RANGE1_TO_OUT_OF_RANGE0");

// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
// one sided v3 position in USDC
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 204720, 204960);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// approve the UniversalRouter to access the position (instead of permit)
vm.startPrank(from);
INonfungiblePositionManager(V3_POSITION_MANAGER).setApprovalForAll(MAINNET_ROUTER, true);
vm.stopPrank();

assertEq(params.value, 0);
USDC.transfer(from, USDC.balanceOf(address(this)));
// approve the universal router on permit2 to spend USDC
vm.startPrank(from);
USDC.approve(MAINNET_PERMIT2, USDC.balanceOf(from));

(bool success,) = address(router).call(params.data);
require(success, "call failed");

// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);

// old position burned, new position minted
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
assertEq(v4PositionManager.balanceOf(RECIPIENT), 1, "V4 NOT MINTED");
}

function test_migrate_outOfRange1_outOfRange1() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_OUT_OF_RANGE1_TO_OUT_OF_RANGE1");

// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
// USDC < WETH
// one sided v3 position in USDC
mintV3Position(address(USDC), address(WETH), 3000, 2500e6, 1e18, 204720, 204960);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// approve the UniversalRouter to access the position (instead of permit)
vm.startPrank(from);
INonfungiblePositionManager(V3_POSITION_MANAGER).setApprovalForAll(MAINNET_ROUTER, true);

assertEq(params.value, 0);

(bool success,) = address(router).call(params.data);
require(success, "call failed");

// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);

// old position burned, new position minted
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
assertEq(v4PositionManager.balanceOf(RECIPIENT), 1, "V4 NOT MINTED");
}
}
Loading
Loading