Skip to content

Commit

Permalink
fix(universal-router-sdk): fix migration encoding (#156)
Browse files Browse the repository at this point in the history
Co-authored-by: Alice Henshaw <[email protected]>
Co-authored-by: Alice <[email protected]>
  • Loading branch information
3 people authored Oct 11, 2024
1 parent f591a5e commit 089431e
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 271 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 @@ -37,7 +37,7 @@
"@uniswap/v2-core": "^1.0.1",
"@uniswap/v2-sdk": "^4.6.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-sdk": "^3.17.0",
"@uniswap/v3-sdk": "^3.18.1",
"@uniswap/v4-sdk": "^1.10.0",
"bignumber.js": "^9.0.2",
"ethers": "^5.7.0"
Expand Down
28 changes: 24 additions & 4 deletions sdks/universal-router-sdk/src/swapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Interface } from '@ethersproject/abi'
import { BigNumber, BigNumberish } from 'ethers'
import {
MethodParameters,
Multicall,
Position as V3Position,
NonfungiblePositionManager as V3PositionManager,
RemoveLiquidityOptions as V3RemoveLiquidityOptions,
Expand Down Expand Up @@ -74,12 +75,17 @@ export abstract class SwapRouter {
* - 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 {
public static migrateV3ToV4CallParameters(
options: MigrateV3ToV4Options,
positionManagerOverride?: string
): MethodParameters {
const token0 = options.inputPosition.pool.token0
const token1 = options.inputPosition.pool.token1
const v4PositionManagerAddress =
positionManagerOverride ??
CHAIN_TO_ADDRESSES_MAP[options.outputPosition.pool.chainId as SupportedChainsType].v4PositionManagerAddress

// validate the parameters
invariant(token0 === options.outputPosition.pool.token0, 'TOKEN0_MISMATCH')
invariant(token1 === options.outputPosition.pool.token1, 'TOKEN1_MISMATCH')
invariant(
Expand All @@ -91,12 +97,12 @@ 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')

const planner = new RoutePlanner()

// add position permit to the universal router planner
if (options.v3RemoveLiquidityOptions.permit) {
// permit spender should be UR
const universalRouterAddress = UNIVERSAL_ROUTER_ADDRESS(
Expand All @@ -106,14 +112,28 @@ export abstract class SwapRouter {
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)
// remove permit so that multicall doesnt add it again
delete options.v3RemoveLiquidityOptions.permit
}

// encode v3 withdraw
const v3RemoveParams = V3PositionManager.removeCallParameters(
const v3RemoveParams: MethodParameters = V3PositionManager.removeCallParameters(
options.inputPosition,
options.v3RemoveLiquidityOptions
)
planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [v3RemoveParams.calldata])
const v3Calls: string[] = Multicall.decodeMulticall(v3RemoveParams.calldata)

for (const v3Call of v3Calls) {
// slice selector - 0x + 4 bytes = 10 characters
const selector = v3Call.slice(0, 10)
invariant(
selector == V3PositionManager.INTERFACE.getSighash('collect') ||
selector == V3PositionManager.INTERFACE.getSighash('decreaseLiquidity') ||
selector == V3PositionManager.INTERFACE.getSighash('burn'),
'INVALID_CALL: ' + selector
)
planner.addCommand(CommandType.V3_POSITION_MANAGER_CALL, [v3Call])
}

// encode v4 mint
const v4AddParams = V4PositionManager.addCallParameters(options.outputPosition, options.v4AddLiquidityOptions)
Expand Down
85 changes: 85 additions & 0 deletions sdks/universal-router-sdk/test/forge/MigratorCallParameters.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Test, stdJson} from "forge-std/Test.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {UniversalRouter} from "universal-router/UniversalRouter.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {DeployRouter} from "./utils/DeployRouter.sol";
import {MethodParameters, Interop} from "./utils/Interop.sol";
import {INonfungiblePositionManager} from "v3-periphery/interfaces/INonfungiblePositionManager.sol";

contract MigratorCallParametersTest is Test, Interop, DeployRouter {
using stdJson for string;

// starting eth balance
uint256 constant BALANCE = 10 ether;

function setUp() public {
fromPrivateKey = 0x1234;
from = vm.addr(fromPrivateKey);
string memory root = vm.projectRoot();
json = vm.readFile(string.concat(root, "/test/forge/interop.json"));

vm.createSelectFork(vm.envString("FORK_URL"), 16075500);
deployV4Contracts();
initializeV4Pools();
vm.startPrank(from);
deployRouter();
vm.deal(from, BALANCE);
}

function test_migrate_withoutPermit() public {
MethodParameters memory params = readFixture(json, "._MIGRATE_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);

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

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(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");
}
}
66 changes: 31 additions & 35 deletions sdks/universal-router-sdk/test/forge/SwapERC20CallParameters.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ import {UniversalRouter} from "universal-router/UniversalRouter.sol";
import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
import {DeployRouter} from "./utils/DeployRouter.sol";
import {MethodParameters, Interop} from "./utils/Interop.sol";
import {INonfungiblePositionManager} from "v3-periphery/interfaces/INonfungiblePositionManager.sol";

contract SwapERC20CallParametersTest is Test, Interop, DeployRouter {
using stdJson for string;

ERC20 private constant WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
ERC20 private constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
ERC20 private constant DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
// starting eth balance
uint256 constant BALANCE = 10 ether;
uint256 ONE_USDC = 10 ** 6;
uint256 ONE_DAI = 1 ether;

function setUp() public {
fromPrivateKey = 0x1234;
Expand All @@ -27,9 +23,9 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter {

vm.createSelectFork(vm.envString("FORK_URL"), 16075500);
deployV4Contracts();
initializeV4Pools(WETH, USDC, DAI);
initializeV4Pools();
vm.startPrank(from);
deployRouterAndPermit2();
deployRouter();
vm.deal(from, BALANCE);
}

Expand Down Expand Up @@ -758,24 +754,24 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter {
}

function testMixedV3ToV4UnwrapWETH() public {
MethodParameters memory params = readFixture(json, "._UNISWAP_MIXED_USDC_DAI_UNWRAP_WETH_V3_TO_V4");
MethodParameters memory params = readFixture(json, "._UNISWAP_MIXED_USDC_DAI_UNWRAP_WETH_V3_TO_V4");

uint256 usdcAmount = 1000000000;
deal(address(USDC), from, usdcAmount);
USDC.approve(address(permit2), usdcAmount);
permit2.approve(address(USDC), address(router), uint160(usdcAmount), uint48(block.timestamp + 1000));
uint256 usdcAmount = 1000000000;
deal(address(USDC), from, usdcAmount);
USDC.approve(address(permit2), usdcAmount);
permit2.approve(address(USDC), address(router), uint160(usdcAmount), uint48(block.timestamp + 1000));

assertEq(USDC.balanceOf(from), usdcAmount);
assertEq(DAI.balanceOf(RECIPIENT), 0);
assertEq(USDC.balanceOf(from), usdcAmount);
assertEq(DAI.balanceOf(RECIPIENT), 0);

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

assertLe(from.balance, BALANCE - params.value);
assertEq(USDC.balanceOf(from), 0);
assertEq(DAI.balanceOf(address(router)), 0);
assertGt(DAI.balanceOf(RECIPIENT), 0);
assertEq(address(router).balance, 0);
assertLe(from.balance, BALANCE - params.value);
assertEq(USDC.balanceOf(from), 0);
assertEq(DAI.balanceOf(address(router)), 0);
assertGt(DAI.balanceOf(RECIPIENT), 0);
assertEq(address(router).balance, 0);
}

function testMixedV2ToV4UnwrapWETH() public {
Expand All @@ -800,24 +796,24 @@ contract SwapERC20CallParametersTest is Test, Interop, DeployRouter {
}

function testMixedV4ToV3WrapETH() public {
MethodParameters memory params = readFixture(json, "._UNISWAP_MIXED_DAI_USDC_WRAP_ETH_V4_TO_V3");
MethodParameters memory params = readFixture(json, "._UNISWAP_MIXED_DAI_USDC_WRAP_ETH_V4_TO_V3");

uint256 daiAmount = 1000 ether;
deal(address(DAI), from, daiAmount);
DAI.approve(address(permit2), daiAmount);
permit2.approve(address(DAI), address(router), uint160(daiAmount), uint48(block.timestamp + 1000));
uint256 daiAmount = 1000 ether;
deal(address(DAI), from, daiAmount);
DAI.approve(address(permit2), daiAmount);
permit2.approve(address(DAI), address(router), uint160(daiAmount), uint48(block.timestamp + 1000));

assertEq(DAI.balanceOf(from), daiAmount);
assertEq(USDC.balanceOf(RECIPIENT), 0);
assertEq(DAI.balanceOf(from), daiAmount);
assertEq(USDC.balanceOf(RECIPIENT), 0);

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

assertLe(from.balance, BALANCE - params.value);
assertEq(DAI.balanceOf(from), 0);
assertEq(USDC.balanceOf(address(router)), 0);
assertGt(USDC.balanceOf(RECIPIENT), 0);
assertEq(address(router).balance, 0);
assertLe(from.balance, BALANCE - params.value);
assertEq(DAI.balanceOf(from), 0);
assertEq(USDC.balanceOf(address(router)), 0);
assertGt(USDC.balanceOf(RECIPIENT), 0);
assertEq(address(router).balance, 0);
}

function testMixedV4ToV2WrapETH() public {
Expand Down
Loading

0 comments on commit 089431e

Please sign in to comment.