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(ur-sdk): migrating out of range -> in range or out of range -> opposite side out of range #277

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
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])
Ayoakala marked this conversation as resolved.
Show resolved Hide resolved
}

// if migrate options has a currency, require a batch permit
dianakocsis marked this conversation as resolved.
Show resolved Hide resolved
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
}
Ayoakala marked this conversation as resolved.
Show resolved Hide resolved

// 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
Ayoakala marked this conversation as resolved.
Show resolved Hide resolved
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)));
Ayoakala marked this conversation as resolved.
Show resolved Hide resolved
// approve permit2 to spend WETH
vm.startPrank(from);
Ayoakala marked this conversation as resolved.
Show resolved Hide resolved
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);
Ayoakala marked this conversation as resolved.
Show resolved Hide resolved

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