@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 +1 -1
- package/src/JBUniswapV4LPSplitHook.sol +168 -97
package/package.json
CHANGED
|
@@ -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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
753
|
-
|
|
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});
|