diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap
index 2fd85a1..35177eb 100644
--- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap
+++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2IncludingInit.snap
@@ -1 +1 @@
-736974
\ No newline at end of file
+737466
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap
index 1c78123..37ca7ef 100644
--- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap
+++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutInit.snap
@@ -1 +1 @@
-693861
\ No newline at end of file
+694305
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap
index 577725f..2af097d 100644
--- a/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap
+++ b/.forge-snapshots/CLMigratorFromPancakeswapV2Test#testMigrateFromV2WithoutNativeToken.snap
@@ -1 +1 @@
-738290
\ No newline at end of file
+738633
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap
index ca570ab..934ebc5 100644
--- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap
+++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3IncludingInit.snap
@@ -1 +1 @@
-793398
\ No newline at end of file
+793890
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap
index 01facce..aa1b374 100644
--- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap
+++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutInit.snap
@@ -1 +1 @@
-752832
\ No newline at end of file
+753276
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap
index ffc5693..481433c 100644
--- a/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap
+++ b/.forge-snapshots/CLMigratorFromPancakeswapV3Test#testMigrateFromV3WithoutNativeToken.snap
@@ -1 +1 @@
-794750
\ No newline at end of file
+795093
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap
index 26cffde..d5c9032 100644
--- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap
+++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2IncludingInit.snap
@@ -1 +1 @@
-736986
\ No newline at end of file
+737478
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap
index f8e8bf1..6504093 100644
--- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap
+++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutInit.snap
@@ -1 +1 @@
-693873
\ No newline at end of file
+694317
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap
index 7ab44b7..0929190 100644
--- a/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap
+++ b/.forge-snapshots/CLMigratorFromUniswapV2Test#testMigrateFromV2WithoutNativeToken.snap
@@ -1 +1 @@
-738287
\ No newline at end of file
+738630
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap
index 473137d..9409d74 100644
--- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap
+++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3IncludingInit.snap
@@ -1 +1 @@
-791380
\ No newline at end of file
+791872
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap
index 79eb1ec..fe5745b 100644
--- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap
+++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutInit.snap
@@ -1 +1 @@
-750814
\ No newline at end of file
+751258
\ No newline at end of file
diff --git a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap
index 0a4c561..865682c 100644
--- a/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap
+++ b/.forge-snapshots/CLMigratorFromUniswapV3Test#testMigrateFromV3WithoutNativeToken.snap
@@ -1 +1 @@
-792728
\ No newline at end of file
+793071
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#BatchMintAndIncreaseLiquidity.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#BatchMintAndIncreaseLiquidity.snap
new file mode 100644
index 0000000..9fd3f90
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#BatchMintAndIncreaseLiquidity.snap
@@ -0,0 +1 @@
+688065
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#batchMintIncreaseAndDecreaseLiquidity.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#batchMintIncreaseAndDecreaseLiquidity.snap
new file mode 100644
index 0000000..0832cc0
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#batchMintIncreaseAndDecreaseLiquidity.snap
@@ -0,0 +1 @@
+722012
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#collect.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#collect.snap
new file mode 100644
index 0000000..c62eea8
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#collect.snap
@@ -0,0 +1 @@
+286391
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#collectWithoutCloseCurrency.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#collectWithoutCloseCurrency.snap
new file mode 100644
index 0000000..56e3c71
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#collectWithoutCloseCurrency.snap
@@ -0,0 +1 @@
+271112
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#decreaseLiquidity.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#decreaseLiquidity.snap
new file mode 100644
index 0000000..ad085d6
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#decreaseLiquidity.snap
@@ -0,0 +1 @@
+180614
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#decreaseLiquidityWithoutCloseCurrency.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#decreaseLiquidityWithoutCloseCurrency.snap
new file mode 100644
index 0000000..04ba4b9
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#decreaseLiquidityWithoutCloseCurrency.snap
@@ -0,0 +1 @@
+165586
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#increaseLiquidity.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#increaseLiquidity.snap
new file mode 100644
index 0000000..7ff1342
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#increaseLiquidity.snap
@@ -0,0 +1 @@
+216217
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#increaseLiquidityWithoutCloseCurrency.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#increaseLiquidityWithoutCloseCurrency.snap
new file mode 100644
index 0000000..9740e0c
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#increaseLiquidityWithoutCloseCurrency.snap
@@ -0,0 +1 @@
+201183
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#mint.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#mint.snap
new file mode 100644
index 0000000..500059b
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#mint.snap
@@ -0,0 +1 @@
+623159
\ No newline at end of file
diff --git a/.forge-snapshots/NonFungiblePositionManagerBatch#mintWithoutCloseCurrency.snap b/.forge-snapshots/NonFungiblePositionManagerBatch#mintWithoutCloseCurrency.snap
new file mode 100644
index 0000000..e6770bc
--- /dev/null
+++ b/.forge-snapshots/NonFungiblePositionManagerBatch#mintWithoutCloseCurrency.snap
@@ -0,0 +1 @@
+608132
\ No newline at end of file
diff --git a/.forge-snapshots/NonfungiblePositionManager#collect.snap b/.forge-snapshots/NonfungiblePositionManager#collect.snap
index d63c490..fcf5a9c 100644
--- a/.forge-snapshots/NonfungiblePositionManager#collect.snap
+++ b/.forge-snapshots/NonfungiblePositionManager#collect.snap
@@ -1 +1 @@
-266620
\ No newline at end of file
+267555
\ No newline at end of file
diff --git a/.forge-snapshots/NonfungiblePositionManager#decreaseLiquidity.snap b/.forge-snapshots/NonfungiblePositionManager#decreaseLiquidity.snap
index be20379..4028183 100644
--- a/.forge-snapshots/NonfungiblePositionManager#decreaseLiquidity.snap
+++ b/.forge-snapshots/NonfungiblePositionManager#decreaseLiquidity.snap
@@ -1 +1 @@
-160400
\ No newline at end of file
+160950
\ No newline at end of file
diff --git a/.forge-snapshots/NonfungiblePositionManager#increaseLiquidity.snap b/.forge-snapshots/NonfungiblePositionManager#increaseLiquidity.snap
index 4b9675c..ad84920 100644
--- a/.forge-snapshots/NonfungiblePositionManager#increaseLiquidity.snap
+++ b/.forge-snapshots/NonfungiblePositionManager#increaseLiquidity.snap
@@ -1 +1 @@
-193953
\ No newline at end of file
+194192
\ No newline at end of file
diff --git a/.forge-snapshots/NonfungiblePositionManager#mint.snap b/.forge-snapshots/NonfungiblePositionManager#mint.snap
index a4e0034..de0d0d3 100644
--- a/.forge-snapshots/NonfungiblePositionManager#mint.snap
+++ b/.forge-snapshots/NonfungiblePositionManager#mint.snap
@@ -1 +1 @@
-606838
\ No newline at end of file
+607181
\ No newline at end of file
diff --git a/.forge-snapshots/NonfungiblePositionManager#multicallMintAndIncreaseLiquidity.snap b/.forge-snapshots/NonfungiblePositionManager#multicallMintAndIncreaseLiquidity.snap
new file mode 100644
index 0000000..f034591
--- /dev/null
+++ b/.forge-snapshots/NonfungiblePositionManager#multicallMintAndIncreaseLiquidity.snap
@@ -0,0 +1 @@
+696986
\ No newline at end of file
diff --git a/.forge-snapshots/NonfungiblePositionManager#multicallMintIncreaseAndDecreaseLiquidity.snap b/.forge-snapshots/NonfungiblePositionManager#multicallMintIncreaseAndDecreaseLiquidity.snap
new file mode 100644
index 0000000..73f5148
--- /dev/null
+++ b/.forge-snapshots/NonfungiblePositionManager#multicallMintIncreaseAndDecreaseLiquidity.snap
@@ -0,0 +1 @@
+749006
\ No newline at end of file
diff --git a/src/pool-cl/NonfungiblePositionManager.sol b/src/pool-cl/NonfungiblePositionManager.sol
index 77d424c..cb97ee2 100644
--- a/src/pool-cl/NonfungiblePositionManager.sol
+++ b/src/pool-cl/NonfungiblePositionManager.sol
@@ -115,6 +115,18 @@ contract NonfungiblePositionManager is
         (tick) = poolManager.initialize(poolKey, sqrtPriceX96, hookData);
     }
 
