diff --git a/.forge-snapshots/PositionManager_burn_empty.snap b/.forge-snapshots/PositionManager_burn_empty.snap index 05561cbe..778d7a4b 100644 --- a/.forge-snapshots/PositionManager_burn_empty.snap +++ b/.forge-snapshots/PositionManager_burn_empty.snap @@ -1 +1 @@ -46767 \ No newline at end of file +47040 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_empty_native.snap b/.forge-snapshots/PositionManager_burn_empty_native.snap index bd63bab0..026b0df0 100644 --- a/.forge-snapshots/PositionManager_burn_empty_native.snap +++ b/.forge-snapshots/PositionManager_burn_empty_native.snap @@ -1 +1 @@ -46584 \ No newline at end of file +46858 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty.snap b/.forge-snapshots/PositionManager_burn_nonEmpty.snap index 6bc4c749..baec174a 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty.snap @@ -1 +1 @@ -129412 \ No newline at end of file +129816 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap index 256a4450..66a4ff4d 100644 --- a/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap +++ b/.forge-snapshots/PositionManager_burn_nonEmpty_native.snap @@ -1 +1 @@ -122334 \ No newline at end of file +122737 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect.snap b/.forge-snapshots/PositionManager_collect.snap index 35c98376..7c9ee97a 100644 --- a/.forge-snapshots/PositionManager_collect.snap +++ b/.forge-snapshots/PositionManager_collect.snap @@ -1 +1 @@ -149447 \ No newline at end of file +149962 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_native.snap b/.forge-snapshots/PositionManager_collect_native.snap index 22674b5b..45903fe4 100644 --- a/.forge-snapshots/PositionManager_collect_native.snap +++ b/.forge-snapshots/PositionManager_collect_native.snap @@ -1 +1 @@ -140599 \ No newline at end of file +141114 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_collect_sameRange.snap b/.forge-snapshots/PositionManager_collect_sameRange.snap index 35c98376..7c9ee97a 100644 --- a/.forge-snapshots/PositionManager_collect_sameRange.snap +++ b/.forge-snapshots/PositionManager_collect_sameRange.snap @@ -1 +1 @@ -149447 \ No newline at end of file +149962 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity.snap b/.forge-snapshots/PositionManager_decreaseLiquidity.snap index c6c2c4fa..7d856031 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity.snap @@ -1 +1 @@ -115032 \ No newline at end of file +115505 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap index a00089a9..729b85fa 100644 --- a/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_decreaseLiquidity_native.snap @@ -1 +1 @@ -107954 \ No newline at end of file +108366 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap index f9bd3ffd..2b82296b 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty.snap @@ -1 +1 @@ -133172 \ No newline at end of file +133849 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap index 59f0e691..16c031f6 100644 --- a/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap +++ b/.forge-snapshots/PositionManager_decrease_burnEmpty_native.snap @@ -1 +1 @@ -125911 \ No newline at end of file +126588 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap index 231e4fce..200c6a98 100644 --- a/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap +++ b/.forge-snapshots/PositionManager_decrease_sameRange_allLiquidity.snap @@ -1 +1 @@ -127706 \ No newline at end of file +128221 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap index 5c321a26..26723396 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_erc20.snap @@ -1 +1 @@ -150926 \ No newline at end of file +152100 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap index f28c353a..35295eaf 100644 --- a/.forge-snapshots/PositionManager_increaseLiquidity_native.snap +++ b/.forge-snapshots/PositionManager_increaseLiquidity_native.snap @@ -1 +1 @@ -132726 \ No newline at end of file +133900 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap index 0b3f25d5..0093c249 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExactUnclaimedFees.snap @@ -1 +1 @@ -133653 \ No newline at end of file +134581 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap index 59b6bd97..493e858f 100644 --- a/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap +++ b/.forge-snapshots/PositionManager_increase_autocompoundExcessFeesCredit.snap @@ -1 +1 @@ -169809 \ No newline at end of file +170737 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint.snap b/.forge-snapshots/PositionManager_mint.snap index a11e91e9..7e0f6688 100644 --- a/.forge-snapshots/PositionManager_mint.snap +++ b/.forge-snapshots/PositionManager_mint.snap @@ -1 +1 @@ -370961 \ No newline at end of file +372012 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_native.snap b/.forge-snapshots/PositionManager_mint_native.snap index ee67f756..e708094b 100644 --- a/.forge-snapshots/PositionManager_mint_native.snap +++ b/.forge-snapshots/PositionManager_mint_native.snap @@ -1 +1 @@ -335661 \ No newline at end of file +336712 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap index d296be79..7dc935fe 100644 --- a/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap +++ b/.forge-snapshots/PositionManager_mint_nativeWithSweep.snap @@ -1 +1 @@ -344170 \ No newline at end of file +345221 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap index 42074607..90fd8fec 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickLower.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickLower.snap @@ -1 +1 @@ -313643 \ No newline at end of file +314694 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap index 35f9fdd1..9b6845af 100644 --- a/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap +++ b/.forge-snapshots/PositionManager_mint_onSameTickUpper.snap @@ -1 +1 @@ -314285 \ No newline at end of file +315336 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_sameRange.snap b/.forge-snapshots/PositionManager_mint_sameRange.snap index 95a3e729..597983b5 100644 --- a/.forge-snapshots/PositionManager_mint_sameRange.snap +++ b/.forge-snapshots/PositionManager_mint_sameRange.snap @@ -1 +1 @@ -239867 \ No newline at end of file +240918 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap index 5c066bfe..bf4916f2 100644 --- a/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap +++ b/.forge-snapshots/PositionManager_mint_settleWithBalance_sweep.snap @@ -1 +1 @@ -368863 \ No newline at end of file +369926 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap index af506196..af4a8aa0 100644 --- a/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap +++ b/.forge-snapshots/PositionManager_mint_warmedPool_differentRange.snap @@ -1 +1 @@ -319661 \ No newline at end of file +320712 \ No newline at end of file diff --git a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap index b8cc5c06..28856ebd 100644 --- a/.forge-snapshots/PositionManager_multicall_initialize_mint.snap +++ b/.forge-snapshots/PositionManager_multicall_initialize_mint.snap @@ -1 +1 @@ -415324 \ No newline at end of file +416388 \ No newline at end of file diff --git a/src/PositionManager.sol b/src/PositionManager.sol index 6719354a..28432492 100644 --- a/src/PositionManager.sol +++ b/src/PositionManager.sol @@ -25,6 +25,7 @@ import {PositionConfig, PositionConfigLibrary} from "./libraries/PositionConfig. import {BaseActionsRouter} from "./base/BaseActionsRouter.sol"; import {Actions} from "./libraries/Actions.sol"; import {CalldataDecoder} from "./libraries/CalldataDecoder.sol"; +import {SlippageCheckLibrary} from "./libraries/SlippageCheck.sol"; contract PositionManager is IPositionManager, @@ -43,6 +44,7 @@ contract PositionManager is using TransientStateLibrary for IPoolManager; using SafeCast for uint256; using CalldataDecoder for bytes; + using SlippageCheckLibrary for BalanceDelta; /// @dev The ID of the next token that will be minted. Skips 0 uint256 public nextTokenId = 1; @@ -77,24 +79,48 @@ contract PositionManager is function _handleAction(uint256 action, bytes calldata params) internal virtual override { if (action == Actions.INCREASE_LIQUIDITY) { - (uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) = - params.decodeModifyLiquidityParams(); - _increase(tokenId, config, liquidity, hookData); + ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + bytes calldata hookData + ) = params.decodeModifyLiquidityParams(); + _increase(tokenId, config, liquidity, amount0Max, amount1Max, hookData); } else if (action == Actions.DECREASE_LIQUIDITY) { - (uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) = - params.decodeModifyLiquidityParams(); - _decrease(tokenId, config, liquidity, hookData); + ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) = params.decodeModifyLiquidityParams(); + _decrease(tokenId, config, liquidity, amount0Min, amount1Min, hookData); } else if (action == Actions.MINT_POSITION) { - (PositionConfig calldata config, uint256 liquidity, address owner, bytes calldata hookData) = - params.decodeMintParams(); - _mint(config, liquidity, owner, hookData); + ( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) = params.decodeMintParams(); + _mint(config, liquidity, amount0Max, amount1Max, owner, hookData); } else if (action == Actions.CLOSE_CURRENCY) { Currency currency = params.decodeCurrency(); _close(currency); } else if (action == Actions.BURN_POSITION) { // Will automatically decrease liquidity to 0 if the position is not already empty. - (uint256 tokenId, PositionConfig calldata config, bytes calldata hookData) = params.decodeBurnParams(); - _burn(tokenId, config, hookData); + ( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) = params.decodeBurnParams(); + _burn(tokenId, config, amount0Min, amount1Min, hookData); } else if (action == Actions.SETTLE_WITH_BALANCE) { Currency currency = params.decodeCurrency(); _settleWithBalance(currency); @@ -111,28 +137,45 @@ contract PositionManager is } /// @dev Calling increase with 0 liquidity will credit the caller with any underlying fees of the position - function _increase(uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) - internal - { + function _increase( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + bytes calldata hookData + ) internal { if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); // Note: The tokenId is used as the salt for this position, so every minted position has unique storage in the pool manager. BalanceDelta liquidityDelta = _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData); + liquidityDelta.validateMaxInNegative(amount0Max, amount1Max); } /// @dev Calling decrease with 0 liquidity will credit the caller with any underlying fees of the position - function _decrease(uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) - internal - { + function _decrease( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) internal { if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender()); if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); // Note: the tokenId is used as the salt. BalanceDelta liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData); + liquidityDelta.validateMinOut(amount0Min, amount1Min); } - function _mint(PositionConfig calldata config, uint256 liquidity, address owner, bytes calldata hookData) - internal - { + function _mint( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) internal { // mint receipt token uint256 tokenId; // tokenId is assigned to current nextTokenId before incrementing it @@ -143,7 +186,7 @@ contract PositionManager is // _beforeModify is not called here because the tokenId is newly minted BalanceDelta liquidityDelta = _modifyLiquidity(config, liquidity.toInt256(), bytes32(tokenId), hookData); - + liquidityDelta.validateMaxIn(amount0Max, amount1Max); positionConfigs[tokenId] = config.toId(); } @@ -168,7 +211,13 @@ contract PositionManager is } /// @dev this is overloaded with ERC721Permit._burn - function _burn(uint256 tokenId, PositionConfig calldata config, bytes calldata hookData) internal { + function _burn( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) internal { if (!_isApprovedOrOwner(_msgSender(), tokenId)) revert NotApproved(_msgSender()); if (positionConfigs[tokenId] != config.toId()) revert IncorrectPositionConfigForTokenId(tokenId); uint256 liquidity = uint256(_getPositionLiquidity(config, tokenId)); @@ -177,6 +226,7 @@ contract PositionManager is // Can only call modify if there is non zero liquidity. if (liquidity > 0) { liquidityDelta = _modifyLiquidity(config, -(liquidity.toInt256()), bytes32(tokenId), hookData); + liquidityDelta.validateMinOut(amount0Min, amount1Min); } delete positionConfigs[tokenId]; diff --git a/src/libraries/CalldataDecoder.sol b/src/libraries/CalldataDecoder.sol index 11d24551..e8dcbcfa 100644 --- a/src/libraries/CalldataDecoder.sol +++ b/src/libraries/CalldataDecoder.sol @@ -43,45 +43,71 @@ library CalldataDecoder { } } - /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, uint256, bytes)) in calldata + /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, uint256, uint128, uint128, bytes)) in calldata function decodeModifyLiquidityParams(bytes calldata params) internal pure - returns (uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0, + uint128 amount1, + bytes calldata hookData + ) { assembly ("memory-safe") { tokenId := calldataload(params.offset) config := add(params.offset, 0x20) liquidity := calldataload(add(params.offset, 0x100)) + amount0 := calldataload(add(params.offset, 0x120)) + amount1 := calldataload(add(params.offset, 0x140)) } - hookData = params.toBytes(9); + hookData = params.toBytes(11); } - /// @dev equivalent to: abi.decode(params, (PositionConfig, uint256, address, bytes)) in calldata + /// @dev equivalent to: abi.decode(params, (PositionConfig, uint256, uint128, uint128, address, bytes)) in calldata function decodeMintParams(bytes calldata params) internal pure - returns (PositionConfig calldata config, uint256 liquidity, address owner, bytes calldata hookData) + returns ( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) { assembly ("memory-safe") { config := params.offset liquidity := calldataload(add(params.offset, 0xe0)) - owner := calldataload(add(params.offset, 0x100)) + amount0Max := calldataload(add(params.offset, 0x100)) + amount1Max := calldataload(add(params.offset, 0x120)) + owner := calldataload(add(params.offset, 0x140)) } - hookData = params.toBytes(9); + hookData = params.toBytes(11); } - /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, bytes)) in calldata + /// @dev equivalent to: abi.decode(params, (uint256, PositionConfig, uint128, uint128, bytes)) in calldata function decodeBurnParams(bytes calldata params) internal pure - returns (uint256 tokenId, PositionConfig calldata config, bytes calldata hookData) + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) { assembly ("memory-safe") { tokenId := calldataload(params.offset) config := add(params.offset, 0x20) + amount0Min := calldataload(add(params.offset, 0x100)) + amount1Min := calldataload(add(params.offset, 0x120)) } - hookData = params.toBytes(8); + hookData = params.toBytes(10); } /// @dev equivalent to: abi.decode(params, (Currency)) in calldata diff --git a/src/libraries/SlippageCheck.sol b/src/libraries/SlippageCheck.sol new file mode 100644 index 00000000..efeafca9 --- /dev/null +++ b/src/libraries/SlippageCheck.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +/// @title Slippage Check Library +/// @notice a library for checking if a delta exceeds a maximum ceiling or fails to meet a minimum floor +library SlippageCheckLibrary { + error MaximumAmountExceeded(); + error MinimumAmountInsufficient(); + + /// @notice Revert if one or both deltas does not meet a minimum output + /// @dev to be used when removing liquidity to guarantee a minimum output + function validateMinOut(BalanceDelta delta, uint128 amount0Min, uint128 amount1Min) internal pure { + if (uint128(delta.amount0()) < amount0Min || uint128(delta.amount1()) < amount1Min) { + revert MinimumAmountInsufficient(); + } + } + + /// @notice Revert if one or both deltas exceeds a maximum input + /// @dev to be used when minting liquidity to guarantee a maximum input + function validateMaxIn(BalanceDelta delta, uint128 amount0Max, uint128 amount1Max) internal pure { + if (uint128(-delta.amount0()) > amount0Max || uint128(-delta.amount1()) > amount1Max) { + revert MaximumAmountExceeded(); + } + } + + /// @notice Revert if one or both deltas exceeds a maximum input + /// @dev When increasing liquidity, delta can be positive when excess fees need to be collected + /// in those cases, slippage checks are not required + function validateMaxInNegative(BalanceDelta delta, uint128 amount0Max, uint128 amount1Max) internal pure { + if ( + delta.amount0() < 0 && amount0Max < uint128(-delta.amount0()) + || delta.amount1() < 0 && amount1Max < uint128(-delta.amount1()) + ) revert MaximumAmountExceeded(); + } +} diff --git a/test/libraries/CalldataDecoder.t.sol b/test/libraries/CalldataDecoder.t.sol index 1366bd3d..08fee4a4 100644 --- a/test/libraries/CalldataDecoder.t.sol +++ b/test/libraries/CalldataDecoder.t.sol @@ -17,41 +17,67 @@ contract CalldataDecoderTest is Test { uint256 _tokenId, PositionConfig calldata _config, uint256 _liquidity, + uint128 _amount0, + uint128 _amount1, bytes calldata _hookData ) public view { - bytes memory params = abi.encode(_tokenId, _config, _liquidity, _hookData); - (uint256 tokenId, PositionConfig memory config, uint256 liquidity, bytes memory hookData) = - decoder.decodeModifyLiquidityParams(params); + bytes memory params = abi.encode(_tokenId, _config, _liquidity, _amount0, _amount1, _hookData); + ( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidity, + uint128 amount0, + uint128 amount1, + bytes memory hookData + ) = decoder.decodeModifyLiquidityParams(params); assertEq(tokenId, _tokenId); assertEq(liquidity, _liquidity); + assertEq(amount0, _amount0); + assertEq(amount1, _amount1); assertEq(hookData, _hookData); _assertEq(_config, config); } - function test_fuzz_decodeBurnParams(uint256 _tokenId, PositionConfig calldata _config, bytes calldata _hookData) - public - view - { - bytes memory params = abi.encode(_tokenId, _config, _hookData); - (uint256 tokenId, PositionConfig memory config, bytes memory hookData) = decoder.decodeBurnParams(params); + function test_fuzz_decodeBurnParams( + uint256 _tokenId, + PositionConfig calldata _config, + uint128 _amount0Min, + uint128 _amount1Min, + bytes calldata _hookData + ) public view { + bytes memory params = abi.encode(_tokenId, _config, _amount0Min, _amount1Min, _hookData); + (uint256 tokenId, PositionConfig memory config, uint128 amount0Min, uint128 amount1Min, bytes memory hookData) = + decoder.decodeBurnParams(params); assertEq(tokenId, _tokenId); assertEq(hookData, _hookData); _assertEq(_config, config); + assertEq(amount0Min, _amount0Min); + assertEq(amount1Min, _amount1Min); } function test_fuzz_decodeMintParams( PositionConfig calldata _config, uint256 _liquidity, + uint128 _amount0Max, + uint128 _amount1Max, address _owner, bytes calldata _hookData ) public view { - bytes memory params = abi.encode(_config, _liquidity, _owner, _hookData); - (PositionConfig memory config, uint256 liquidity, address owner, bytes memory hookData) = - decoder.decodeMintParams(params); + bytes memory params = abi.encode(_config, _liquidity, _amount0Max, _amount1Max, _owner, _hookData); + ( + PositionConfig memory config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes memory hookData + ) = decoder.decodeMintParams(params); assertEq(liquidity, _liquidity); + assertEq(amount0Max, _amount0Max); + assertEq(amount1Max, _amount1Max); assertEq(owner, _owner); assertEq(hookData, _hookData); _assertEq(_config, config); diff --git a/test/mocks/MockCalldataDecoder.sol b/test/mocks/MockCalldataDecoder.sol index 88332553..5a5b1df8 100644 --- a/test/mocks/MockCalldataDecoder.sol +++ b/test/mocks/MockCalldataDecoder.sol @@ -12,7 +12,14 @@ contract MockCalldataDecoder { function decodeModifyLiquidityParams(bytes calldata params) external pure - returns (uint256 tokenId, PositionConfig calldata config, uint256 liquidity, bytes calldata hookData) + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0, + uint128 amount1, + bytes calldata hookData + ) { return params.decodeModifyLiquidityParams(); } @@ -20,7 +27,13 @@ contract MockCalldataDecoder { function decodeBurnParams(bytes calldata params) external pure - returns (uint256 tokenId, PositionConfig calldata config, bytes calldata hookData) + returns ( + uint256 tokenId, + PositionConfig calldata config, + uint128 amount0Min, + uint128 amount1Min, + bytes calldata hookData + ) { return params.decodeBurnParams(); } @@ -28,7 +41,14 @@ contract MockCalldataDecoder { function decodeMintParams(bytes calldata params) external pure - returns (PositionConfig calldata config, uint256 liquidity, address owner, bytes calldata hookData) + returns ( + PositionConfig calldata config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address owner, + bytes calldata hookData + ) { return params.decodeMintParams(); } diff --git a/test/position-managers/Execute.t.sol b/test/position-managers/Execute.t.sol index 4d7c5e15..d75f3ac7 100644 --- a/test/position-managers/Execute.t.sol +++ b/test/position-managers/Execute.t.sol @@ -91,8 +91,14 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { Plan memory planner = Planner.init(); - planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, ZERO_BYTES)); - planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd2, ZERO_BYTES)); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd2, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -113,8 +119,16 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(config, initialLiquidity, address(this), ZERO_BYTES)); - planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, ZERO_BYTES)); + planner.add( + Actions.MINT_POSITION, + abi.encode( + config, initialLiquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES + ) + ); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -151,8 +165,16 @@ contract ExecuteTest is Test, PosmTestSetup, LiquidityFuzzers { hook.clearDeltas(); // clear the delta so that we can check the net delta for BURN & MINT Plan memory planner = Planner.init(); - planner.add(Actions.BURN_POSITION, abi.encode(tokenId, config, ZERO_BYTES)); - planner.add(Actions.MINT_POSITION, abi.encode(newConfig, newLiquidity, address(this), ZERO_BYTES)); + planner.add( + Actions.BURN_POSITION, + abi.encode( + tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ) + ); + planner.add( + Actions.MINT_POSITION, + abi.encode(newConfig, newLiquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); diff --git a/test/position-managers/IncreaseLiquidity.t.sol b/test/position-managers/IncreaseLiquidity.t.sol index ef76e3cc..8d02b110 100644 --- a/test/position-managers/IncreaseLiquidity.t.sol +++ b/test/position-managers/IncreaseLiquidity.t.sol @@ -20,6 +20,7 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {SlippageCheckLibrary} from "../../src/libraries/SlippageCheck.sol"; import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {Planner, Plan} from "../shared/Planner.sol"; @@ -114,7 +115,9 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { // TODO: Can we make this easier to re-invest fees, so that you don't need to know the exact collect amount? Plan memory planner = Planner.init(); - planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityDelta, ZERO_BYTES)); + planner.add( + Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityDelta, 0 wei, 0 wei, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); vm.startPrank(alice); lpm.modifyLiquidities(calls, _deadline); @@ -369,11 +372,84 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { } } + function test_increaseLiquidity_slippage_revertAmount0() public { + // increasing liquidity with strict slippage parameters (amount0) will revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + // revert since amount0Max is too low + bytes memory calls = getIncreaseEncoded(tokenId, config, 100e18, 1 wei, type(uint128).max, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_increaseLiquidity_slippage_revertAmount1() public { + // increasing liquidity with strict slippage parameters (amount1) will revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + // revert since amount1Max is too low + bytes memory calls = getIncreaseEncoded(tokenId, config, 100e18, type(uint128).max, 1 wei, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_increaseLiquidity_slippage_exactDoesNotRevert() public { + // increasing liquidity with perfect slippage parameters does not revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + uint128 newLiquidity = 10e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + newLiquidity + ); + assertEq(amount0, amount1); // symmetric liquidity addition + uint128 slippage = uint128(amount0) + 1; + + bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta delta = getLastDelta(); + + // confirm that delta == slippage tolerance + assertEq(-delta.amount0(), int128(slippage)); + assertEq(-delta.amount1(), int128(slippage)); + } + + /// price movement from swaps will cause slippage reverts + function test_increaseLiquidity_slippage_revert_swap() public { + // increasing liquidity with perfect slippage parameters does not revert + uint256 tokenId = lpm.nextTokenId(); + mint(config, 100e18, address(this), ZERO_BYTES); + + uint128 newLiquidity = 10e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + newLiquidity + ); + assertEq(amount0, amount1); // symmetric liquidity addition + uint128 slippage = uint128(amount0) + 1; + + // swap to create slippage + swap(key, true, -10e18, ZERO_BYTES); + + bytes memory calls = getIncreaseEncoded(tokenId, config, newLiquidity, slippage, slippage, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + function test_mint_settleWithBalance() public { uint256 liquidityAlice = 3_000e18; Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(config, liquidityAlice, alice, ZERO_BYTES)); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, alice, ZERO_BYTES) + ); planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0)); planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1)); planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); @@ -422,7 +498,10 @@ contract IncreaseLiquidityTest is Test, PosmTestSetup, Fuzzers { // alice increases with the balance in the position manager Plan memory planner = Planner.init(); - planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityAlice, ZERO_BYTES)); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0)); planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1)); planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); diff --git a/test/position-managers/NativeToken.t.sol b/test/position-managers/NativeToken.t.sol index f8d23c54..b7a8e901 100644 --- a/test/position-managers/NativeToken.t.sol +++ b/test/position-managers/NativeToken.t.sol @@ -111,7 +111,10 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { uint256 tokenId = lpm.nextTokenId(); Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(config, liquidityToAdd, address(this), ZERO_BYTES)); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency0)); planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency1)); // sweep the excess eth @@ -310,7 +313,10 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { ); Plan memory planner = Planner.init(); - planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, ZERO_BYTES)); + planner.add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency0)); planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency1)); // sweep the excess eth diff --git a/test/position-managers/PositionManager.gas.t.sol b/test/position-managers/PositionManager.gas.t.sol index a6d9a1da..7f3373f0 100644 --- a/test/position-managers/PositionManager.gas.t.sol +++ b/test/position-managers/PositionManager.gas.t.sol @@ -70,8 +70,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { } function test_gas_mint() public { - Plan memory planner = - Planner.init().add(Actions.MINT_POSITION, abi.encode(config, 10_000 ether, address(this), ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); snapLastCall("PositionManager_mint"); @@ -84,8 +86,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff config, diff user. - Plan memory planner = - Planner.init().add(Actions.MINT_POSITION, abi.encode(config, 10_000 ether, address(alice), ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); @@ -99,8 +103,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff config, diff user. - Plan memory planner = - Planner.init().add(Actions.MINT_POSITION, abi.encode(config, 10_000 ether, address(alice), ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); @@ -114,8 +120,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { mint(bob_mint, 10_000 ether, address(bob), ZERO_BYTES); vm.stopPrank(); // Mint to a diff config, diff user. - Plan memory planner = - Planner.init().add(Actions.MINT_POSITION, abi.encode(config, 10_000 ether, address(alice), ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); @@ -126,8 +134,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = - Planner.init().add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, 10_000 ether, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -168,8 +178,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { tokensOwedAlice ); - Plan memory planner = - Planner.init().add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityDelta, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); vm.prank(alice); @@ -211,8 +223,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { halfTokensOwedAlice ); - Plan memory planner = - Planner.init().add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenIdAlice, config, liquidityDelta, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.INCREASE_LIQUIDITY, + abi.encode(tokenIdAlice, config, liquidityDelta, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); @@ -225,8 +239,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = - Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 10_000 ether, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -247,7 +263,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { }); Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(config, 100e18, address(this), ZERO_BYTES)); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); bytes memory actions = planner.finalizeModifyLiquidity(config.poolKey); calls[1] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); @@ -264,7 +283,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { donateRouter.donate(config.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); // Collect by calling decrease with 0. - Plan memory planner = Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 0, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 0, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -275,8 +297,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { function test_gas_sameRange_mint() public { mint(config, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = - Planner.init().add(Actions.MINT_POSITION, abi.encode(config, 10_001 ether, address(alice), ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.MINT_POSITION, + abi.encode(config, 10_001 ether, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(alice), ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); vm.prank(alice); lpm.modifyLiquidities(calls, _deadline); @@ -292,8 +316,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = - Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 10_000 ether, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -312,7 +338,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { // donate to create fee revenue donateRouter.donate(config.poolKey, 0.2e18, 0.2e18, ZERO_BYTES); - Plan memory planner = Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 0, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 0, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -323,7 +352,9 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = Planner.init().add(Actions.BURN_POSITION, abi.encode(tokenId, config, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, abi.encode(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -335,7 +366,9 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { mint(config, 10_000 ether, address(this), ZERO_BYTES); decreaseLiquidity(tokenId, config, 10_000 ether, ZERO_BYTES); - Plan memory planner = Planner.init().add(Actions.BURN_POSITION, abi.encode(tokenId, config, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, abi.encode(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); // There is no need to include CLOSE commands. bytes memory calls = planner.encode(); @@ -349,9 +382,13 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 tokenId = lpm.nextTokenId(); mint(config, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = - Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 10_000 ether, ZERO_BYTES)); - planner.add(Actions.BURN_POSITION, abi.encode(tokenId, config, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, config, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + planner.add( + Actions.BURN_POSITION, abi.encode(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); // We must include CLOSE commands. bytes memory calls = planner.finalizeModifyLiquidity(config.poolKey); @@ -382,7 +419,12 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 liquidityToAdd = 10_000 ether; Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(configNative, liquidityToAdd, address(this), ZERO_BYTES)); + planner.add( + Actions.MINT_POSITION, + abi.encode( + configNative, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES + ) + ); planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency0)); planner.add(Actions.CLOSE_CURRENCY, abi.encode(nativeKey.currency1)); planner.add(Actions.SWEEP, abi.encode(CurrencyLibrary.NATIVE, address(this))); @@ -441,7 +483,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 tokenId = lpm.nextTokenId(); mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = Planner.init().add(Actions.BURN_POSITION, abi.encode(tokenId, configNative, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, + abi.encode(tokenId, configNative, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); bytes memory calls = planner.finalizeModifyLiquidity(configNative.poolKey); lpm.modifyLiquidities(calls, _deadline); @@ -453,7 +498,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); decreaseLiquidity(tokenId, configNative, 10_000 ether, ZERO_BYTES); - Plan memory planner = Planner.init().add(Actions.BURN_POSITION, abi.encode(tokenId, configNative, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.BURN_POSITION, + abi.encode(tokenId, configNative, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); // There is no need to include CLOSE commands. bytes memory calls = planner.encode(); @@ -467,9 +515,11 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 tokenId = lpm.nextTokenId(); mintWithNative(SQRT_PRICE_1_1, configNative, 10_000 ether, address(this), ZERO_BYTES); - Plan memory planner = - Planner.init().add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, configNative, 10_000 ether, ZERO_BYTES)); - planner.add(Actions.BURN_POSITION, abi.encode(tokenId, configNative, ZERO_BYTES)); + Plan memory planner = Planner.init().add( + Actions.DECREASE_LIQUIDITY, + abi.encode(tokenId, configNative, 10_000 ether, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, ZERO_BYTES) + ); + planner.add(Actions.BURN_POSITION, abi.encode(tokenId, configNative, 0 wei, 0 wei, ZERO_BYTES)); // We must include CLOSE commands. bytes memory calls = planner.finalizeModifyLiquidity(configNative.poolKey); @@ -481,7 +531,10 @@ contract PosMGasTest is Test, PosmTestSetup, GasSnapshot { uint256 liquidityAlice = 3_000e18; Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(config, liquidityAlice, alice, ZERO_BYTES)); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, liquidityAlice, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, alice, ZERO_BYTES) + ); planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency0)); planner.add(Actions.SETTLE_WITH_BALANCE, abi.encode(currency1)); planner.add(Actions.SWEEP, abi.encode(currency0, address(this))); diff --git a/test/position-managers/PositionManager.multicall.t.sol b/test/position-managers/PositionManager.multicall.t.sol index 5dde4e17..da8774fd 100644 --- a/test/position-managers/PositionManager.multicall.t.sol +++ b/test/position-managers/PositionManager.multicall.t.sol @@ -55,7 +55,10 @@ contract PositionManagerMulticallTest is Test, PosmTestSetup, LiquidityFuzzers { }); Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(config, 100e18, address(this), ZERO_BYTES)); + planner.add( + Actions.MINT_POSITION, + abi.encode(config, 100e18, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES) + ); bytes memory actions = planner.finalizeModifyLiquidity(config.poolKey); calls[1] = abi.encodeWithSelector(IPositionManager.modifyLiquidities.selector, actions, _deadline); diff --git a/test/position-managers/PositionManager.t.sol b/test/position-managers/PositionManager.t.sol index 513e9710..cbfc80b2 100644 --- a/test/position-managers/PositionManager.t.sol +++ b/test/position-managers/PositionManager.t.sol @@ -23,6 +23,7 @@ import {IPositionManager} from "../../src/interfaces/IPositionManager.sol"; import {Actions} from "../../src/libraries/Actions.sol"; import {PositionManager} from "../../src/PositionManager.sol"; import {PositionConfig} from "../../src/libraries/PositionConfig.sol"; +import {SlippageCheckLibrary} from "../../src/libraries/SlippageCheck.sol"; import {BaseActionsRouter} from "../../src/base/BaseActionsRouter.sol"; import {LiquidityFuzzers} from "../shared/fuzz/LiquidityFuzzers.sol"; @@ -180,6 +181,65 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertEq(currency1.balanceOf(alice), balance1BeforeAlice); } + function test_mint_slippage_revertAmount0() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + bytes memory calls = getMintEncoded(config, 1e18, 1 wei, MAX_SLIPPAGE_INCREASE, address(this), ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_revertAmount1() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + bytes memory calls = getMintEncoded(config, 1e18, MAX_SLIPPAGE_INCREASE, 1 wei, address(this), ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_mint_slippage_exactDoesNotRevert() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 1e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + assertEq(amount0, amount1); // symmetric liquidity + uint128 slippage = uint128(amount0) + 1; + + bytes memory calls = getMintEncoded(config, liquidity, slippage, slippage, address(this), ZERO_BYTES); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta delta = getLastDelta(); + assertEq(uint256(int256(-delta.amount0())), slippage); + assertEq(uint256(int256(-delta.amount1())), slippage); + } + + function test_mint_slippage_revert_swap() public { + // swapping will cause a slippage revert + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + + uint256 liquidity = 100e18; + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(config.tickLower), + TickMath.getSqrtPriceAtTick(config.tickUpper), + uint128(liquidity) + ); + assertEq(amount0, amount1); // symmetric liquidity + uint128 slippage = uint128(amount0) + 1; + + bytes memory calls = getMintEncoded(config, liquidity, slippage, slippage, address(this), ZERO_BYTES); + + // swap to move the price and cause a slippage revert + swap(key, true, -1e18, ZERO_BYTES); + + vm.expectRevert(SlippageCheckLibrary.MaximumAmountExceeded.selector); + lpm.modifyLiquidities(calls, _deadline); + } + function test_fuzz_burn_emptyPosition(IPoolManager.ModifyLiquidityParams memory params) public { uint256 balance0Start = currency0.balanceOfSelf(); uint256 balance1Start = currency1.balanceOfSelf(); @@ -279,6 +339,65 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertApproxEqAbs(currency1.balanceOfSelf(), balance1Start, 1 wei); } + function test_burn_slippage_revertAmount0() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = + getBurnEncoded(tokenId, config, uint128(-delta.amount0()) + 1 wei, MIN_SLIPPAGE_DECREASE, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_burn_slippage_revertAmount1() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = + getBurnEncoded(tokenId, config, MIN_SLIPPAGE_DECREASE, uint128(-delta.amount1()) + 1 wei, ZERO_BYTES); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_burn_slippage_exactDoesNotRevert() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + // TODO: why does burning a newly minted position return original delta - 1 wei? + bytes memory calls = getBurnEncoded( + tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta burnDelta = getLastDelta(); + + assertApproxEqAbs(-delta.amount0(), burnDelta.amount0(), 1 wei); + assertApproxEqAbs(-delta.amount1(), burnDelta.amount1(), 1 wei); + } + + function test_burn_slippage_revert_swap() public { + // swapping will cause a slippage revert + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getBurnEncoded( + tokenId, config, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + + // swap to move the price and cause a slippage revert + swap(key, true, -1e18, ZERO_BYTES); + + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + function test_fuzz_decreaseLiquidity( IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta @@ -343,6 +462,68 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { assertApproxEqAbs(currency1.balanceOfSelf() - balance1Before, amount1 + feeRevenue1, 1 wei); } + function test_decreaseLiquidity_slippage_revertAmount0() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, uint128(-delta.amount0()) + 1 wei, MIN_SLIPPAGE_DECREASE, ZERO_BYTES + ); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_decreaseLiquidity_slippage_revertAmount1() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, MIN_SLIPPAGE_DECREASE, uint128(-delta.amount1()) + 1 wei, ZERO_BYTES + ); + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + + function test_decreaseLiquidity_slippage_exactDoesNotRevert() public { + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + // TODO: why does decreasing a newly minted position return original delta - 1 wei? + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + lpm.modifyLiquidities(calls, _deadline); + BalanceDelta decreaseDelta = getLastDelta(); + + // TODO: why does decreasing a newly minted position return original delta - 1 wei? + assertApproxEqAbs(-delta.amount0(), decreaseDelta.amount0(), 1 wei); + assertApproxEqAbs(-delta.amount1(), decreaseDelta.amount1(), 1 wei); + } + + function test_decreaseLiquidity_slippage_revert_swap() public { + // swapping will cause a slippage revert + PositionConfig memory config = PositionConfig({poolKey: key, tickLower: -120, tickUpper: 120}); + uint256 tokenId = lpm.nextTokenId(); + mint(config, 1e18, address(this), ZERO_BYTES); + BalanceDelta delta = getLastDelta(); + + bytes memory calls = getDecreaseEncoded( + tokenId, config, 1e18, uint128(-delta.amount0()) - 1 wei, uint128(-delta.amount1()) - 1 wei, ZERO_BYTES + ); + + // swap to move the price and cause a slippage revert + swap(key, true, -1e18, ZERO_BYTES); + + vm.expectRevert(SlippageCheckLibrary.MinimumAmountInsufficient.selector); + lpm.modifyLiquidities(calls, _deadline); + } + function test_fuzz_decreaseLiquidity_assertCollectedBalance( IPoolManager.ModifyLiquidityParams memory params, uint256 decreaseLiquidityDelta @@ -530,7 +711,8 @@ contract PositionManagerTest is Test, PosmTestSetup, LiquidityFuzzers { } function test_fuzz_initialize(uint160 sqrtPrice, uint24 fee) public { - sqrtPrice = uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE)); + sqrtPrice = + uint160(bound(sqrtPrice, TickMath.MIN_SQRT_PRICE, TickMath.MAX_SQRT_PRICE_MINUS_MIN_SQRT_PRICE_MINUS_ONE)); fee = uint24(bound(fee, 0, LPFeeLibrary.MAX_LP_FEE)); key = PoolKey({currency0: currency0, currency1: currency1, fee: fee, tickSpacing: 10, hooks: IHooks(address(0))}); diff --git a/test/shared/LiquidityOperations.sol b/test/shared/LiquidityOperations.sol index 433fa711..a4623092 100644 --- a/test/shared/LiquidityOperations.sol +++ b/test/shared/LiquidityOperations.sol @@ -22,6 +22,9 @@ abstract contract LiquidityOperations is CommonBase { uint256 _deadline = block.timestamp + 1; + uint128 constant MAX_SLIPPAGE_INCREASE = type(uint128).max; + uint128 constant MIN_SLIPPAGE_DECREASE = 0 wei; + function mint(PositionConfig memory config, uint256 liquidity, address recipient, bytes memory hookData) internal { bytes memory calls = getMintEncoded(config, liquidity, recipient, hookData); lpm.modifyLiquidities(calls, _deadline); @@ -84,8 +87,19 @@ abstract contract LiquidityOperations is CommonBase { pure returns (bytes memory) { + return getMintEncoded(config, liquidity, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, recipient, hookData); + } + + function getMintEncoded( + PositionConfig memory config, + uint256 liquidity, + uint128 amount0Max, + uint128 amount1Max, + address recipient, + bytes memory hookData + ) internal pure returns (bytes memory) { Plan memory planner = Planner.init(); - planner.add(Actions.MINT_POSITION, abi.encode(config, liquidity, recipient, hookData)); + planner.add(Actions.MINT_POSITION, abi.encode(config, liquidity, amount0Max, amount1Max, recipient, hookData)); return planner.finalizeModifyLiquidity(config.poolKey); } @@ -95,9 +109,24 @@ abstract contract LiquidityOperations is CommonBase { PositionConfig memory config, uint256 liquidityToAdd, bytes memory hookData + ) internal pure returns (bytes memory) { + // max slippage + return + getIncreaseEncoded(tokenId, config, liquidityToAdd, MAX_SLIPPAGE_INCREASE, MAX_SLIPPAGE_INCREASE, hookData); + } + + function getIncreaseEncoded( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToAdd, + uint128 amount0Max, + uint128 amount1Max, + bytes memory hookData ) internal pure returns (bytes memory) { Plan memory planner = Planner.init(); - planner.add(Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, hookData)); + planner.add( + Actions.INCREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToAdd, amount0Max, amount1Max, hookData) + ); return planner.finalizeModifyLiquidity(config.poolKey); } @@ -106,9 +135,24 @@ abstract contract LiquidityOperations is CommonBase { PositionConfig memory config, uint256 liquidityToRemove, bytes memory hookData + ) internal pure returns (bytes memory) { + return getDecreaseEncoded( + tokenId, config, liquidityToRemove, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, hookData + ); + } + + function getDecreaseEncoded( + uint256 tokenId, + PositionConfig memory config, + uint256 liquidityToRemove, + uint128 amount0Min, + uint128 amount1Min, + bytes memory hookData ) internal pure returns (bytes memory) { Plan memory planner = Planner.init(); - planner.add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToRemove, hookData)); + planner.add( + Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, liquidityToRemove, amount0Min, amount1Min, hookData) + ); return planner.finalizeModifyLiquidity(config.poolKey); } @@ -117,8 +161,18 @@ abstract contract LiquidityOperations is CommonBase { pure returns (bytes memory) { + return getCollectEncoded(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, hookData); + } + + function getCollectEncoded( + uint256 tokenId, + PositionConfig memory config, + uint128 amount0Min, + uint128 amount1Min, + bytes memory hookData + ) internal pure returns (bytes memory) { Plan memory planner = Planner.init(); - planner.add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 0, hookData)); + planner.add(Actions.DECREASE_LIQUIDITY, abi.encode(tokenId, config, 0, amount0Min, amount1Min, hookData)); return planner.finalizeModifyLiquidity(config.poolKey); } @@ -127,8 +181,18 @@ abstract contract LiquidityOperations is CommonBase { pure returns (bytes memory) { + return getBurnEncoded(tokenId, config, MIN_SLIPPAGE_DECREASE, MIN_SLIPPAGE_DECREASE, hookData); + } + + function getBurnEncoded( + uint256 tokenId, + PositionConfig memory config, + uint128 amount0Min, + uint128 amount1Min, + bytes memory hookData + ) internal pure returns (bytes memory) { Plan memory planner = Planner.init(); - planner.add(Actions.BURN_POSITION, abi.encode(tokenId, config, hookData)); + planner.add(Actions.BURN_POSITION, abi.encode(tokenId, config, amount0Min, amount1Min, hookData)); // Close needed on burn in case there is liquidity left in the position. return planner.finalizeModifyLiquidity(config.poolKey); } diff --git a/test/shared/fuzz/LiquidityFuzzers.sol b/test/shared/fuzz/LiquidityFuzzers.sol index 025d3c9f..5bbfbc73 100644 --- a/test/shared/fuzz/LiquidityFuzzers.sol +++ b/test/shared/fuzz/LiquidityFuzzers.sol @@ -27,8 +27,17 @@ contract LiquidityFuzzers is Fuzzers { PositionConfig memory config = PositionConfig({poolKey: key, tickLower: params.tickLower, tickUpper: params.tickUpper}); + uint128 MAX_SLIPPAGE_INCREASE = type(uint128).max; Plan memory planner = Planner.init().add( - Actions.MINT_POSITION, abi.encode(config, uint256(params.liquidityDelta), recipient, hookData) + Actions.MINT_POSITION, + abi.encode( + config, + uint256(params.liquidityDelta), + MAX_SLIPPAGE_INCREASE, + MAX_SLIPPAGE_INCREASE, + recipient, + hookData + ) ); uint256 tokenId = lpm.nextTokenId();