@bananapus/univ4-lp-split-hook-v6 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/univ4-lp-split-hook-v6",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -653,105 +653,17 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
653
653
  address projectToken = address(IJBTokens(TOKENS).tokenOf(projectId));
654
654
  PoolKey memory key = _poolKeys[projectId][terminalToken];
655
655
 
656
- // Step 1: Collect accrued fees via DECREASE_LIQUIDITY(0) + TAKE_PAIR
657
- {
658
- uint256 bal0Before = _currencyBalance(key.currency0);
659
- uint256 bal1Before = _currencyBalance(key.currency1);
660
-
661
- bytes memory feeActions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR));
662
-
663
- bytes[] memory feeParams = new bytes[](2);
664
- feeParams[0] = abi.encode(tokenId, uint256(0), uint128(0), uint128(0), "");
665
- feeParams[1] = abi.encode(key.currency0, key.currency1, address(this));
666
-
667
- POSITION_MANAGER.modifyLiquidities({
668
- unlockData: abi.encode(feeActions, feeParams), deadline: block.timestamp + _DEADLINE_SECONDS
669
- });
670
-
671
- uint256 feeAmount0 = _currencyBalance(key.currency0) - bal0Before;
672
- uint256 feeAmount1 = _currencyBalance(key.currency1) - bal1Before;
673
-
674
- _routeCollectedFees({
675
- projectId: projectId,
676
- projectToken: projectToken,
677
- terminalToken: terminalToken,
678
- amount0: feeAmount0,
679
- amount1: feeAmount1
680
- });
681
- _burnReceivedTokens({projectId: projectId, projectToken: projectToken});
682
- }
683
-
684
- // Step 2: Burn position to recover principal via BURN_POSITION + TAKE_PAIR
685
- {
686
- bytes memory burnActions = abi.encodePacked(uint8(Actions.BURN_POSITION), uint8(Actions.TAKE_PAIR));
687
-
688
- bytes[] memory burnParams = new bytes[](2);
689
- // Safe: min amounts are user-provided slippage params; PositionManager accepts uint128.
690
- // forge-lint: disable-next-line(unsafe-typecast)
691
- burnParams[0] = abi.encode(tokenId, uint128(decreaseAmount0Min), uint128(decreaseAmount1Min), "");
692
- burnParams[1] = abi.encode(key.currency0, key.currency1, address(this));
693
-
694
- POSITION_MANAGER.modifyLiquidities({
695
- unlockData: abi.encode(burnActions, burnParams), deadline: block.timestamp + _DEADLINE_SECONDS
696
- });
697
- }
698
-
699
- // Step 2: Mint new position with updated tick bounds
700
- {
701
- uint256 projectTokenBalance = IERC20(projectToken).balanceOf(address(this));
702
- uint256 terminalTokenBalance = _getTerminalTokenBalance(terminalToken);
703
-
704
- (int24 tickLower, int24 tickUpper) =
705
- _calculateTickBounds({projectId: projectId, terminalToken: terminalToken, projectToken: projectToken});
706
-
707
- // Use the actual pool price for liquidity calculation so the target matches the pool's
708
- // current state. Using JB issuance price here would produce suboptimal liquidity when the
709
- // pool price has diverged.
710
- // slither-disable-next-line unused-return
711
- (uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(key.toId());
712
- uint160 sqrtPriceA = TickMath.getSqrtPriceAtTick(tickLower);
713
- uint160 sqrtPriceB = TickMath.getSqrtPriceAtTick(tickUpper);
714
-
715
- // Sort amounts by currency order
716
- Currency terminalCurrency = _toCurrency(terminalToken);
717
- (address token0,) = _sortTokens({tokenA: projectToken, tokenB: Currency.unwrap(terminalCurrency)});
718
- uint256 amount0 = projectToken == token0 ? projectTokenBalance : terminalTokenBalance;
719
- uint256 amount1 = projectToken == token0 ? terminalTokenBalance : projectTokenBalance;
720
-
721
- // Calculate liquidity from amounts
722
- uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts({
723
- sqrtPriceX96: sqrtPriceX96,
724
- sqrtPriceAX96: sqrtPriceA,
725
- sqrtPriceBX96: sqrtPriceB,
726
- amount0: amount0,
727
- amount1: amount1
728
- });
729
-
730
- if (liquidity > 0) {
731
- uint256 newTokenId = POSITION_MANAGER.nextTokenId();
732
-
733
- _mintPosition({
734
- key: key,
735
- tickLower: tickLower,
736
- tickUpper: tickUpper,
737
- liquidity: liquidity,
738
- amount0: amount0,
739
- amount1: amount1
740
- });
656
+ _collectAndRouteFees({
657
+ projectId: projectId, projectToken: projectToken, terminalToken: terminalToken, tokenId: tokenId, key: key
658
+ });
741
659
 
742
- tokenIdOf[projectId][terminalToken] = newTokenId;
743
- } else {
744
- // Zero liquidity means the position cannot be re-created (e.g., price moved
745
- // outside tick range making the position single-sided with zero on one side).
746
- // Revert to prevent bricking the project's LP — the old position was already
747
- // burned by the BURN_POSITION action above, so this protects the invariant
748
- // that tokenIdOf is always nonzero for deployed projects.
749
- revert JBUniswapV4LPSplitHook_InsufficientLiquidity();
750
- }
660
+ _burnExistingPosition({
661
+ tokenId: tokenId, key: key, decreaseAmount0Min: decreaseAmount0Min, decreaseAmount1Min: decreaseAmount1Min
662
+ });
751
663
 
752
- // Handle leftover tokens
753
- _handleLeftoverTokens({projectId: projectId, projectToken: projectToken, terminalToken: terminalToken});
754
- }
664
+ _mintRebalancedPosition({
665
+ projectId: projectId, projectToken: projectToken, terminalToken: terminalToken, key: key
666
+ });
755
667
  }