+    /// @inheritdoc INonfungiblePositionManager
+    function modifyLiquidities(bytes calldata lockData, uint256 deadline)
+        external
+        payable
+        checkDeadline(deadline)
+        returns (bytes[] memory)
+    {
+        return abi.decode(
+            vault.lock(abi.encode(CallbackData(msg.sender, CallbackDataType.BatchModifyLiquidity, lockData))), (bytes[])
+        );
+    }
+
     /// @inheritdoc INonfungiblePositionManager
     function mint(MintParams calldata params)
         external
@@ -206,20 +218,46 @@ contract NonfungiblePositionManager is
         }
 
         CallbackData memory data = abi.decode(rawData, (CallbackData));
+        if (data.callbackDataType == CallbackDataType.BatchModifyLiquidity) {
+            bytes[] memory params = abi.decode(data.params, (bytes[]));
+            return _dispatch(params, data.sender);
+        } else {
+            return _handleSingleAction(data, true);
+        }
+    }
+
+    function _dispatch(bytes[] memory params, address sender) internal returns (bytes memory returnDataArrayBytes) {
+        uint256 length = params.length;
+        bytes[] memory returnData = new bytes[](length);
+        // In order to save gas, we will set the settle flag to true if only one liquidity modification
+        bool shouldSettle = length == 1;
+        for (uint256 i; i < length; i++) {
+            CallbackData memory data = abi.decode(params[i], (CallbackData));
+            data.sender = sender;
+            // TODO:
+            returnData[i] = _handleSingleAction(data, shouldSettle);
+        }
+
+        return abi.encode(returnData);
+    }
+
+    function _handleSingleAction(CallbackData memory data, bool shouldSettle) internal returns (bytes memory) {
         if (data.callbackDataType == CallbackDataType.Mint) {
-            return _handleMint(data);
+            return _handleMint(data, shouldSettle);
         } else if (data.callbackDataType == CallbackDataType.IncreaseLiquidity) {
-            return _handleIncreaseLiquidity(data);
+            return _handleIncreaseLiquidity(data, shouldSettle);
         } else if (data.callbackDataType == CallbackDataType.DecreaseLiquidity) {
-            return _handleDecreaseLiquidity(data);
+            return _handleDecreaseLiquidity(data, shouldSettle);
         } else if (data.callbackDataType == CallbackDataType.Collect) {
-            return _handleCollect(data);
+            return _handleCollect(data, shouldSettle);
+        } else if (data.callbackDataType == CallbackDataType.CloseCurrency) {
+            return _close(data.params, data.sender);
         } else {
             revert InvalidCalldataType();
         }
     }
 
