Skip to content

Commit

Permalink
feat(ur-sdk): allow migrating to eth position (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
dianakocsis authored Feb 4, 2025
1 parent 17c345d commit 13a909c
Show file tree
Hide file tree
Showing 6 changed files with 508 additions and 51 deletions.
2 changes: 1 addition & 1 deletion sdks/universal-router-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@uniswap/v2-sdk": "^4.13.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-sdk": "^3.24.0",
"@uniswap/v4-sdk": "^1.18.1",
"@uniswap/v4-sdk": "^1.19.2",
"bignumber.js": "^9.0.2",
"ethers": "^5.7.0"
},
Expand Down
19 changes: 15 additions & 4 deletions sdks/universal-router-sdk/src/swapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,25 @@ export abstract class SwapRouter {
positionManagerOverride?: string
): MethodParameters {
const v4Pool: V4Pool = options.outputPosition.pool
const token0 = options.inputPosition.pool.token0
const token1 = options.inputPosition.pool.token1
const v3Token0 = options.inputPosition.pool.token0
const v3Token1 = options.inputPosition.pool.token1
const v4PositionManagerAddress =
positionManagerOverride ?? CHAIN_TO_ADDRESSES_MAP[v4Pool.chainId as SupportedChainsType].v4PositionManagerAddress

// owner of the v3 nft must be the receiver of the v4 nft

// validate the parameters
invariant(token0 === v4Pool.token0, 'TOKEN0_MISMATCH')
invariant(token1 === v4Pool.token1, 'TOKEN1_MISMATCH')
if (v4Pool.currency0.isNative) {
invariant(
(v4Pool.currency0.wrapped.equals(v3Token0) && v4Pool.currency1.equals(v3Token1)) ||
(v4Pool.currency0.wrapped.equals(v3Token1) && v4Pool.currency1.equals(v3Token0)),
'TOKEN_MISMATCH'
)
} else {
invariant(v3Token0 === v4Pool.token0, 'TOKEN0_MISMATCH')
invariant(v3Token1 === v4Pool.token1, 'TOKEN1_MISMATCH')
}

invariant(
options.v3RemoveLiquidityOptions.liquidityPercentage.equalTo(new Percent(100, 100)),
'FULL_REMOVAL_REQUIRED'
Expand Down
223 changes: 217 additions & 6 deletions sdks/universal-router-sdk/test/forge/MigratorCallParameters.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
vm.deal(from, BALANCE);
}

function test_migrate_withoutPermit() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_WITHOUT_PERMIT");
function test_migrate_toEth_withoutPermit() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_TO_ETH_WITHOUT_PERMIT");

// add the position to v3 so we have something to migrate
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0);
Expand All @@ -42,6 +42,16 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
vm.prank(from);
INonfungiblePositionManager(V3_POSITION_MANAGER).setApprovalForAll(MAINNET_ROUTER, true);

// pool manager balance before
uint256 ethBalanceBefore = address(poolManager).balance;
uint256 usdcBalanceBefore = USDC.balanceOf(address(poolManager));
uint256 wethBalanceBefore = WETH.balanceOf(address(poolManager));

// recipient balance before
uint256 recipientBalanceBefore = address(RECIPIENT).balance;
uint256 recipientUSDCBalanceBefore = USDC.balanceOf(RECIPIENT);
uint256 recipientWETHBalanceBefore = WETH.balanceOf(RECIPIENT);

assertEq(params.value, 0);
vm.prank(from);
(bool success,) = address(router).call(params.data);
Expand All @@ -50,23 +60,49 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(address(MAINNET_ROUTER).balance, 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);
assertEq(address(v4PositionManager).balance, 0);

// pool manager balance after, eth and usdc deposited
assertGt(address(poolManager).balance, ethBalanceBefore);
assertGt(USDC.balanceOf(address(poolManager)), usdcBalanceBefore);
assertEq(WETH.balanceOf(address(poolManager)), wethBalanceBefore);

// recipient balance after
assertEq(address(RECIPIENT).balance, recipientBalanceBefore);
assertGe(USDC.balanceOf(RECIPIENT), recipientUSDCBalanceBefore);
assertGe(WETH.balanceOf(RECIPIENT), recipientWETHBalanceBefore);

