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

[Part 2 of 2] Internal feedback from universal router]: protect against invalid Positionmanage action #18

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1 +1 @@
146304
146587
Original file line number Diff line number Diff line change
@@ -1 +1 @@
123362
123645
Original file line number Diff line number Diff line change
@@ -1 +1 @@
147266
147552
Original file line number Diff line number Diff line change
@@ -1 +1 @@
178676
179182
Original file line number Diff line number Diff line change
@@ -1 +1 @@
149062
149348
Original file line number Diff line number Diff line change
@@ -1 +1 @@
182517
183027
Original file line number Diff line number Diff line change
@@ -1 +1 @@
153435
153723
Original file line number Diff line number Diff line change
@@ -1 +1 @@
151645
151929
Original file line number Diff line number Diff line change
@@ -1 +1 @@
172308
172365
Original file line number Diff line number Diff line change
@@ -1 +1 @@
174558
174621
Original file line number Diff line number Diff line change
@@ -1 +1 @@
181657
181723
Original file line number Diff line number Diff line change
@@ -1 +1 @@
246971
247037
Original file line number Diff line number Diff line change
@@ -1 +1 @@
183197
183263
Original file line number Diff line number Diff line change
@@ -1 +1 @@
185996
186062
Original file line number Diff line number Diff line change
@@ -1 +1 @@
250730
250804
Original file line number Diff line number Diff line change
@@ -1 +1 @@
187529
187599
Original file line number Diff line number Diff line change
@@ -1 +1 @@
100125
100141
Original file line number Diff line number Diff line change
@@ -1 +1 @@
100754
100768
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152659
150557
Original file line number Diff line number Diff line change
@@ -1 +1 @@
193198
193201
Original file line number Diff line number Diff line change
@@ -1 +1 @@
192654
192657
2 changes: 1 addition & 1 deletion .forge-snapshots/UniversalRouterBytecodeSize.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24160
24465
2 changes: 1 addition & 1 deletion .forge-snapshots/UniversalRouterTest#test_sweep_token.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
55441
55447
Original file line number Diff line number Diff line change
@@ -1 +1 @@
559373
564846
Original file line number Diff line number Diff line change
@@ -1 +1 @@
291590
291594
Original file line number Diff line number Diff line change
@@ -1 +1 @@
594191
594429
Original file line number Diff line number Diff line change
@@ -1 +1 @@
570028
570263
Original file line number Diff line number Diff line change
@@ -1 +1 @@
583544
589080
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ src = "src"
out = 'foundry-out'
libs = ["lib"]
via_ir = true
optimizer_runs = 30_000
optimizer_runs = 25_000
ffi = true
fs_permissions = [
{ access = "read-write", path = ".forge-snapshots/" },
Expand Down
10 changes: 8 additions & 2 deletions src/base/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ abstract contract Dispatcher is
error InvalidCommandType(uint256 commandType);
error BalanceTooLow();
error InvalidAction(bytes4 action);
error InvalidPositionManagerAction(uint256 action);
error NotAuthorizedForToken(uint256 tokenId);

/// @notice Executes encoded commands along with provided inputs.
Expand Down Expand Up @@ -305,8 +306,13 @@ abstract contract Dispatcher is
(success, output) = address(V3_POSITION_MANAGER).call(inputs);
return (success, output);
} 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
// Only modifyLiquidities() with Actions.CL_MINT_POSITION and SETTLEMENT related option are allowed
// CL_INCREASE_LIQUIDITY, CL_DECREASE_LIQUIDITY and CL_BURN_POSITION are blocked
(bool isInvalid, uint256 action) = containInValidV4AClActions(inputs);
if (isInvalid) {
revert InvalidPositionManagerAction(action);
}