756
668
 
757
669
  //*********************************************************************//
@@ -899,6 +811,38 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
899
811
  return rounded;
900
812
  }
901
813
 
814
+ /// @notice Burn an existing LP position via `BURN_POSITION` + `TAKE_PAIR` and recover its principal.
815
+ /// @dev Called during rebalancing after fees have already been collected. The recovered tokens remain in
816
+ /// this contract for the subsequent `_mintRebalancedPosition` call.
817
+ /// @param tokenId The Uniswap V4 position NFT token ID to burn.
818
+ /// @param key The pool key identifying the Uniswap V4 pool.
819
+ /// @param decreaseAmount0Min Minimum amount of token0 to receive (slippage protection).
820
+ /// @param decreaseAmount1Min Minimum amount of token1 to receive (slippage protection).
821
+ function _burnExistingPosition(
822
+ uint256 tokenId,
823
+ PoolKey memory key,
824
+ uint256 decreaseAmount0Min,
825
+ uint256 decreaseAmount1Min
826
+ )
827
+ internal
828
+ {
829
+ // BURN_POSITION removes all remaining liquidity and destroys the NFT.
830
+ // TAKE_PAIR transfers the recovered token0 and token1 to this contract.
831
+ bytes memory burnActions = abi.encodePacked(uint8(Actions.BURN_POSITION), uint8(Actions.TAKE_PAIR));
832
+
833
+ bytes[] memory burnParams = new bytes[](2);
834
+ // BURN_POSITION params: (tokenId, minAmount0, minAmount1, hookData).
835
+ // Min amounts are caller-supplied slippage bounds; PositionManager accepts uint128.
836
+ // forge-lint: disable-next-line(unsafe-typecast)
837
+ burnParams[0] = abi.encode(tokenId, uint128(decreaseAmount0Min), uint128(decreaseAmount1Min), "");
838
+ // TAKE_PAIR params: (currency0, currency1, recipient).
839
+ burnParams[1] = abi.encode(key.currency0, key.currency1, address(this));
840
+
841
+ POSITION_MANAGER.modifyLiquidities({
842
+ unlockData: abi.encode(burnActions, burnParams), deadline: block.timestamp + _DEADLINE_SECONDS
843
+ });
844
+ }
845
+
902
846
  /// @notice Burn project tokens using the controller
903
847
  // slither-disable-next-line incorrect-equality,reentrancy-events
