Skip to content

Commit

Permalink
feat: update universal router with new action and position call prote…
Browse files Browse the repository at this point in the history
…ction
  • Loading branch information
ChefMist committed Nov 15, 2024
1 parent 799d077 commit e7f914c
Show file tree
Hide file tree
Showing 23 changed files with 368 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
138891
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
159093
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
140191
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
160343
Original file line number Diff line number Diff line change
@@ -1 +1 @@
151862
151895
Original file line number Diff line number Diff line change
@@ -1 +1 @@
154767
154800
Original file line number Diff line number Diff line change
@@ -1 +1 @@
243258
243324
Original file line number Diff line number Diff line change
@@ -1 +1 @@
150557
150590
Original file line number Diff line number Diff line change
@@ -1 +1 @@
254163
254229
2 changes: 1 addition & 1 deletion .forge-snapshots/UniversalRouterBytecodeSize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24566
25469
Original file line number Diff line number Diff line change
@@ -1 +1 @@
558784
560312
Original file line number Diff line number Diff line change
@@ -1 +1 @@
594723
595771
Original file line number Diff line number Diff line change
@@ -1 +1 @@
570540
571588
Original file line number Diff line number Diff line change
@@ -1 +1 @@
582977
584505
33 changes: 27 additions & 6 deletions src/base/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {IERC721Permit} from "pancake-v4-periphery/src/pool-cl/interfaces/IERC721
import {ActionConstants} from "pancake-v4-periphery/src/libraries/ActionConstants.sol";
import {BaseActionsRouter} from "pancake-v4-periphery/src/base/BaseActionsRouter.sol";
import {CalldataDecoder} from "pancake-v4-periphery/src/libraries/CalldataDecoder.sol";
import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol";
import {IBinPoolManager} from "pancake-v4-core/src/pool-bin/interfaces/IBinPoolManager.sol";

/// @title Decodes and Executes Commands
/// @notice Called by the UniversalRouter contract to efficiently decode and execute a singular command
Expand All @@ -34,8 +37,6 @@ abstract contract Dispatcher is

error InvalidCommandType(uint256 commandType);
error BalanceTooLow();
error InvalidAction(bytes4 action);
error NotAuthorizedForToken(uint256 tokenId);