(success, output) = address(V4_CL_POSITION_MANAGER).call{value: address(this).balance}(inputs);
return (success, output);
} else if (command == Commands.V4_BIN_POSITION_CALL) {
Expand Down
2 changes: 0 additions & 2 deletions src/libraries/Constants.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import {IWETH9} from "../interfaces/IWETH9.sol";

/// @title Constant state
/// @notice Constant state used by the Universal Router
library Constants {
Expand Down
20 changes: 20 additions & 0 deletions src/libraries/MaxInputAmount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.24;

/// @notice A library used to store the maximum desired amount of input tokens for exact output swaps; used for checking slippage
library MaxInputAmount {
// The slot holding the the maximum desired amount of input tokens, transiently. bytes32(uint256(keccak256("MaxAmountIn")) - 1)
bytes32 constant MAX_AMOUNT_IN_SLOT = 0xaf28d9864a81dfdf71cab65f4e5d79a0cf9b083905fb8971425e6cb581b3f692;

function set(uint256 maxAmountIn) internal {
assembly ("memory-safe") {
tstore(MAX_AMOUNT_IN_SLOT, maxAmountIn)
}
}

function get() internal view returns (uint256 maxAmountIn) {
assembly ("memory-safe") {
maxAmountIn := tload(MAX_AMOUNT_IN_SLOT)
}
}
}
35 changes: 35 additions & 0 deletions src/modules/V3ToV4Migrator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ pragma solidity ^0.8.0;
import {RouterImmutables} from "../base/RouterImmutables.sol";
import {IV3NonfungiblePositionManager} from
"pancake-v4-periphery/src/interfaces/external/IV3NonfungiblePositionManager.sol";
import {CalldataDecoder} from "pancake-v4-periphery/src/libraries/CalldataDecoder.sol";
import {Actions} from "pancake-v4-periphery/src/libraries/Actions.sol";
import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";
import {IPositionManager} from "pancake-v4-periphery/src/interfaces/IPositionManager.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;

/// @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 +27,33 @@ abstract contract V3ToV4Migrator is RouterImmutables {
return caller == owner || V3_POSITION_MANAGER.getApproved(tokenId) == caller
|| V3_POSITION_MANAGER.isApprovedForAll(owner, caller);
}

function containInValidV4AClActions(bytes calldata inputs) internal pure returns (bool isInvalid, uint256 action) {
// Decode the data of modifyLiquidities(bytes calldata payload, uint256 deadline)
bytes4 selector = bytes4(inputs[:4]); // todo:

if (selector == IPositionManager.modifyLiquidities.selector) {
bytes calldata data = inputs[4:];

// decode payload to get bytes calldata actions, bytes[] calldata params
(bytes memory payload,) = abi.decode(data, (bytes, uint256));
(bytes memory actions,) = abi.decode(payload, (bytes, bytes[]));

for (uint256 actionIndex = 0; actionIndex < actions.length; actionIndex++) {
action = uint8(actions[actionIndex]);
if (
action == Actions.CL_INCREASE_LIQUIDITY || action == Actions.CL_DECREASE_LIQUIDITY
|| action == Actions.CL_BURN_POSITION
) {
return (true, action);
}
}
} else if (selector == IPositionManager.modifyLiquiditiesWithoutLock.selector) {
// todo:
} else if (selector == Multicall.multicall.selector) {
// todo:
}

// todo: revert InvalidSelector
}
}
4 changes: 2 additions & 2 deletions src/modules/pancakeswap/StableSwapRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {Constants} from "../../libraries/Constants.sol";
import {UniversalRouterHelper} from "../../libraries/UniversalRouterHelper.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IStableSwap} from "../../interfaces/IStableSwap.sol";