-    function _handleMint(CallbackData memory data) internal returns (bytes memory) {
+    function _handleMint(CallbackData memory data, bool shouldSettle) internal returns (bytes memory) {
         INonfungiblePositionManager.MintParams memory params =
             abi.decode(data.params, (INonfungiblePositionManager.MintParams));
 
@@ -259,12 +297,14 @@ contract NonfungiblePositionManager is
             _poolIdToPoolKey[params.poolKey.toId()] = params.poolKey;
         }
 
-        settleDeltas(data.sender, params.poolKey, delta);
+        if (shouldSettle) {
+            settleDeltas(data.sender, params.poolKey, delta);
+        }
 
         return abi.encode(tokenId, liquidity, -delta.amount0(), -delta.amount1());
     }
 
-    function _handleIncreaseLiquidity(CallbackData memory data) internal returns (bytes memory) {
+    function _handleIncreaseLiquidity(CallbackData memory data, bool shouldSettle) internal returns (bytes memory) {
         IncreaseLiquidityParams memory params = abi.decode(data.params, (IncreaseLiquidityParams));
         Position storage nftPosition = _positions[params.tokenId];
         PoolId poolId = nftPosition.poolId;
@@ -320,13 +360,19 @@ contract NonfungiblePositionManager is
         nftPosition.feeGrowthInside1LastX128 = poolManagerPositionInfo.feeGrowthInside1LastX128;
         nftPosition.liquidity += liquidity;
 
-        settleDeltas(data.sender, poolKey, delta);
+        if (shouldSettle) {
+            settleDeltas(data.sender, poolKey, delta);
+        }
 
         return abi.encode(liquidity, -delta.amount0(), -delta.amount1());
     }
 
-    function _handleDecreaseLiquidity(CallbackData memory data) internal returns (bytes memory) {
+    function _handleDecreaseLiquidity(CallbackData memory data, bool shouldSettle) internal returns (bytes memory) {
         DecreaseLiquidityParams memory params = abi.decode(data.params, (DecreaseLiquidityParams));
+        if (params.liquidity == 0) {
+            revert InvalidLiquidityDecreaseAmount();
+        }
+        // TODO: add isAuthorizedForToken check for modifyLiquidities
         Position storage nftPosition = _positions[params.tokenId];
         PoolId poolId = nftPosition.poolId;
         uint128 liquidity = nftPosition.liquidity;
@@ -387,13 +433,21 @@ contract NonfungiblePositionManager is
             nftPosition.liquidity -= params.liquidity;
         }
 
-        settleDeltas(data.sender, poolKey, delta);
+        if (shouldSettle) {
+            settleDeltas(data.sender, poolKey, delta);
+        }
 
         return abi.encode(delta.amount0(), delta.amount1());
     }
 
-    function _handleCollect(CallbackData memory data) internal returns (bytes memory) {
+    function _handleCollect(CallbackData memory data, bool shouldSettle) internal returns (bytes memory) {
         CollectParams memory params = abi.decode(data.params, (CollectParams));
+        // check for modifyLiquidities
+        if (params.amount0Max == 0 && params.amount1Max == 0) {
+            revert InvalidMaxCollectAmount();
+        }
+        params.recipient = params.recipient == address(0) ? address(msg.sender) : params.recipient;
+        // TODO: add isAuthorizedForToken check for modifyLiquidities
         Position storage nftPosition = _positions[params.tokenId];
         Position memory nftPositionCache = _positions[params.tokenId];
         PoolId poolId = nftPositionCache.poolId;
@@ -459,12 +513,27 @@ contract NonfungiblePositionManager is
         );
 
         // cash out from vault
-        burnAndTake(poolKey.currency0, params.recipient, amount0Collect);
-        burnAndTake(poolKey.currency1, params.recipient, amount1Collect);
+        burnAndTake(poolKey.currency0, params.recipient, amount0Collect, shouldSettle);
+        burnAndTake(poolKey.currency1, params.recipient, amount1Collect, shouldSettle);
 
         return abi.encode(amount0Collect, amount1Collect);
     }
 
+    /// @param params is an encoding of the Currency to close
+    /// @param sender is the msg.sender encoded by the `modifyLiquidities` function before the `lockAcquired`.
+    /// @return an encoding of int256 the balance of the currency being settled by this call
+    function _close(bytes memory params, address sender) internal returns (bytes memory) {
+        (Currency currency) = abi.decode(params, (Currency));
+        // this address has applied all deltas on behalf of the user/owner
+        // it is safe to close this entire delta because of slippage checks throughout the batched calls.
+        int256 currencyDelta = vault.currencyDelta(address(this), currency);
+
+        settleOrTake(currency, sender, int128(currencyDelta));
+        //TODO: add refund logic for the remaining native currency
+
+        return abi.encode(currencyDelta);
+    }
+
     function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory) {
         if (!_exists(tokenId)) {
             revert NonexistentToken();
diff --git a/src/pool-cl/base/LiquidityManagement.sol b/src/pool-cl/base/LiquidityManagement.sol
index e48e699..1c2dcea 100644
--- a/src/pool-cl/base/LiquidityManagement.sol
+++ b/src/pool-cl/base/LiquidityManagement.sol
@@ -126,33 +126,28 @@ abstract contract LiquidityManagement is CLPeripheryImmutableState, PeripheryPay
         }
     }
 