/// @notice Executes encoded commands along with provided inputs.
/// @param commands A set of concatenated commands, each 1 byte in length
Expand Down Expand Up @@ -305,14 +306,34 @@ abstract contract Dispatcher is
/// @dev ensure there's follow-up action if v3 position's removed token are sent to router contract
(success, output) = address(V3_POSITION_MANAGER).call(inputs);
return (success, output);
} else if (command == Commands.V4_CL_INITIALIZE_POOL) {
PoolKey calldata poolKey;
uint160 sqrtPriceX96;
assembly {
poolKey := inputs.offset
sqrtPriceX96 := calldataload(add(inputs.offset, 0xc0)) // poolKey has 6 variable, so it takes 192 space = 0xc0
}
// <wip> remove "" hookData once we updated universal-router dependencies
(success, output) = address(clPoolManager).call(
abi.encodeCall(ICLPoolManager.initialize, (poolKey, sqrtPriceX96, ""))
);
} else if (command == Commands.V4_BIN_INITIALIZE_POOL) {
PoolKey calldata poolKey;
uint24 activeId;
assembly {
poolKey := inputs.offset
activeId := calldataload(add(inputs.offset, 0xc0)) // poolKey has 6 variable, so it takes 192 space = 0xc0
}
// <wip> remove "" hookData once we updated universal-router dependencies
(success, output) = address(binPoolManager).call(
abi.encodeCall(IBinPoolManager.initialize, (poolKey, activeId, ""))
);
} else if (command == Commands.V4_CL_POSITION_CALL) {
// should only call modifyLiquidities() with Actions.CL_MINT_POSITION
// do not permit or approve this contract over a v4 position or someone could use this command to decrease, burn, or transfer your position
_checkV4ClPositionManagerCall(inputs);
(success, output) = address(V4_CL_POSITION_MANAGER).call{value: address(this).balance}(inputs);
return (success, output);
} else if (command == Commands.V4_BIN_POSITION_CALL) {
// should only call modifyLiquidities() with Actions.BIN_ADD_LIQUIDITY
// do not permit or approve this contract over a v4 position or someone could use this command to decrease, burn, or transfer your position
_checkV4BinPositionManagerCall(inputs);
(success, output) = address(V4_BIN_POSITION_MANAGER).call{value: address(this).balance}(inputs);
return (success, output);
} else {
Expand Down
6 changes: 4 additions & 2 deletions src/libraries/Commands.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ library Commands {
uint256 constant V4_SWAP = 0x10;
uint256 constant V3_POSITION_MANAGER_PERMIT = 0x11;
uint256 constant V3_POSITION_MANAGER_CALL = 0x12;
uint256 constant V4_CL_POSITION_CALL = 0x13;
uint256 constant V4_BIN_POSITION_CALL = 0x14;
uint256 constant V4_CL_INITIALIZE_POOL = 0x13;
uint256 constant V4_BIN_INITIALIZE_POOL = 0x14;
uint256 constant V4_CL_POSITION_CALL = 0x15;
uint256 constant V4_BIN_POSITION_CALL = 0x16;
// COMMAND_PLACEHOLDER = 0x15 -> 0x20

// Command Types where 0x21<=value<=0x3f
Expand Down
90 changes: 90 additions & 0 deletions src/modules/V3ToV4Migrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,27 @@ pragma solidity ^0.8.0;
import {RouterImmutables} from "../base/RouterImmutables.sol";
import {IV3NonfungiblePositionManager} from
"pancake-v4-periphery/src/interfaces/external/IV3NonfungiblePositionManager.sol";
import {Actions} from "pancake-v4-periphery/src/libraries/Actions.sol";
import {CalldataDecoder} from "pancake-v4-periphery/src/libraries/CalldataDecoder.sol";
import {IPositionManager} from "pancake-v4-periphery/src/interfaces/IPositionManager.sol";
import {console2} from "forge-std/console2.sol";

/// @title V3 to V4 Migrator
/// @notice A contract that migrates liquidity from PancakeSwap V3 to V4
abstract contract V3ToV4Migrator is RouterImmutables {
using CalldataDecoder for bytes;

error NotAuthorizedForToken(uint256 tokenId);
error InvalidAction(bytes4 action);
error OnlyMintAllowed();
error OnlyAddLiqudityAllowed();
error BlacklistedAction();

enum PoolType {
CL,
BIN
}

/// @dev validate if an action is decreaseLiquidity, collect, or burn
function isValidAction(bytes4 selector) internal pure returns (bool) {
return selector == IV3NonfungiblePositionManager.decreaseLiquidity.selector
Expand All @@ -21,4 +38,77 @@ abstract contract V3ToV4Migrator is RouterImmutables {
return caller == owner || V3_POSITION_MANAGER.getApproved(tokenId) == caller
|| V3_POSITION_MANAGER.isApprovedForAll(owner, caller);
}

function isBlacklistedCLPositionManagerAction(uint256 action) internal pure returns (bool) {
return action == Actions.CL_INCREASE_LIQUIDITY || action == Actions.CL_DECREASE_LIQUIDITY
|| action == Actions.CL_BURN_POSITION;
}

function isBlacklistedBinPositionManagerAction(uint256 action) internal pure returns (bool) {
return action == Actions.BIN_REMOVE_LIQUIDITY;
}

/// @dev check that the v4 position manager call is a safe call
/// of the position-altering Actions, we only allow Actions.MINT
/// this is because, if a user could be tricked into approving the UniversalRouter for
/// their position, an attacker could take their fees, or drain their entire position
function _checkV4ClPositionManagerCall(bytes calldata inputs) internal view {
bytes4 selector;
assembly {
selector := calldataload(inputs.offset)
}
if (selector != V4_CL_POSITION_MANAGER.modifyLiquidities.selector) {
revert InvalidAction(selector);
}

// slice is `abi.encode(bytes unlockData, uint256 deadline)`
bytes calldata slice = inputs[4:];
// the first bytes(0) extracts the unlockData parameter from modifyLiquidities
// unlockData = `abi.encode(bytes actions, bytes[] params)`
// the second bytes(0) extracts the actions parameter from unlockData
bytes calldata actions = slice.toBytes(0).toBytes(0);

uint256 numActions = actions.length;

for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) {
uint256 action = uint8(actions[actionIndex]);

if (
action == Actions.CL_INCREASE_LIQUIDITY || action == Actions.CL_DECREASE_LIQUIDITY
|| action == Actions.CL_BURN_POSITION
) {
revert OnlyMintAllowed();
}
}
}

/// @dev check that the v4 position manager call is a safe call
/// of the position-altering Actions, we only allow Actions.BIN_ADD_LIQUIDITY
/// this is because, if a user could be tricked into approving the UniversalRouter for
/// their position, an attacker could drain their entire position
function _checkV4BinPositionManagerCall(bytes calldata inputs) internal view {
bytes4 selector;
assembly {
selector := calldataload(inputs.offset)
}
if (selector != V4_BIN_POSITION_MANAGER.modifyLiquidities.selector) {
revert InvalidAction(selector);
}

// slice is `abi.encode(bytes unlockData, uint256 deadline)`
bytes calldata slice = inputs[4:];
// the first bytes(0) extracts the unlockData parameter from modifyLiquidities
// unlockData = `abi.encode(bytes actions, bytes[] params)`
// the second bytes(0) extracts the actions parameter from unlockData
bytes calldata actions = slice.toBytes(0).toBytes(0);

uint256 numActions = actions.length;

for (uint256 actionIndex = 0; actionIndex < numActions; actionIndex++) {
uint256 action = uint8(actions[actionIndex]);
if (action == Actions.BIN_REMOVE_LIQUIDITY) {
revert OnlyAddLiqudityAllowed();
}
}
}
}
2 changes: 1 addition & 1 deletion test/UniversalRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ contract UniversalRouterTest is Test, GasSnapshot, Permit2SignatureHelpers, Depl
// if valid commands, return
if (command >= 0x00 && command <= 0x06) return;
if (command >= 0x08 && command <= 0x0e) return;
if (command >= 0x10 && command <= 0x14) return;
if (command >= 0x10 && command <= 0x16) return;
if (command >= 0x21 && command <= 0x23) return;

bytes memory commands = abi.encodePacked(bytes1(uint8(command)));
Expand Down
73 changes: 71 additions & 2 deletions test/V3ToV4Migration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {BinLiquidityHelper} from "pancake-v4-periphery/test/pool-bin/helper/BinL

import {IPancakeV3PoolDeployer} from "../src/modules/pancakeswap/v3/interfaces/IPancakeV3PoolDeployer.sol";
import {IPancakeV3Factory} from "../src/modules/pancakeswap/v3/interfaces/IPancakeV3Factory.sol";
import {V3ToV4Migrator} from "../src/modules/V3ToV4Migrator.sol";
import {IUniversalRouter} from "../src/interfaces/IUniversalRouter.sol";
import {Commands} from "../src/libraries/Commands.sol";
import {RouterParameters} from "../src/base/RouterImmutables.sol";
Expand Down Expand Up @@ -184,7 +185,7 @@ contract V3ToV4MigrationTest is BasePancakeSwapV4, OldVersionHelper, BinLiquidit
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encodePacked(IV3NonfungiblePositionManager.collect.selector, abi.encode(params));

vm.expectRevert(abi.encodeWithSelector(Dispatcher.NotAuthorizedForToken.selector, params.tokenId));
vm.expectRevert(abi.encodeWithSelector(V3ToV4Migrator.NotAuthorizedForToken.selector, params.tokenId));
router.execute(commands, inputs);
}

Expand All @@ -208,7 +209,7 @@ contract V3ToV4MigrationTest is BasePancakeSwapV4, OldVersionHelper, BinLiquidit
inputs[0] = abi.encodePacked(IV3NonfungiblePositionManager.mint.selector, abi.encode(params));

vm.expectRevert(
abi.encodeWithSelector(Dispatcher.InvalidAction.selector, IV3NonfungiblePositionManager.mint.selector)
abi.encodeWithSelector(V3ToV4Migrator.InvalidAction.selector, IV3NonfungiblePositionManager.mint.selector)
);
router.execute(commands, inputs);
}
Expand Down Expand Up @@ -280,6 +281,74 @@ contract V3ToV4MigrationTest is BasePancakeSwapV4, OldVersionHelper, BinLiquidit
assertEq(token1.balanceOf(address(router)), 9999999999999999999);
}