/// @title Router for PancakeSwap Stable Trades
abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable {
abstract contract StableSwapRouter is RouterImmutables, Permit2Payments, Ownable2Step {
using SafeTransferLib for ERC20;
using UniversalRouterHelper for address;

Expand Down
14 changes: 4 additions & 10 deletions src/modules/pancakeswap/v3/V3SwapRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {Constants} from "../../../libraries/Constants.sol";
import {UniversalRouterHelper} from "../../../libraries/UniversalRouterHelper.sol";
import {RouterImmutables} from "../../../base/RouterImmutables.sol";
import {Permit2Payments} from "../../Permit2Payments.sol";
import {MaxInputAmount} from "../../../libraries/MaxInputAmount.sol";
import {ERC20} from "solmate/src/tokens/ERC20.sol";
import {CalldataDecoder} from "pancake-v4-periphery/src/libraries/CalldataDecoder.sol";

Expand All @@ -26,13 +27,6 @@ abstract contract V3SwapRouter is RouterImmutables, Permit2Payments, IPancakeV3S
error V3InvalidAmountOut();
error V3InvalidCaller();

/// @dev Used as the placeholder value for maxAmountIn, because the computed amount in for an exact output swap
/// can never actually be this value
uint256 private constant DEFAULT_MAX_AMOUNT_IN = type(uint256).max;

/// @dev Transient storage variable used for checking slippage
uint256 private maxAmountInCached = DEFAULT_MAX_AMOUNT_IN;

/// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK)
uint160 internal constant MIN_SQRT_RATIO = 4295128739;

Expand Down Expand Up @@ -66,7 +60,7 @@ abstract contract V3SwapRouter is RouterImmutables, Permit2Payments, IPancakeV3S
path = path.skipToken();
_swap(-amountToPay.toInt256(), msg.sender, path, payer, false);
} else {
if (amountToPay > maxAmountInCached) revert V3TooMuchRequested();
if (amountToPay > MaxInputAmount.get()) revert V3TooMuchRequested();
// note that because exact output swaps are executed in reverse order, tokenOut is actually tokenIn
payOrPermit2Transfer(tokenOut, payer, msg.sender, amountToPay);
}
Expand Down Expand Up @@ -133,15 +127,15 @@ abstract contract V3SwapRouter is RouterImmutables, Permit2Payments, IPancakeV3S
bytes calldata path,
address payer
) internal {
maxAmountInCached = amountInMaximum;
MaxInputAmount.set(amountInMaximum);
(int256 amount0Delta, int256 amount1Delta, bool zeroForOne) =
_swap(-amountOut.toInt256(), recipient, path, payer, false);

uint256 amountOutReceived = zeroForOne ? uint256(-amount1Delta) : uint256(-amount0Delta);

if (amountOutReceived != amountOut) revert V3InvalidAmountOut();

maxAmountInCached = DEFAULT_MAX_AMOUNT_IN;
MaxInputAmount.set(0);
}

/// @dev Performs a single swap for both exactIn and exactOut
Expand Down
15 changes: 15 additions & 0 deletions test/UniversalRouter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ contract UniversalRouterTest is Test, GasSnapshot, Permit2SignatureHelpers, Depl
v4BinPositionManager: address(0)
});
router = new UniversalRouter(params);
assertEq(router.owner(), address(this));

router = new UniversalRouter(params);
erc20 = new MockERC20();
Expand All @@ -81,6 +82,20 @@ contract UniversalRouterTest is Test, GasSnapshot, Permit2SignatureHelpers, Depl
emit log_uint(bytecodeSize);
}

function test_Owner_TransferOwnership() public {
address alice = makeAddr("alice");
assertEq(router.owner(), address(this));

router.transferOwnership(alice);
assertEq(router.owner(), address(this));
assertEq(router.pendingOwner(), alice);

vm.prank(alice);
router.acceptOwnership();
assertEq(router.owner(), alice);
assertEq(router.pendingOwner(), address(0));
}

function test_sweep_token() public {
bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.SWEEP)));
bytes[] memory inputs = new bytes[](1);
Expand Down
24 changes: 24 additions & 0 deletions test/V3ToV4Migration.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,30 @@ contract V3ToV4MigrationTest is BasePancakeSwapV4, OldVersionHelper, BinLiquidit
assertEq(clPositionManager.ownerOf(1), alice);
}

function test_v4CLPositionmanager_InvalidActions() public {
uint256[] memory blackListedActions = new uint256[](3);
blackListedActions[0] = Actions.CL_INCREASE_LIQUIDITY;
blackListedActions[1] = Actions.CL_DECREASE_LIQUIDITY;
blackListedActions[2] = Actions.CL_BURN_POSITION;

for (uint256 i = 0; i < blackListedActions.length; i++) {
Plan memory planner = Planner.init();
planner.add(Actions.CL_INCREASE_LIQUIDITY, abi.encode(10));

bytes memory commands = abi.encodePacked(bytes1(uint8(Commands.V4_CL_POSITION_CALL)));
bytes[] memory inputs = new bytes[](1);
inputs[0] = abi.encodePacked(
IPositionManager.modifyLiquidities.selector, abi.encode(planner.encode(), block.timestamp)
);

vm.expectRevert(
abi.encodeWithSelector(Dispatcher.InvalidPositionManagerAction.selector, Actions.CL_INCREASE_LIQUIDITY)
);
vm.prank(alice);
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_v4BinPositionmanager_BinAddLiquidity() public {
Expand Down
Loading
Loading