904
848
  function _burnProjectTokens(uint256 projectId, address projectToken, uint256 amount, string memory memo) internal {
@@ -989,6 +933,59 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
989
933
  }
990
934
  }
991
935
 
936
+ /// @notice Collect accrued Uniswap LP trading fees and route them to the project's terminal balance.
937
+ /// @dev Uses `DECREASE_LIQUIDITY(0)` to trigger fee collection without removing any principal, followed by
938
+ /// `TAKE_PAIR` to transfer the fees to this contract. Terminal-token fees are added to the project's balance;
939
+ /// project-token fees are burned to avoid inflating supply.
940
+ /// @param projectId The ID of the Juicebox project whose LP fees are being collected.
941
+ /// @param projectToken The project's ERC-20 token address.
942
+ /// @param terminalToken The terminal token (e.g. ETH or USDC) paired with the project token.
943
+ /// @param tokenId The Uniswap V4 position NFT token ID to collect fees from.
944
+ /// @param key The pool key identifying the Uniswap V4 pool.
945
+ // slither-disable-next-line reentrancy-eth,reentrancy-benign,reentrancy-events
946
+ function _collectAndRouteFees(
947
+ uint256 projectId,
948
+ address projectToken,
949
+ address terminalToken,
950
+ uint256 tokenId,
951
+ PoolKey memory key
952
+ )
953
+ internal
954
+ {
955
+ // Snapshot balances before collection to isolate fee amounts from any existing balance.
956
+ uint256 bal0Before = _currencyBalance(key.currency0);
957
+ uint256 bal1Before = _currencyBalance(key.currency1);
958
+
959
+ // DECREASE_LIQUIDITY with amount=0 triggers fee collection without removing principal.
960
+ // TAKE_PAIR transfers the collected fees (both currencies) to this contract.
961
+ bytes memory feeActions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR));
962
+
963
+ bytes[] memory feeParams = new bytes[](2);
964
+ // DECREASE_LIQUIDITY params: (tokenId, liquidity=0, minAmount0=0, minAmount1=0, hookData).
965
+ feeParams[0] = abi.encode(tokenId, uint256(0), uint128(0), uint128(0), "");
966
+ // TAKE_PAIR params: (currency0, currency1, recipient).
967
+ feeParams[1] = abi.encode(key.currency0, key.currency1, address(this));
968
+
969
+ POSITION_MANAGER.modifyLiquidities({
970
+ unlockData: abi.encode(feeActions, feeParams), deadline: block.timestamp + _DEADLINE_SECONDS
971
+ });
972
+
973
+ // Diff balances to determine exactly how much was collected as fees.
974
+ uint256 feeAmount0 = _currencyBalance(key.currency0) - bal0Before;
975
+ uint256 feeAmount1 = _currencyBalance(key.currency1) - bal1Before;
976
+
977
+ // Route terminal-token fees to the project's balance; project-token fees are burned below.
978
+ _routeCollectedFees({
979
+ projectId: projectId,
980
+ projectToken: projectToken,
981
+ terminalToken: terminalToken,
982
+ amount0: feeAmount0,
983
+ amount1: feeAmount1
984
+ });
985
+ // Burn any project tokens received as fees to avoid inflating circulating supply.
986
+ _burnReceivedTokens({projectId: projectId, projectToken: projectToken});
987
+ }
988
+
992
989
  /// @notice Compute the initial sqrtPriceX96 for pool initialization
993
990
  function _computeInitialSqrtPrice(
994
991
  uint256 projectId,
@@ -1267,6 +1264,80 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
1267
1264
  });
1268
1265
  }
1269
1266
 