function test_v4CLPositionmanger_InvalidAction() public {
Plan memory planner = Planner.init();

// prep universal router actions
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_CL_POSITION_CALL)));
bytes[] memory inputs = new bytes[](1);

bytes4 invalidSelector = IPositionManager.modifyLiquiditiesWithoutLock.selector;
inputs[0] = abi.encodePacked(invalidSelector, abi.encode(planner.encode(), block.timestamp));
vm.expectRevert(abi.encodeWithSelector(V3ToV4Migrator.InvalidAction.selector, invalidSelector));
router.execute(commands, inputs);
}

function test_v4CLPositionmanger_BlacklistedAction() public {
Plan memory planner = Planner.init();
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_CL_POSITION_CALL)));
bytes[] memory inputs = new bytes[](1);

uint256[] memory invalidActions = new uint256[](3);
invalidActions[0] = Actions.CL_INCREASE_LIQUIDITY;
invalidActions[1] = Actions.CL_DECREASE_LIQUIDITY;
invalidActions[2] = Actions.CL_BURN_POSITION;

for (uint256 i; i < invalidActions.length; i++) {
planner.add(invalidActions[i], "");
inputs[0] = abi.encodePacked(
IPositionManager.modifyLiquidities.selector, abi.encode(planner.encode(), block.timestamp)
);

// verify revert for invalid actions
vm.expectRevert(V3ToV4Migrator.BlacklistedAction.selector);
router.execute(commands, inputs);
}
}