// 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_withPermit() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_WITH_PERMIT");
function test_migrate_toErc20_withoutPermit() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_TO_ERC20_WITHOUT_PERMIT");

// 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);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

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

// pool manager balance before
uint256 ethBalanceBefore = address(poolManager).balance;
uint256 usdcBalanceBefore = USDC.balanceOf(address(poolManager));
uint256 wethBalanceBefore = WETH.balanceOf(address(poolManager));

// recipient balance before
uint256 recipientBalanceBefore = address(RECIPIENT).balance;
uint256 recipientUSDCBalanceBefore = USDC.balanceOf(RECIPIENT);
uint256 recipientWETHBalanceBefore = WETH.balanceOf(RECIPIENT);

assertEq(params.value, 0);
vm.prank(from);
(bool success,) = address(router).call(params.data);
Expand All @@ -75,23 +111,45 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(address(MAINNET_ROUTER).balance, 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);
assertEq(address(v4PositionManager).balance, 0);

// pool manager balance after, weth and usdc deposited
assertEq(address(poolManager).balance, ethBalanceBefore);
assertGt(USDC.balanceOf(address(poolManager)), usdcBalanceBefore);
assertGt(WETH.balanceOf(address(poolManager)), wethBalanceBefore);

// recipient balance after
assertEq(address(RECIPIENT).balance, recipientBalanceBefore);
assertGe(USDC.balanceOf(RECIPIENT), recipientUSDCBalanceBefore);
assertGe(WETH.balanceOf(RECIPIENT), recipientWETHBalanceBefore);

// 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_withPermitAndPoolInitialize() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_WITH_PERMIT_AND_POOL_INITIALIZE");
function test_migrate_toEth_withPermit() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_TO_ETH_WITH_PERMIT");

// 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);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// pool manager balance before
uint256 ethBalanceBefore = address(poolManager).balance;
uint256 usdcBalanceBefore = USDC.balanceOf(address(poolManager));
uint256 wethBalanceBefore = WETH.balanceOf(address(poolManager));

// recipient balance before
uint256 recipientBalanceBefore = address(RECIPIENT).balance;
uint256 recipientUSDCBalanceBefore = USDC.balanceOf(RECIPIENT);
uint256 recipientWETHBalanceBefore = WETH.balanceOf(RECIPIENT);

