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(); + } +}