1267
+ /// @notice Mint a new LP position with tick bounds recalculated from current issuance and cash-out rates.
1268
+ /// @dev Called after `_burnExistingPosition` has recovered the old position's principal. Computes liquidity from
1269
+ /// this contract's current token balances and the pool's live `sqrtPriceX96`. Reverts with
1270
+ /// `JBUniswapV4LPSplitHook_InsufficientLiquidity` if the resulting liquidity is zero (e.g. price moved entirely
1271
+ /// outside the new tick range), preventing `tokenIdOf` from being left stale. Any leftover tokens after minting
1272
+ /// are routed back to the project via `_handleLeftoverTokens`.
1273
+ /// @param projectId The ID of the Juicebox project being rebalanced.
1274
+ /// @param projectToken The project's ERC-20 token address.
1275
+ /// @param terminalToken The terminal token paired with the project token.
1276
+ /// @param key The pool key identifying the Uniswap V4 pool.
1277
+ // slither-disable-next-line reentrancy-eth,reentrancy-benign,reentrancy-events
1278
+ function _mintRebalancedPosition(
1279
+ uint256 projectId,
1280
+ address projectToken,
1281
+ address terminalToken,
1282
+ PoolKey memory key
1283
+ )
1284
+ internal
1285
+ {
1286
+ uint256 projectTokenBalance = IERC20(projectToken).balanceOf(address(this));
1287
+ uint256 terminalTokenBalance = _getTerminalTokenBalance(terminalToken);
1288
+
1289
+ (int24 tickLower, int24 tickUpper) =
1290
+ _calculateTickBounds({projectId: projectId, terminalToken: terminalToken, projectToken: projectToken});
1291
+
1292
+ // Use the actual pool price for liquidity calculation so the target matches the pool's
1293
+ // current state. Using JB issuance price here would produce suboptimal liquidity when the
1294
+ // pool price has diverged.
1295
+ // slither-disable-next-line unused-return
1296
+ (uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(key.toId());
1297
+ uint160 sqrtPriceA = TickMath.getSqrtPriceAtTick(tickLower);
1298
+ uint160 sqrtPriceB = TickMath.getSqrtPriceAtTick(tickUpper);
1299
+
1300
+ // Map token balances to (amount0, amount1) matching the pool's currency ordering.
1301
+ Currency terminalCurrency = _toCurrency(terminalToken);
1302
+ (address token0,) = _sortTokens({tokenA: projectToken, tokenB: Currency.unwrap(terminalCurrency)});
1303
+ uint256 amount0 = projectToken == token0 ? projectTokenBalance : terminalTokenBalance;
1304
+ uint256 amount1 = projectToken == token0 ? terminalTokenBalance : projectTokenBalance;
1305
+
1306
+ // Derive the maximum liquidity mintable from our balances at the current pool price.
1307
+ uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts({
1308
+ sqrtPriceX96: sqrtPriceX96,
1309
+ sqrtPriceAX96: sqrtPriceA,
1310
+ sqrtPriceBX96: sqrtPriceB,
1311
+ amount0: amount0,
1312
+ amount1: amount1
1313
+ });
1314
+
1315
+ if (liquidity > 0) {
1316
+ uint256 newTokenId = POSITION_MANAGER.nextTokenId();
1317
+
1318
+ _mintPosition({
1319
+ key: key,
1320
+ tickLower: tickLower,
1321
+ tickUpper: tickUpper,
1322
+ liquidity: liquidity,
1323
+ amount0: amount0,
1324
+ amount1: amount1
1325
+ });
1326
+
1327
+ tokenIdOf[projectId][terminalToken] = newTokenId;
1328
+ } else {
1329
+ // Zero liquidity means the position cannot be re-created (e.g., price moved
1330
+ // outside tick range making the position single-sided with zero on one side).
1331
+ // Revert to prevent bricking the project's LP — the old position was already
1332
+ // burned by the BURN_POSITION action above, so this protects the invariant
1333
+ // that tokenIdOf is always nonzero for deployed projects.
1334
+ revert JBUniswapV4LPSplitHook_InsufficientLiquidity();
1335
+ }
1336
+
1337
+ // Return any dust left over after minting (due to rounding or single-sided excess) to the project.
1338
+ _handleLeftoverTokens({projectId: projectId, projectToken: projectToken, terminalToken: terminalToken});
1339
+ }
1340
+
1270
1341
  /// @notice Approve an ERC20 token via Permit2 so PositionManager can pull it during SETTLE.
1271
1342
  function _approveViaPermit2(address token, uint256 amount) internal {
1272
1343
  IERC20(token).forceApprove({spender: address(PERMIT2), value: amount});