assertEq(params.value, 0);
vm.prank(from);
(bool success,) = address(router).call(params.data);
Expand All @@ -100,8 +158,161 @@ contract MigratorCallParametersTest is Test, Interop, DeployRouter {
// all funds were swept out of contracts
assertEq(USDC.balanceOf(MAINNET_ROUTER), 0);
assertEq(WETH.balanceOf(MAINNET_ROUTER), 0);
assertEq(address(MAINNET_ROUTER).balance, 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);
assertEq(address(v4PositionManager).balance, 0);

// pool manager balance after, eth and usdc deposited
assertGt(address(poolManager).balance, ethBalanceBefore);
assertGt(USDC.balanceOf(address(poolManager)), usdcBalanceBefore);
assertEq(WETH.balanceOf(address(poolManager)), wethBalanceBefore);

// recipient balance after
assertEq(address(RECIPIENT).balance, recipientBalanceBefore);
assertGe(USDC.balanceOf(RECIPIENT), recipientUSDCBalanceBefore);
assertGe(WETH.balanceOf(RECIPIENT), recipientWETHBalanceBefore);

// 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_toErc20_withPermit() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_TO_ERC20_WITH_PERMIT");

// 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);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// pool manager balance before
uint256 ethBalanceBefore = address(poolManager).balance;
uint256 usdcBalanceBefore = USDC.balanceOf(address(poolManager));
uint256 wethBalanceBefore = WETH.balanceOf(address(poolManager));

// recipient balance before
uint256 recipientBalanceBefore = address(RECIPIENT).balance;
uint256 recipientUSDCBalanceBefore = USDC.balanceOf(RECIPIENT);
uint256 recipientWETHBalanceBefore = WETH.balanceOf(RECIPIENT);

assertEq(params.value, 0);
vm.prank(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(address(MAINNET_ROUTER).balance, 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);
assertEq(address(v4PositionManager).balance, 0);

// pool manager balance after, weth and usdc deposited
assertEq(address(poolManager).balance, ethBalanceBefore);
assertGt(USDC.balanceOf(address(poolManager)), usdcBalanceBefore);
assertGt(WETH.balanceOf(address(poolManager)), wethBalanceBefore);

// recipient balance after
assertEq(address(RECIPIENT).balance, recipientBalanceBefore);
assertGe(USDC.balanceOf(RECIPIENT), recipientUSDCBalanceBefore);
assertGe(WETH.balanceOf(RECIPIENT), recipientWETHBalanceBefore);

// 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_toEth_withPermitAndPoolInitialize() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_TO_ETH_WITH_PERMIT_AND_POOL_INITIALIZE");

// 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);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// pool manager balance before
uint256 ethBalanceBefore = address(poolManager).balance;
uint256 usdcBalanceBefore = USDC.balanceOf(address(poolManager));
uint256 wethBalanceBefore = WETH.balanceOf(address(poolManager));

// recipient balance before
uint256 recipientBalanceBefore = address(RECIPIENT).balance;
uint256 recipientUSDCBalanceBefore = USDC.balanceOf(RECIPIENT);
uint256 recipientWETHBalanceBefore = WETH.balanceOf(RECIPIENT);

assertEq(params.value, 0);
vm.prank(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(address(MAINNET_ROUTER).balance, 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);
assertEq(address(v4PositionManager).balance, 0);

// pool manager balance after, eth and usdc deposited
assertGt(address(poolManager).balance, ethBalanceBefore);
assertGt(USDC.balanceOf(address(poolManager)), usdcBalanceBefore);
assertEq(WETH.balanceOf(address(poolManager)), wethBalanceBefore);

// recipient balance after
assertEq(address(RECIPIENT).balance, recipientBalanceBefore);
assertGe(USDC.balanceOf(RECIPIENT), recipientUSDCBalanceBefore);
assertGe(WETH.balanceOf(RECIPIENT), recipientWETHBalanceBefore);

// 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_toErc20_withPermitAndPoolInitialize() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_TO_ERC20_WITH_PERMIT_AND_POOL_INITIALIZE");

// 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);
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 1);

// pool manager balance before
uint256 ethBalanceBefore = address(poolManager).balance;
uint256 usdcBalanceBefore = USDC.balanceOf(address(poolManager));
uint256 wethBalanceBefore = WETH.balanceOf(address(poolManager));

// recipient balance before
uint256 recipientBalanceBefore = address(RECIPIENT).balance;
uint256 recipientUSDCBalanceBefore = USDC.balanceOf(RECIPIENT);
uint256 recipientWETHBalanceBefore = WETH.balanceOf(RECIPIENT);

assertEq(params.value, 0);
vm.prank(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(address(MAINNET_ROUTER).balance, 0);
assertEq(USDC.balanceOf(address(v4PositionManager)), 0);
assertEq(WETH.balanceOf(address(v4PositionManager)), 0);
assertEq(address(v4PositionManager).balance, 0);

// pool manager balance after, weth and usdc deposited
assertEq(address(poolManager).balance, ethBalanceBefore);
assertGt(USDC.balanceOf(address(poolManager)), usdcBalanceBefore);
assertGt(WETH.balanceOf(address(poolManager)), wethBalanceBefore);

// recipient balance after
assertEq(address(RECIPIENT).balance, recipientBalanceBefore);
assertGe(USDC.balanceOf(RECIPIENT), recipientUSDCBalanceBefore);
assertGe(WETH.balanceOf(RECIPIENT), recipientWETHBalanceBefore);

// old position burned, new position minted
assertEq(INonfungiblePositionManager(V3_POSITION_MANAGER).balanceOf(from), 0, "V3 NOT BURNT");
Expand Down
Loading

0 comments on commit 13a909c

Please sign in to comment.