-    function burnAndTake(Currency currency, address to, uint256 amount) internal {
+    function burnAndTake(Currency currency, address to, uint256 amount, bool shouldSettle) internal {
         vault.burn(address(this), currency, amount);
-        vault.take(currency, to, amount);
+        if (shouldSettle) {
+            vault.take(currency, to, amount);
+        }
     }
 
     function settleDeltas(address sender, PoolKey memory poolKey, BalanceDelta delta) internal {
-        if (delta.amount0() > 0) {
-            vault.take(poolKey.currency0, sender, uint128(delta.amount0()));
-        } else if (delta.amount0() < 0) {
-            if (poolKey.currency0.isNative()) {
-                vault.settle{value: uint256(int256(-delta.amount0()))}(poolKey.currency0);
-            } else {
-                vault.sync(poolKey.currency0);
-                pay(poolKey.currency0, sender, address(vault), uint256(int256(-delta.amount0())));
-                vault.settle(poolKey.currency0);
-            }
-        }
+        settleOrTake(poolKey.currency0, sender, delta.amount0());
+        settleOrTake(poolKey.currency1, sender, delta.amount1());
+    }
 
-        if (delta.amount1() > 0) {
-            vault.take(poolKey.currency1, sender, uint128(delta.amount1()));
-        } else if (delta.amount1() < 0) {
-            if (poolKey.currency1.isNative()) {
-                vault.settle{value: uint256(int256(-delta.amount1()))}(poolKey.currency1);
+    function settleOrTake(Currency currency, address sender, int128 amount) internal {
+        if (amount > 0) {
+            vault.take(currency, sender, uint128(amount));
+        } else if (amount < 0) {
+            if (currency.isNative()) {
+                vault.settle{value: uint256(int256(-amount))}(currency);
             } else {
-                vault.sync(poolKey.currency1);
-                pay(poolKey.currency1, sender, address(vault), uint256(int256(-delta.amount1())));
-                vault.settle(poolKey.currency1);
+                vault.sync(currency);
+                pay(currency, sender, address(vault), uint256(int256(-amount)));
+                vault.settle(currency);
             }
         }
     }
diff --git a/src/pool-cl/interfaces/INonfungiblePositionManager.sol b/src/pool-cl/interfaces/INonfungiblePositionManager.sol
index 6bfe935..0cb224e 100644
--- a/src/pool-cl/interfaces/INonfungiblePositionManager.sol
+++ b/src/pool-cl/interfaces/INonfungiblePositionManager.sol
@@ -41,7 +41,9 @@ interface INonfungiblePositionManager is
         Mint,
         IncreaseLiquidity,
         DecreaseLiquidity,
-        Collect
+        Collect,
+        BatchModifyLiquidity,
+        CloseCurrency
     }
 
     struct CallbackData {
@@ -139,6 +141,12 @@ interface INonfungiblePositionManager is
         uint256 deadline;
     }
 
+    /// @notice Batches many liquidity modification calls to pool manager
+    /// @param payload is an encoding of actions, and parameters for those actions
+    /// @param deadline is the deadline for the batched actions to be executed
+    /// @return returnData is the endocing of each actions return information
+    function modifyLiquidities(bytes calldata payload, uint256 deadline) external payable returns (bytes[] memory);
+
     /// @notice Creates a new position wrapped in a NFT
     /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized
     /// a method does not exist, i.e. the pool is assumed to be initialized.
diff --git a/test/pool-cl/NonFungiblePositionManager.t.sol b/test/pool-cl/NonFungiblePositionManager.t.sol
index e19dadf..6dbd7aa 100644
--- a/test/pool-cl/NonFungiblePositionManager.t.sol
+++ b/test/pool-cl/NonFungiblePositionManager.t.sol
@@ -782,6 +782,210 @@ contract NonFungiblePositionManagerTest is TokenFixture, Test, GasSnapshot {
         }
     }
 
+    function testMulticallMintAndIncreaseLiquidity() external {
+        bytes32 salt = keccak256("salt");
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: salt,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+        // nonfungiblePoolManager.mint(mintParams);
+
+        // generate multicall data
+        bytes[] memory data = new bytes[](2);
+        data[0] = abi.encodeWithSelector(INonfungiblePositionManager.mint.selector, mintParams);
+
+        INonfungiblePositionManager.IncreaseLiquidityParams memory increaseParams = INonfungiblePositionManager
+            .IncreaseLiquidityParams({
+            tokenId: 1,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        data[1] = abi.encodeWithSelector(INonfungiblePositionManager.increaseLiquidity.selector, increaseParams);
+
+        // multicall
+        snapStart("NonfungiblePositionManager#multicallMintAndIncreaseLiquidity");
+        nonfungiblePoolManager.multicall(data);
+        snapEnd();
+
+        {
+            (
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                uint128 _liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+            ) = nonfungiblePoolManager.positions(1);
+
+            uint128 liquidityExpected = LiquidityAmounts.getLiquidityForAmounts(
+                sqrtPriceX96, TickMath.getSqrtRatioAtTick(46053), TickMath.getSqrtRatioAtTick(46055), 1 ether, 1 ether
+            );
+
+            assertEq(_liquidity, 1991375027067913587988 + liquidityExpected, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected feesOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected feesOwed1");
+        }
+        assertEq(poolManager.getLiquidity(key.toId()), 2 * 1991375027067913587988, "Unexpected liquidity for the pool");
+        assertEq(
+            poolManager.getLiquidity(key.toId(), address(nonfungiblePoolManager), 46053, 46055, salt),
+            1991375027067913587988 * 2,
+            "Unexpected liquidity for current position"
+        );
+
+        assertEq(
+            nonfungiblePoolManager.balanceOf(address(this)), 1, "Unexpected balance of the position owner after mint"
+        );
+
+        assertEq(nonfungiblePoolManager.ownerOf(1), address(this), "Unexpected owner of the position");
+
+        {
+            (
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                uint128 _liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+            ) = nonfungiblePoolManager.positions(1);
+            assertEq(_liquidity, 1991375027067913587988 * 2, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected tokensOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected tokensOwed1");
+        }
+    }
+
+    function testMulticallMintIncreaseAndDecreaseLiquidity() external {
+        bytes32 salt = keccak256("salt");
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: salt,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+        // nonfungiblePoolManager.mint(mintParams);
+
+        // generate multicall data
+        bytes[] memory data = new bytes[](3);
+        data[0] = abi.encodeWithSelector(INonfungiblePositionManager.mint.selector, mintParams);
+
+        INonfungiblePositionManager.IncreaseLiquidityParams memory increaseParams = INonfungiblePositionManager
+            .IncreaseLiquidityParams({
+            tokenId: 1,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        data[1] = abi.encodeWithSelector(INonfungiblePositionManager.increaseLiquidity.selector, increaseParams);
+
+        INonfungiblePositionManager.DecreaseLiquidityParams memory decreaseParams = INonfungiblePositionManager
+            .DecreaseLiquidityParams({
+            tokenId: 1,
+            liquidity: 1991375027067913587988 + 1991375027067913587987,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+
+        data[2] = abi.encodeWithSelector(INonfungiblePositionManager.decreaseLiquidity.selector, decreaseParams);
+
+        // multicall
+        snapStart("NonfungiblePositionManager#multicallMintIncreaseAndDecreaseLiquidity");
+        nonfungiblePoolManager.multicall(data);
+        snapEnd();
+
+        {
+            (
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                uint128 _liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+            ) = nonfungiblePoolManager.positions(1);
+
+            // uint128 liquidityExpected = LiquidityAmounts.getLiquidityForAmounts(
+            //     sqrtPriceX96, TickMath.getSqrtRatioAtTick(46053), TickMath.getSqrtRatioAtTick(46055), 1 ether, 1 ether
+            // );
+
+            assertEq(_liquidity, 1, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected feesOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected feesOwed1");
+        }
+    }
+
     function testIncreaseLiquidity_gas() external {
         PoolKey memory key = PoolKey({
             currency0: currency0,
diff --git a/test/pool-cl/NonFungiblePositionManagerBatchTest.t.sol b/test/pool-cl/NonFungiblePositionManagerBatchTest.t.sol
new file mode 100644
index 0000000..ed3a273
--- /dev/null
+++ b/test/pool-cl/NonFungiblePositionManagerBatchTest.t.sol
@@ -0,0 +1,850 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.19;
+
+import "forge-std/Test.sol";
+
+import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol";
+import {IVault} from "pancake-v4-core/src/interfaces/IVault.sol";
+import {Vault} from "pancake-v4-core/src/Vault.sol";
+import {IHooks} from "pancake-v4-core/src/interfaces/IHooks.sol";
+import {TokenFixture} from "pancake-v4-core/test/helpers/TokenFixture.sol";
+import {ICLPoolManager, CLPosition} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol";
+import {CLPoolManager} from "pancake-v4-core/src/pool-cl/CLPoolManager.sol";
+import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
+import {FixedPoint96} from "pancake-v4-core/src/pool-cl/libraries/FixedPoint96.sol";
+import {FixedPoint128} from "pancake-v4-core/src/pool-cl/libraries/FixedPoint128.sol";
+import {Currency} from "pancake-v4-core/src/types/Currency.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol";
+import {TickMath} from "pancake-v4-core/src/pool-cl/libraries/TickMath.sol";
+import {CLPoolManagerRouter} from "pancake-v4-core/test/pool-cl/helpers/CLPoolManagerRouter.sol";
+import {FullMath} from "pancake-v4-core/src/pool-cl/libraries/FullMath.sol";
+import {BalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol";
+import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
+
+import {NonfungibleTokenPositionDescriptorOffChain} from
+    "../../src/pool-cl/NonfungibleTokenPositionDescriptorOffChain.sol";
+import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
+import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
+import {NonfungiblePositionManager} from "../../src/pool-cl/NonfungiblePositionManager.sol";
+import {INonfungiblePositionManager} from "../../src/pool-cl/interfaces/INonfungiblePositionManager.sol";
+import {LiquidityAmounts} from "../../src/pool-cl/libraries/LiquidityAmounts.sol";
+import {LiquidityManagement} from "../../src/pool-cl/base/LiquidityManagement.sol";
+
+contract NonFungiblePositionManagerBatchTest is TokenFixture, Test, GasSnapshot {
+    using PoolIdLibrary for PoolKey;
+    using Strings for uint256;
+
+    event IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
+    event DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
+    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
+    event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1);
+
+    IVault public vault;
+    ICLPoolManager public poolManager;
+    CLPoolManagerRouter public router;
+    NonfungiblePositionManager public nonfungiblePoolManager;
+
+    function setUp() public {
+        vault = new Vault();
+        poolManager = new CLPoolManager(vault, 3000);
+        vault.registerApp(address(poolManager));
+
+        initializeTokens();
+
+        router = new CLPoolManagerRouter(vault, poolManager);
+        IERC20(Currency.unwrap(currency0)).approve(address(router), type(uint256).max);
+        IERC20(Currency.unwrap(currency1)).approve(address(router), type(uint256).max);
+        NonfungibleTokenPositionDescriptorOffChain NFTPositionDescriptorContract =
+            new NonfungibleTokenPositionDescriptorOffChain();
+        ProxyAdmin proxyAdminContract = new ProxyAdmin();
+
+        string memory baseTokenURI = string.concat("https://nft.pancakeswap.com/v4/", block.chainid.toString(), "/");
+        TransparentUpgradeableProxy NFTDescriptorProxy = new TransparentUpgradeableProxy(
+            address(NFTPositionDescriptorContract),
+            address(proxyAdminContract),
+            abi.encodeCall(NonfungibleTokenPositionDescriptorOffChain.initialize, (baseTokenURI))
+        );
+
+        nonfungiblePoolManager =
+            new NonfungiblePositionManager(vault, poolManager, address(NFTDescriptorProxy), address(0));
+        vm.label(Currency.unwrap(currency0), "currency0");
+        vm.label(Currency.unwrap(currency1), "currency1");
+
+        IERC20(Currency.unwrap(currency0)).approve(address(nonfungiblePoolManager), type(uint256).max);
+        IERC20(Currency.unwrap(currency1)).approve(address(nonfungiblePoolManager), type(uint256).max);
+    }
+
+    function testBatchMint() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        {
+            INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+                poolKey: key,
+                tickLower: 46053,
+                tickUpper: 46055,
+                salt: bytes32(0),
+                amount0Desired: 1 ether,
+                amount1Desired: 1 ether,
+                amount0Min: 0,
+                amount1Min: 0,
+                recipient: makeAddr("someone"),
+                deadline: type(uint256).max
+            });
+
+            uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+            nonfungiblePoolManager.initialize(key, sqrtPriceX96, new bytes(0));
+
+            // generate modifyLiquidities data
+            bytes memory mintData = abi.encode(
+                INonfungiblePositionManager.CallbackData(
+                    address(this), INonfungiblePositionManager.CallbackDataType.Mint, abi.encode(mintParams)
+                )
+            );
+            bytes[] memory data = new bytes[](3);
+            data[0] = mintData;
+            // set current close data
+            data[1] = abi.encode(
+                INonfungiblePositionManager.CallbackData(
+                    address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency0)
+                )
+            );
+            data[2] = abi.encode(
+                INonfungiblePositionManager.CallbackData(
+                    address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency1)
+                )
+            );
+            snapStart("NonFungiblePositionManagerBatch#mint");
+            nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+            snapEnd();
+        }
+
+        {
+            (
+                uint96 nonce,
+                address operator,
+                PoolId poolId,
+                Currency _currency0,
+                Currency _currency1,
+                uint24 fee,
+                int24 tickLower,
+                int24 tickUpper,
+                uint128 liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+                bytes32 salt
+            ) = nonfungiblePoolManager.positions(1);
+            assertEq(nonce, 0, "Unexpected nonce");
+            assertEq(operator, address(0), "Unexpected operator");
+            assertEq(PoolId.unwrap(poolId), PoolId.unwrap(key.toId()), "Unexpected poolId");
+            assertEq(Currency.unwrap(_currency0), Currency.unwrap(currency0), "Unexpected currency0");
+            assertEq(Currency.unwrap(_currency1), Currency.unwrap(currency1), "Unexpected currency1");
+            assertEq(fee, 3000, "Unexpected fee");
+            assertEq(tickLower, 46053, "Unexpected tickLower");
+            assertEq(tickUpper, 46055, "Unexpected tickUpper");
+            assertEq(liquidity, 1991375027067913587988, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected tokensOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected tokensOwed1");
+            assertEq(salt, bytes32(0), "Unexpected salt");
+            string memory expectTokenURI =
+                string.concat("https://nft.pancakeswap.com/v4/", block.chainid.toString(), "/1");
+            string memory realTokenURI = nonfungiblePoolManager.tokenURI(1);
+            assertEq(expectTokenURI, realTokenURI, "Unexpected tokenURI");
+        }
+    }
+
+    function testBatchMintWithoutCloseCurrency() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        {
+            INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+                poolKey: key,
+                tickLower: 46053,
+                tickUpper: 46055,
+                salt: bytes32(0),
+                amount0Desired: 1 ether,
+                amount1Desired: 1 ether,
+                amount0Min: 0,
+                amount1Min: 0,
+                recipient: makeAddr("someone"),
+                deadline: type(uint256).max
+            });
+
+            uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+
+            nonfungiblePoolManager.initialize(key, sqrtPriceX96, new bytes(0));
+
+            // generate modifyLiquidities data
+            bytes memory mintData = abi.encode(
+                INonfungiblePositionManager.CallbackData(
+                    address(this), INonfungiblePositionManager.CallbackDataType.Mint, abi.encode(mintParams)
+                )
+            );
+            bytes[] memory data = new bytes[](1);
+            data[0] = mintData;
+
+            snapStart("NonFungiblePositionManagerBatch#mintWithoutCloseCurrency");
+            nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+            snapEnd();
+        }
+
+        {
+            (
+                uint96 nonce,
+                address operator,
+                PoolId poolId,
+                Currency _currency0,
+                Currency _currency1,
+                uint24 fee,
+                int24 tickLower,
+                int24 tickUpper,
+                uint128 liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+                bytes32 salt
+            ) = nonfungiblePoolManager.positions(1);
+            assertEq(nonce, 0, "Unexpected nonce");
+            assertEq(operator, address(0), "Unexpected operator");
+            assertEq(PoolId.unwrap(poolId), PoolId.unwrap(key.toId()), "Unexpected poolId");
+            assertEq(Currency.unwrap(_currency0), Currency.unwrap(currency0), "Unexpected currency0");
+            assertEq(Currency.unwrap(_currency1), Currency.unwrap(currency1), "Unexpected currency1");
+            assertEq(fee, 3000, "Unexpected fee");
+            assertEq(tickLower, 46053, "Unexpected tickLower");
+            assertEq(tickUpper, 46055, "Unexpected tickUpper");
+            assertEq(liquidity, 1991375027067913587988, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected tokensOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected tokensOwed1");
+            assertEq(salt, bytes32(0), "Unexpected salt");
+            string memory expectTokenURI =
+                string.concat("https://nft.pancakeswap.com/v4/", block.chainid.toString(), "/1");
+            string memory realTokenURI = nonfungiblePoolManager.tokenURI(1);
+            assertEq(expectTokenURI, realTokenURI, "Unexpected tokenURI");
+        }
+    }
+
+    function testBatchMintAndIncreaseLiquidity() external {
+        bytes32 salt = keccak256("salt");
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: salt,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+
+        // generate multicall data
+        bytes[] memory data = new bytes[](4);
+        data[0] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.Mint, abi.encode(mintParams)
+            )
+        );
+
+        INonfungiblePositionManager.IncreaseLiquidityParams memory increaseParams = INonfungiblePositionManager
+            .IncreaseLiquidityParams({
+            tokenId: 1,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        data[1] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this),
+                INonfungiblePositionManager.CallbackDataType.IncreaseLiquidity,
+                abi.encode(increaseParams)
+            )
+        );
+
+        // set currency close data
+        data[2] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency0)
+            )
+        );
+        data[3] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency1)
+            )
+        );
+
+        // batch mint and increase liquidity
+        snapStart("NonFungiblePositionManagerBatch#BatchMintAndIncreaseLiquidity");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+
+        {
+            (
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                uint128 _liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+            ) = nonfungiblePoolManager.positions(1);
+
+            uint128 liquidityExpected = LiquidityAmounts.getLiquidityForAmounts(
+                sqrtPriceX96, TickMath.getSqrtRatioAtTick(46053), TickMath.getSqrtRatioAtTick(46055), 1 ether, 1 ether
+            );
+
+            assertEq(_liquidity, 1991375027067913587988 + liquidityExpected, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected feesOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected feesOwed1");
+        }
+        assertEq(poolManager.getLiquidity(key.toId()), 2 * 1991375027067913587988, "Unexpected liquidity for the pool");
+        assertEq(
+            poolManager.getLiquidity(key.toId(), address(nonfungiblePoolManager), 46053, 46055, salt),
+            1991375027067913587988 * 2,
+            "Unexpected liquidity for current position"
+        );
+
+        assertEq(
+            nonfungiblePoolManager.balanceOf(address(this)), 1, "Unexpected balance of the position owner after mint"
+        );
+
+        assertEq(nonfungiblePoolManager.ownerOf(1), address(this), "Unexpected owner of the position");
+
+        {
+            (
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                uint128 _liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+            ) = nonfungiblePoolManager.positions(1);
+            assertEq(_liquidity, 1991375027067913587988 * 2, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected tokensOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected tokensOwed1");
+        }
+    }
+
+    function testBatchMintIncreaseAndDecreaseLiquidity(bytes32 salt) external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: salt,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+        // nonfungiblePoolManager.mint(mintParams);
+
+        // generate multicall data
+        bytes[] memory data = new bytes[](5);
+        data[0] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.Mint, abi.encode(mintParams)
+            )
+        );
+
+        INonfungiblePositionManager.IncreaseLiquidityParams memory increaseParams = INonfungiblePositionManager
+            .IncreaseLiquidityParams({
+            tokenId: 1,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        data[1] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this),
+                INonfungiblePositionManager.CallbackDataType.IncreaseLiquidity,
+                abi.encode(increaseParams)
+            )
+        );
+
+        INonfungiblePositionManager.DecreaseLiquidityParams memory decreaseParams = INonfungiblePositionManager
+            .DecreaseLiquidityParams({
+            tokenId: 1,
+            liquidity: 1991375027067913587988 + 1991375027067913587987,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+
+        data[2] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this),
+                INonfungiblePositionManager.CallbackDataType.DecreaseLiquidity,
+                abi.encode(decreaseParams)
+            )
+        );
+
+        // set currency close data
+        data[3] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency0)
+            )
+        );
+        data[4] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency1)
+            )
+        );
+
+        // batch mint and increase liquidity, then decrease liquidity
+        snapStart("NonFungiblePositionManagerBatch#batchMintIncreaseAndDecreaseLiquidity");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+
+        {
+            (
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                ,
+                uint128 _liquidity,
+                uint256 feeGrowthInside0LastX128,
+                uint256 feeGrowthInside1LastX128,
+                uint128 tokensOwed0,
+                uint128 tokensOwed1,
+            ) = nonfungiblePoolManager.positions(1);
+
+            assertEq(_liquidity, 1, "Unexpected liquidity");
+            assertEq(feeGrowthInside0LastX128, 0, "Unexpected feeGrowthInside0LastX128");
+            assertEq(feeGrowthInside1LastX128, 0, "Unexpected feeGrowthInside1LastX128");
+            assertEq(tokensOwed0, 0, "Unexpected feesOwed0");
+            assertEq(tokensOwed1, 0, "Unexpected feesOwed1");
+        }
+    }
+
+    function testBatchIncreaseLiquidityWithoutCloseCurrency() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: bytes32(0),
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+        nonfungiblePoolManager.mint(mintParams);
+
+        // generate modifyLiquidities data
+        INonfungiblePositionManager.IncreaseLiquidityParams memory increaseParams = INonfungiblePositionManager
+            .IncreaseLiquidityParams({
+            tokenId: 1,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        bytes memory increaseData = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this),
+                INonfungiblePositionManager.CallbackDataType.IncreaseLiquidity,
+                abi.encode(increaseParams)
+            )
+        );
+        bytes[] memory data = new bytes[](1);
+        data[0] = increaseData;
+
+        snapStart("NonFungiblePositionManagerBatch#increaseLiquidityWithoutCloseCurrency");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+    }
+
+    function testBatchIncreaseLiquidity() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: bytes32(0),
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+        nonfungiblePoolManager.mint(mintParams);
+
+        // generate modifyLiquidities data
+        INonfungiblePositionManager.IncreaseLiquidityParams memory increaseParams = INonfungiblePositionManager
+            .IncreaseLiquidityParams({
+            tokenId: 1,
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        bytes memory increaseData = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this),
+                INonfungiblePositionManager.CallbackDataType.IncreaseLiquidity,
+                abi.encode(increaseParams)
+            )
+        );
+        bytes[] memory data = new bytes[](3);
+        data[0] = increaseData;
+        // set current close data
+        data[1] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency0)
+            )
+        );
+        data[2] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency1)
+            )
+        );
+
+        snapStart("NonFungiblePositionManagerBatch#increaseLiquidity");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+    }
+
+    function testBatchDecreaseLiquidityWithoutCloseCurrency() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: bytes32(0),
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+
+        nonfungiblePoolManager.mint(mintParams);
+
+        // generate modifyLiquidities data
+        INonfungiblePositionManager.DecreaseLiquidityParams memory decreaseParams = INonfungiblePositionManager
+            .DecreaseLiquidityParams({
+            tokenId: 1,
+            liquidity: 1991375027067913587988,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        bytes memory decreaseData = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this),
+                INonfungiblePositionManager.CallbackDataType.DecreaseLiquidity,
+                abi.encode(decreaseParams)
+            )
+        );
+        bytes[] memory data = new bytes[](1);
+        data[0] = decreaseData;
+
+        snapStart("NonFungiblePositionManagerBatch#decreaseLiquidityWithoutCloseCurrency");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+    }
+
+    function testBatchDecreaseLiquidity() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: bytes32(0),
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+
+        nonfungiblePoolManager.mint(mintParams);
+
+        // generate modifyLiquidities data
+        INonfungiblePositionManager.DecreaseLiquidityParams memory decreaseParams = INonfungiblePositionManager
+            .DecreaseLiquidityParams({
+            tokenId: 1,
+            liquidity: 1991375027067913587988,
+            amount0Min: 0,
+            amount1Min: 0,
+            deadline: type(uint256).max
+        });
+        bytes memory decreaseData = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this),
+                INonfungiblePositionManager.CallbackDataType.DecreaseLiquidity,
+                abi.encode(decreaseParams)
+            )
+        );
+        bytes[] memory data = new bytes[](3);
+        data[0] = decreaseData;
+
+        // set current close data
+        data[1] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency0)
+            )
+        );
+        data[2] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency1)
+            )
+        );
+
+        snapStart("NonFungiblePositionManagerBatch#decreaseLiquidity");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+    }
+
+    function testBatchCollectWithoutCloseCurrency() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: bytes32(0),
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+
+        nonfungiblePoolManager.mint(mintParams);
+        // make the LPing balance of the position non-zero
+        router.donate(key, 1 ether, 1 ether, "");
+
+        INonfungiblePositionManager.CollectParams memory collectParams = INonfungiblePositionManager.CollectParams({
+            tokenId: 1,
+            recipient: address(this),
+            amount0Max: 999999999999999999,
+            amount1Max: 999999999999999999
+        });
+        // generate modifyLiquidities data
+        bytes memory collectData = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.Collect, abi.encode(collectParams)
+            )
+        );
+        bytes[] memory data = new bytes[](1);
+        data[0] = collectData;
+        snapStart("NonFungiblePositionManagerBatch#collectWithoutCloseCurrency");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+    }
+
+    function testBatchCollect() external {
+        PoolKey memory key = PoolKey({
+            currency0: currency0,
+            currency1: currency1,
+            hooks: IHooks(address(0)),
+            poolManager: poolManager,
+            fee: uint24(3000),
+            // 0 ~ 15  hookRegistrationMap = nil
+            // 16 ~ 24 tickSpacing = 1
+            parameters: bytes32(uint256(0x10000))
+        });
+
+        INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({
+            poolKey: key,
+            tickLower: 46053,
+            tickUpper: 46055,
+            salt: bytes32(0),
+            amount0Desired: 1 ether,
+            amount1Desired: 1 ether,
+            amount0Min: 0,
+            amount1Min: 0,
+            recipient: address(this),
+            deadline: type(uint256).max
+        });
+
+        uint160 sqrtPriceX96 = uint160(10 * FixedPoint96.Q96);
+        poolManager.initialize(key, sqrtPriceX96, new bytes(0));
+
+        nonfungiblePoolManager.mint(mintParams);
+        // make the LPing balance of the position non-zero
+        router.donate(key, 1 ether, 1 ether, "");
+
+        INonfungiblePositionManager.CollectParams memory collectParams = INonfungiblePositionManager.CollectParams({
+            tokenId: 1,
+            recipient: address(this),
+            amount0Max: 999999999999999999,
+            amount1Max: 999999999999999999
+        });
+        // generate modifyLiquidities data
+        bytes memory collectData = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.Collect, abi.encode(collectParams)
+            )
+        );
+        bytes[] memory data = new bytes[](3);
+        data[0] = collectData;
+        // set current close data
+        data[1] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency0)
+            )
+        );
+        data[2] = abi.encode(
+            INonfungiblePositionManager.CallbackData(
+                address(this), INonfungiblePositionManager.CallbackDataType.CloseCurrency, abi.encode(currency1)
+            )
+        );
+        snapStart("NonFungiblePositionManagerBatch#collect");
+        nonfungiblePoolManager.modifyLiquidities(abi.encode(data), block.timestamp + 100);
+        snapEnd();
+    }
+}