function test_v4BinPositionmanger_InvalidAction() public {
Plan memory planner = Planner.init();

// prep universal router actions
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_BIN_POSITION_CALL)));
bytes[] memory inputs = new bytes[](1);

bytes4 invalidSelector = IPositionManager.modifyLiquiditiesWithoutLock.selector;
inputs[0] = abi.encodePacked(invalidSelector, abi.encode(planner.encode(), block.timestamp));
vm.expectRevert(abi.encodeWithSelector(V3ToV4Migrator.InvalidAction.selector, invalidSelector));
router.execute(commands, inputs);
}

function test_v4BinPositionmanger_BlacklistedAction() public {
Plan memory planner = Planner.init();
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_BIN_POSITION_CALL)));
bytes[] memory inputs = new bytes[](1);

uint256[] memory invalidActions = new uint256[](1);
invalidActions[0] = Actions.BIN_REMOVE_LIQUIDITY;

for (uint256 i; i < invalidActions.length; i++) {
planner.add(invalidActions[i], "");
inputs[0] = abi.encodePacked(
IPositionManager.modifyLiquidities.selector, abi.encode(planner.encode(), block.timestamp)
);

// verify revert for invalid actions
vm.expectRevert(V3ToV4Migrator.BlacklistedAction.selector);
router.execute(commands, inputs);
}
}

/// @dev Assume token0/token1 is aready in universal router from earlier steps on v3
/// then add liquidity to v4 cl and sweep remaining token
function test_v4CLPositionmanager_Mint() public {
Expand Down
Loading

0 comments on commit e7f914c

Please sign in to comment.