@bananapus/suckers-v6 0.0.48 → 0.0.49

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.
@@ -183,6 +183,45 @@ library JBSwapPoolLib {
183
183
  }
184
184
  }
185
185
 
186
+ /// @notice Execute the body of a V3 swap callback. Called via DELEGATECALL from the sucker's
187
+ /// `uniswapV3SwapCallback` so the V3 callback logic lives in library bytecode.
188
+ /// @dev DELEGATECALL preserves msg.sender (the V3 pool), allowing pool verification.
189
+ /// @param v3Factory The Uniswap V3 factory for pool verification.
190
+ /// @param amount0Delta The amount of token0 used for the swap.
191
+ /// @param amount1Delta The amount of token1 used for the swap.
192
+ /// @param data Encoded (originalTokenIn, normalizedTokenIn, normalizedTokenOut).
193
+ function executeV3SwapCallback(
194
+ IUniswapV3Factory v3Factory,
195
+ int256 amount0Delta,
196
+ int256 amount1Delta,
197
+ bytes calldata data
198
+ )
199
+ external
200
+ {
201
+ // Decode the callback data packed during _executeV3Swap.
202
+ (address originalTokenIn, address normalizedIn, address normalizedOut) =
203
+ abi.decode(data, (address, address, address));
204
+
205
+ // Verify caller is a legitimate V3 pool via the factory.
206
+ uint24 fee = IUniswapV3Pool(msg.sender).fee();
207
+ address expectedPool = v3Factory.getPool({tokenA: normalizedIn, tokenB: normalizedOut, fee: fee});
208
+ if (msg.sender != expectedPool) revert JBSwapPoolLib_CallerNotPool(msg.sender);
209
+
210
+ // The positive delta is what we owe to the pool.
211
+ // The V3 pool callback guarantees exactly one positive delta for exact-input swaps.
212
+ // forge-lint: disable-next-line(unsafe-typecast)
213
+ uint256 amountToSend = amount0Delta < 0 ? uint256(amount1Delta) : uint256(amount0Delta);
214
+
215
+ // If input is the native token, wrap for V3.
216
+ // When originalTokenIn == NATIVE_TOKEN, normalizedIn is already the wrapped native address.
217
+ if (originalTokenIn == JBConstants.NATIVE_TOKEN) {
218
+ IWrappedNativeToken(normalizedIn).deposit{value: amountToSend}();
219
+ }
220
+
221
+ // Transfer the owed tokens to the V3 pool.
222
+ IERC20(normalizedIn).safeTransfer({to: msg.sender, value: amountToSend});
223
+ }
224
+
186
225
  /// @notice Execute the body of a V4 unlock callback. Called via DELEGATECALL from the sucker's
187
226
  /// `unlockCallback` so the V4 swap logic lives in library bytecode instead of the sucker's.
188
227
  /// @dev DELEGATECALL preserves msg.sender, address(this), and the sucker's token balances.
@@ -265,45 +304,6 @@ library JBSwapPoolLib {
265
304
  return abi.encode(amountOut);
266
305
  }
267
306
 
268
- /// @notice Execute the body of a V3 swap callback. Called via DELEGATECALL from the sucker's
269
- /// `uniswapV3SwapCallback` so the V3 callback logic lives in library bytecode.
270
- /// @dev DELEGATECALL preserves msg.sender (the V3 pool), allowing pool verification.
271
- /// @param v3Factory The Uniswap V3 factory for pool verification.
272
- /// @param amount0Delta The amount of token0 used for the swap.
273
- /// @param amount1Delta The amount of token1 used for the swap.
274
- /// @param data Encoded (originalTokenIn, normalizedTokenIn, normalizedTokenOut).
275
- function executeV3SwapCallback(
276
- IUniswapV3Factory v3Factory,
277
- int256 amount0Delta,
278
- int256 amount1Delta,
279
- bytes calldata data
280
- )
281
- external
282
- {
283
- // Decode the callback data packed during _executeV3Swap.
284
- (address originalTokenIn, address normalizedIn, address normalizedOut) =
285
- abi.decode(data, (address, address, address));
286
-
287
- // Verify caller is a legitimate V3 pool via the factory.
288
- uint24 fee = IUniswapV3Pool(msg.sender).fee();
289
- address expectedPool = v3Factory.getPool({tokenA: normalizedIn, tokenB: normalizedOut, fee: fee});
290
- if (msg.sender != expectedPool) revert JBSwapPoolLib_CallerNotPool(msg.sender);
291
-
292
- // The positive delta is what we owe to the pool.
293
- // The V3 pool callback guarantees exactly one positive delta for exact-input swaps.
294
- // forge-lint: disable-next-line(unsafe-typecast)
295
- uint256 amountToSend = amount0Delta < 0 ? uint256(amount1Delta) : uint256(amount0Delta);
296
-
297
- // If input is the native token, wrap for V3.
298
- // When originalTokenIn == NATIVE_TOKEN, normalizedIn is already the wrapped native address.
299
- if (originalTokenIn == JBConstants.NATIVE_TOKEN) {
300
- IWrappedNativeToken(normalizedIn).deposit{value: amountToSend}();
301
- }
302
-
303
- // Transfer the owed tokens to the V3 pool.
304
- IERC20(normalizedIn).safeTransfer({to: msg.sender, value: amountToSend});
305
- }
306
-
307
307
  //*********************************************************************//
308
308
  // ----------------------- external views ---------------------------- //
309
309
  //*********************************************************************//
@@ -518,46 +518,42 @@ library JBSwapPoolLib {
518
518
  }
519
519
  }
520
520
 
521
- /// @notice Probe a single V4 pool configuration for liquidity.
522
- /// @param poolManager The Uniswap V4 pool manager to query.
523
- /// @param sorted0 The lower-address token in the pair (sorted).
524
- /// @param sorted1 The higher-address token in the pair (sorted).
525
- /// @param hookAddr The hook address to use for this pool configuration.
526
- /// @param tierIndex The fee tier index (0-3) to probe.
527
- /// @return key The constructed pool key for this configuration.
528
- /// @return poolLiquidity The current in-range liquidity of the pool, or 0 if uninitialized.
529
- function _probeV4Pool(
530
- IPoolManager poolManager,
531
- address sorted0,
532
- address sorted1,
533
- address hookAddr,
534
- uint256 tierIndex
521
+ /// @notice Compute the sigmoid slippage tolerance for a given swap.
522
+ /// @param amountIn The amount of input tokens.
523
+ /// @param liquidity The pool's in-range liquidity.
524
+ /// @param tokenOut The output token address.
525
+ /// @param tokenIn The input token address.
526
+ /// @param arithmeticMeanTick The arithmetic mean tick from the TWAP.
527
+ /// @param poolFeeBps The pool's fee in basis points.
528
+ /// @return The slippage tolerance in basis points (out of _SLIPPAGE_DENOMINATOR).
529
+ function _getSlippageTolerance(
530
+ uint256 amountIn,
531
+ uint128 liquidity,
532
+ address tokenOut,
533
+ address tokenIn,
534
+ int24 arithmeticMeanTick,
535
+ uint256 poolFeeBps
535
536
  )
536
537
  internal
537
- view
538
- returns (PoolKey memory key, uint128 poolLiquidity)
538
+ pure
539
+ returns (uint256)
539
540
  {
540
- // Look up fee and tick spacing for this tier index.
541
- (uint24 fee, int24 tickSpacing) = _v4FeeAndTickSpacing(tierIndex);
541
+ // Sort the tokens to determine swap direction.
542
+ address token0 = tokenOut < tokenIn ? tokenOut : tokenIn;
543
+ bool zeroForOne = tokenIn == token0;
542
544
 
543
- // Construct the pool key from the sorted tokens and tier parameters.
544
- key = PoolKey({
545
- currency0: Currency.wrap(sorted0),
546
- currency1: Currency.wrap(sorted1),
547
- fee: fee,
548
- tickSpacing: tickSpacing,
549
- hooks: IHooks(hookAddr)
550
- });
545
+ // Get the sqrt price at the mean tick for impact calculation.
546
+ uint160 sqrtP = V3TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
551
547
 
552
- // Derive the pool ID from the key.
553
- PoolId id = key.toId();
548
+ // If sqrtP is zero, return maximum slippage (accept any output).
549
+ if (sqrtP == 0) return _SLIPPAGE_DENOMINATOR;
554
550
 
555
- // Check if pool is initialized (sqrtPriceX96 != 0).
556
- (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(id);
557
- if (sqrtPriceX96 == 0) return (key, 0);
551
+ // Calculate the price impact of the swap.
552
+ uint256 impact =
553
+ JBSwapLib.calculateImpact({amountIn: amountIn, liquidity: liquidity, sqrtP: sqrtP, zeroForOne: zeroForOne});
558
554
 
559
- // Query the pool's current in-range liquidity.
560
- poolLiquidity = poolManager.getLiquidity(id);
555
+ // Map the impact to a sigmoid slippage tolerance.
556
+ return JBSwapLib.getSlippageTolerance({impact: impact, poolFeeBps: poolFeeBps});
561
557
  }
562
558
 
563
559
  /// @notice Get a TWAP-based quote with dynamic slippage for a V3 pool.
@@ -685,77 +681,6 @@ library JBSwapPoolLib {
685
681
  });
686
682
  }
687
683
 
688
- /// @notice Checks whether a V3 pool can serve the full default TWAP window.
689
- /// @dev Reads the observation ring directly so discovery can skip young pools without reverting. The oldest
690
- /// initialized observation must be at least `_DEFAULT_TWAP_WINDOW` seconds old.
691
- /// @param pool The V3 pool to inspect.
692
- /// @return True if the pool has enough initialized history for a default-window TWAP.
693
- function _v3PoolHasFullTwapHistory(IUniswapV3Pool pool) internal view returns (bool) {
694
- // slot0 gives the current observation cursor and total initialized/available observation slots.
695
- (bool slot0Ok, uint16 observationIndex, uint16 observationCardinality) = _v3ObservationStateOf(pool);
696
- if (!slot0Ok || observationCardinality == 0) return false;
697
-
698
- // In a full ring, the next slot after the cursor is the oldest observation.
699
- uint256 oldestIndex = (uint256(observationIndex) + 1) % uint256(observationCardinality);
700
- (bool observationOk, uint32 observationTimestamp, bool initialized) =
701
- _v3ObservationOf({pool: pool, index: oldestIndex});
702
- if (!observationOk) return false;
703
-
704
- // If the ring has not wrapped yet, slot 0 is the oldest initialized observation.
705
- if (!initialized) {
706
- (observationOk, observationTimestamp, initialized) = _v3ObservationOf({pool: pool, index: 0});
707
- if (!observationOk || !initialized) return false;
708
- }
709
-
710
- return _observationIsOldEnough({observationTimestamp: observationTimestamp, window: _DEFAULT_TWAP_WINDOW});
711
- }
712
-
713
- /// @notice Reads the observation cursor and cardinality from a V3 pool's slot0.
714
- /// @dev Uses `staticcall` instead of the typed interface so malformed or hooklike candidate pools are rejected
715
- /// as unusable candidates without reverting the whole bounded pool-discovery scan.
716
- /// @param pool The V3 pool candidate to inspect.
717
- /// @return ok True if `slot0()` returned enough data to decode.
718
- /// @return observationIndex The pool's current observation cursor.
719
- /// @return observationCardinality The number of initialized/available observation slots.
720
- function _v3ObservationStateOf(IUniswapV3Pool pool)
721
- internal
722
- view
723
- returns (bool ok, uint16 observationIndex, uint16 observationCardinality)
724
- {
725
- // Pool discovery intentionally probes candidate pools in a bounded fee-tier list; failed probes mean "skip".
726
- (bool success, bytes memory data) =
727
- address(pool).staticcall(abi.encodeWithSelector(IUniswapV3PoolState.slot0.selector));
728
- if (!success || data.length < 224) return (false, 0, 0);
729
-
730
- (,, observationIndex, observationCardinality,,,) =
731
- abi.decode(data, (uint160, int24, uint16, uint16, uint16, uint8, bool));
732
- ok = true;
733
- }
734
-
735
- /// @notice Reads one V3 observation.
736
- /// @dev Uses `staticcall` so a bad candidate pool cannot interrupt pool discovery.
737
- /// @param pool The V3 pool candidate to inspect.
738
- /// @param index The observation ring index to read.
739
- /// @return ok True if the observation returned enough data to decode.
740
- /// @return observationTimestamp The timestamp stored at `index`.
741
- /// @return initialized True if the observation slot has been initialized.
742
- function _v3ObservationOf(
743
- IUniswapV3Pool pool,
744
- uint256 index
745
- )
746
- internal
747
- view
748
- returns (bool ok, uint32 observationTimestamp, bool initialized)
749
- {
750
- // Pool discovery intentionally probes candidate pools in a bounded fee-tier list; failed probes mean "skip".
751
- (bool success, bytes memory data) =
752
- address(pool).staticcall(abi.encodeWithSelector(IUniswapV3PoolState.observations.selector, index));
753
- if (!success || data.length < 128) return (false, 0, false);
754
-
755
- (observationTimestamp,,, initialized) = abi.decode(data, (uint32, int56, uint160, bool));
756
- ok = true;
757
- }
758
-
759
684
  /// @notice Check whether an oracle observation is old enough to cover a TWAP window ending at the current block.
760
685
  /// @dev Current or future timestamps are rejected before subtracting so the age check cannot underflow.
761
686
  /// @param observationTimestamp The timestamp recorded in the pool's oracle observation.
@@ -769,27 +694,46 @@ library JBSwapPoolLib {
769
694
  return block.timestamp - observationTimestamp >= window;
770
695
  }
771
696
 
772
- /// @notice Check whether a V4 hooked pool can return cumulative ticks for the required TWAP window.
773
- /// @dev Hookless pools return false. Reverting hooks and hooks that return fewer than two cumulative tick values
774
- /// are treated as unusable for TWAP routing.
775
- /// @param key The V4 pool key whose hook should be probed.
776
- /// @return True if the hook can serve both the historical and current cumulative tick observations.
777
- function _v4PoolHasTwap(PoolKey memory key) internal view returns (bool) {
778
- if (address(key.hooks) == address(0)) return false;
697
+ /// @notice Probe a single V4 pool configuration for liquidity.
698
+ /// @param poolManager The Uniswap V4 pool manager to query.
699
+ /// @param sorted0 The lower-address token in the pair (sorted).
700
+ /// @param sorted1 The higher-address token in the pair (sorted).
701
+ /// @param hookAddr The hook address to use for this pool configuration.
702
+ /// @param tierIndex The fee tier index (0-3) to probe.
703
+ /// @return key The constructed pool key for this configuration.
704
+ /// @return poolLiquidity The current in-range liquidity of the pool, or 0 if uninitialized.
705
+ function _probeV4Pool(
706
+ IPoolManager poolManager,
707
+ address sorted0,
708
+ address sorted1,
709
+ address hookAddr,
710
+ uint256 tierIndex
711
+ )
712
+ internal
713
+ view
714
+ returns (PoolKey memory key, uint128 poolLiquidity)
715
+ {
716
+ // Look up fee and tick spacing for this tier index.
717
+ (uint24 fee, int24 tickSpacing) = _v4FeeAndTickSpacing(tierIndex);
779
718
 
780
- uint32[] memory secondsAgos = new uint32[](2);
781
- secondsAgos[0] = _V4_TWAP_WINDOW;
782
- secondsAgos[1] = 0;
719
+ // Construct the pool key from the sorted tokens and tier parameters.
720
+ key = PoolKey({
721
+ currency0: Currency.wrap(sorted0),
722
+ currency1: Currency.wrap(sorted1),
723
+ fee: fee,
724
+ tickSpacing: tickSpacing,
725
+ hooks: IHooks(hookAddr)
726
+ });
783
727
 
784
- // Pool discovery intentionally probes candidate hooks in a bounded pool list. The seconds-per-liquidity array
785
- // is not needed for the history check.
786
- try IGeomeanOracle(address(key.hooks)).observe({key: key, secondsAgos: secondsAgos}) returns (
787
- int56[] memory tickCumulatives, uint160[] memory
788
- ) {
789
- return tickCumulatives.length >= 2;
790
- } catch {
791
- return false;
792
- }
728
+ // Derive the pool ID from the key.
729
+ PoolId id = key.toId();
730
+
731
+ // Check if pool is initialized (sqrtPriceX96 != 0).
732
+ (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(id);
733
+ if (sqrtPriceX96 == 0) return (key, 0);
734
+
735
+ // Query the pool's current in-range liquidity.
736
+ poolLiquidity = poolManager.getLiquidity(id);
793
737
  }
794
738
 
795
739
  /// @notice Compute the minimum acceptable output using sigmoid slippage at the given tick.
@@ -842,119 +786,104 @@ library JBSwapPoolLib {
842
786
  minAmountOut -= (minAmountOut * slippageTolerance) / _SLIPPAGE_DENOMINATOR;
843
787
  }
844
788
 
845
- /// @notice Compute the sigmoid slippage tolerance for a given swap.
846
- /// @param amountIn The amount of input tokens.
847
- /// @param liquidity The pool's in-range liquidity.
848
- /// @param tokenOut The output token address.
849
- /// @param tokenIn The input token address.
850
- /// @param arithmeticMeanTick The arithmetic mean tick from the TWAP.
851
- /// @param poolFeeBps The pool's fee in basis points.
852
- /// @return The slippage tolerance in basis points (out of _SLIPPAGE_DENOMINATOR).
853
- function _getSlippageTolerance(
854
- uint256 amountIn,
855
- uint128 liquidity,
856
- address tokenOut,
857
- address tokenIn,
858
- int24 arithmeticMeanTick,
859
- uint256 poolFeeBps
789
+ /// @notice Reads one V3 observation.
790
+ /// @dev Uses `staticcall` so a bad candidate pool cannot interrupt pool discovery.
791
+ /// @param pool The V3 pool candidate to inspect.
792
+ /// @param index The observation ring index to read.
793
+ /// @return ok True if the observation returned enough data to decode.
794
+ /// @return observationTimestamp The timestamp stored at `index`.
795
+ /// @return initialized True if the observation slot has been initialized.
796
+ function _v3ObservationOf(
797
+ IUniswapV3Pool pool,
798
+ uint256 index
860
799
  )
861
800
  internal
862
- pure
863
- returns (uint256)
801
+ view
802
+ returns (bool ok, uint32 observationTimestamp, bool initialized)
864
803
  {
865
- // Sort the tokens to determine swap direction.
866
- address token0 = tokenOut < tokenIn ? tokenOut : tokenIn;
867
- bool zeroForOne = tokenIn == token0;
868
-
869
- // Get the sqrt price at the mean tick for impact calculation.
870
- uint160 sqrtP = V3TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
804
+ // Pool discovery intentionally probes candidate pools in a bounded fee-tier list; failed probes mean "skip".
805
+ (bool success, bytes memory data) =
806
+ address(pool).staticcall(abi.encodeWithSelector(IUniswapV3PoolState.observations.selector, index));
807
+ if (!success || data.length < 128) return (false, 0, false);
871
808
 
872
- // If sqrtP is zero, return maximum slippage (accept any output).
873
- if (sqrtP == 0) return _SLIPPAGE_DENOMINATOR;
809
+ (observationTimestamp,,, initialized) = abi.decode(data, (uint32, int56, uint160, bool));
810
+ ok = true;
811
+ }
874
812
 
875
- // Calculate the price impact of the swap.
876
- uint256 impact =
877
- JBSwapLib.calculateImpact({amountIn: amountIn, liquidity: liquidity, sqrtP: sqrtP, zeroForOne: zeroForOne});
813
+ /// @notice Reads the observation cursor and cardinality from a V3 pool's slot0.
814
+ /// @dev Uses `staticcall` instead of the typed interface so malformed or hooklike candidate pools are rejected
815
+ /// as unusable candidates without reverting the whole bounded pool-discovery scan.
816
+ /// @param pool The V3 pool candidate to inspect.
817
+ /// @return ok True if `slot0()` returned enough data to decode.
818
+ /// @return observationIndex The pool's current observation cursor.
819
+ /// @return observationCardinality The number of initialized/available observation slots.
820
+ function _v3ObservationStateOf(IUniswapV3Pool pool)
821
+ internal
822
+ view
823
+ returns (bool ok, uint16 observationIndex, uint16 observationCardinality)
824
+ {
825
+ // Pool discovery intentionally probes candidate pools in a bounded fee-tier list; failed probes mean "skip".
826
+ (bool success, bytes memory data) =
827
+ address(pool).staticcall(abi.encodeWithSelector(IUniswapV3PoolState.slot0.selector));
828
+ if (!success || data.length < 224) return (false, 0, 0);
878
829
 
879
- // Map the impact to a sigmoid slippage tolerance.
880
- return JBSwapLib.getSlippageTolerance({impact: impact, poolFeeBps: poolFeeBps});
830
+ (,, observationIndex, observationCardinality,,,) =
831
+ abi.decode(data, (uint160, int24, uint16, uint16, uint16, uint8, bool));
832
+ ok = true;
881
833
  }
882
834
 
883
- //*********************************************************************//
884
- // ----------------------- internal helpers -------------------------- //
885
- //*********************************************************************//
835
+ /// @notice Checks whether a V3 pool can serve the full default TWAP window.
836
+ /// @dev Reads the observation ring directly so discovery can skip young pools without reverting. The oldest
837
+ /// initialized observation must be at least `_DEFAULT_TWAP_WINDOW` seconds old.
838
+ /// @param pool The V3 pool to inspect.
839
+ /// @return True if the pool has enough initialized history for a default-window TWAP.
840
+ function _v3PoolHasFullTwapHistory(IUniswapV3Pool pool) internal view returns (bool) {
841
+ // slot0 gives the current observation cursor and total initialized/available observation slots.
842
+ (bool slot0Ok, uint16 observationIndex, uint16 observationCardinality) = _v3ObservationStateOf(pool);
843
+ if (!slot0Ok || observationCardinality == 0) return false;
886
844
 
887
- /// @notice Quote via V4 TWAP/spot and execute swap. Separate function for stack isolation.
888
- /// @param config The swap configuration (pool manager, wrapped native token addresses).
889
- /// @param key The V4 pool key to swap through.
890
- /// @param normalizedTokenIn The normalized input token address.
891
- /// @param normalizedTokenOut The normalized output token address.
892
- /// @param amount The amount of input tokens to swap.
893
- /// @return amountOut The amount of output tokens received.
894
- function _quoteAndSwapV4(
895
- SwapConfig memory config,
896
- PoolKey memory key,
897
- address normalizedTokenIn,
898
- address normalizedTokenOut,
899
- address originalTokenIn,
900
- uint256 amount
901
- )
902
- internal
903
- returns (uint256 amountOut)
904
- {
905
- // Get the TWAP-based minimum output for slippage protection.
906
- uint256 minOut = _getV4Quote({
907
- config: config,
908
- key: key,
909
- normalizedTokenIn: normalizedTokenIn,
910
- normalizedTokenOut: normalizedTokenOut,
911
- amount: amount
912
- });
845
+ // In a full ring, the next slot after the cursor is the oldest observation.
846
+ uint256 oldestIndex = (uint256(observationIndex) + 1) % uint256(observationCardinality);
847
+ (bool observationOk, uint32 observationTimestamp, bool initialized) =
848
+ _v3ObservationOf({pool: pool, index: oldestIndex});
849
+ if (!observationOk) return false;
913
850
 
914
- // Execute the swap through the V4 PoolManager.
915
- amountOut = _executeV4Swap({
916
- config: config,
917
- key: key,
918
- normalizedTokenIn: normalizedTokenIn,
919
- originalTokenIn: originalTokenIn,
920
- amount: amount,
921
- minAmountOut: minOut
922
- });
851
+ // If the ring has not wrapped yet, slot 0 is the oldest initialized observation.
852
+ if (!initialized) {
853
+ (observationOk, observationTimestamp, initialized) = _v3ObservationOf({pool: pool, index: 0});
854
+ if (!observationOk || !initialized) return false;
855
+ }
856
+
857
+ return _observationIsOldEnough({observationTimestamp: observationTimestamp, window: _DEFAULT_TWAP_WINDOW});
923
858
  }
924
859
 
925
- /// @notice Quote via V3 TWAP and execute swap. Separate function for stack isolation.
926
- /// @param pool The V3 pool to swap through.
927
- /// @param normalizedTokenIn The normalized input token address.
928
- /// @param normalizedTokenOut The normalized output token address.
929
- /// @param amount The amount of input tokens to swap.
930
- /// @param originalTokenIn The original (pre-normalization) input token address.
931
- /// @return amountOut The amount of output tokens received.
932
- function _quoteAndSwapV3(
933
- IUniswapV3Pool pool,
934
- address normalizedTokenIn,
935
- address normalizedTokenOut,
936
- uint256 amount,
937
- address originalTokenIn
938
- )
939
- internal
940
- returns (uint256 amountOut)
941
- {
942
- // Get the TWAP-based minimum output for slippage protection.
943
- uint256 minOut = _getV3TwapQuote({
944
- pool: pool, normalizedTokenIn: normalizedTokenIn, normalizedTokenOut: normalizedTokenOut, amount: amount
945
- });
860
+ /// @notice Check whether a V4 hooked pool can return cumulative ticks for the required TWAP window.
861
+ /// @dev Hookless pools return false. Reverting hooks and hooks that return fewer than two cumulative tick values
862
+ /// are treated as unusable for TWAP routing.
863
+ /// @param key The V4 pool key whose hook should be probed.
864
+ /// @return True if the hook can serve both the historical and current cumulative tick observations.
865
+ function _v4PoolHasTwap(PoolKey memory key) internal view returns (bool) {
866
+ if (address(key.hooks) == address(0)) return false;
946
867
 
947
- // Execute the swap through the V3 pool.
948
- amountOut = _executeV3Swap({
949
- pool: pool,
950
- normalizedTokenIn: normalizedTokenIn,
951
- normalizedTokenOut: normalizedTokenOut,
952
- amount: amount,
953
- minAmountOut: minOut,
954
- originalTokenIn: originalTokenIn
955
- });
868
+ uint32[] memory secondsAgos = new uint32[](2);
869
+ secondsAgos[0] = _V4_TWAP_WINDOW;
870
+ secondsAgos[1] = 0;
871
+
872
+ // Pool discovery intentionally probes candidate hooks in a bounded pool list. The seconds-per-liquidity array
873
+ // is not needed for the history check.
874
+ try IGeomeanOracle(address(key.hooks)).observe({key: key, secondsAgos: secondsAgos}) returns (
875
+ int56[] memory tickCumulatives, uint160[] memory
876
+ ) {
877
+ return tickCumulatives.length >= 2;
878
+ } catch {
879
+ return false;
880
+ }
956
881
  }
957
882
 
883
+ //*********************************************************************//
884
+ // ----------------------- internal helpers -------------------------- //
885
+ //*********************************************************************//
886
+
958
887
  /// @notice Execute a swap through a V3 pool.
959
888
  /// @param pool The V3 pool to execute the swap on.
960
889
  /// @param normalizedTokenIn The normalized input token address.
@@ -1070,14 +999,6 @@ library JBSwapPoolLib {
1070
999
  amountOut = abi.decode(result, (uint256));
1071
1000
  }
1072
1001
 
1073
- /// @notice Normalize a token address, converting the NATIVE_TOKEN sentinel to the wrapped native token.
1074
- /// @param token The token address to normalize.
1075
- /// @param wrappedNativeToken The wrapped native token address on this chain.
1076
- /// @return The normalized token address.
1077
- function _normalize(address token, address wrappedNativeToken) internal pure returns (address) {
1078
- return token == JBConstants.NATIVE_TOKEN ? wrappedNativeToken : token;
1079
- }
1080
-
1081
1002
  /// @notice Get the Uniswap V3 fee tier for a given index.
1082
1003
  /// @param index The fee tier index (0 = 0.3%, 1 = 0.05%, 2 = 1%, 3 = 0.01%).
1083
1004
  /// @return fee The fee tier in hundredths of a basis point.
@@ -1088,6 +1009,85 @@ library JBSwapPoolLib {
1088
1009
  return 100;
1089
1010
  }
1090
1011
 
1012
+ /// @notice Normalize a token address, converting the NATIVE_TOKEN sentinel to the wrapped native token.
1013
+ /// @param token The token address to normalize.
1014
+ /// @param wrappedNativeToken The wrapped native token address on this chain.
1015
+ /// @return The normalized token address.
1016
+ function _normalize(address token, address wrappedNativeToken) internal pure returns (address) {
1017
+ return token == JBConstants.NATIVE_TOKEN ? wrappedNativeToken : token;
1018
+ }
1019
+
1020
+ /// @notice Quote via V3 TWAP and execute swap. Separate function for stack isolation.
1021
+ /// @param pool The V3 pool to swap through.
1022
+ /// @param normalizedTokenIn The normalized input token address.
1023
+ /// @param normalizedTokenOut The normalized output token address.
1024
+ /// @param amount The amount of input tokens to swap.
1025
+ /// @param originalTokenIn The original (pre-normalization) input token address.
1026
+ /// @return amountOut The amount of output tokens received.
1027
+ function _quoteAndSwapV3(
1028
+ IUniswapV3Pool pool,
1029
+ address normalizedTokenIn,
1030
+ address normalizedTokenOut,
1031
+ uint256 amount,
1032
+ address originalTokenIn
1033
+ )
1034
+ internal
1035
+ returns (uint256 amountOut)
1036
+ {
1037
+ // Get the TWAP-based minimum output for slippage protection.
1038
+ uint256 minOut = _getV3TwapQuote({
1039
+ pool: pool, normalizedTokenIn: normalizedTokenIn, normalizedTokenOut: normalizedTokenOut, amount: amount
1040
+ });
1041
+
1042
+ // Execute the swap through the V3 pool.
1043
+ amountOut = _executeV3Swap({
1044
+ pool: pool,
1045
+ normalizedTokenIn: normalizedTokenIn,
1046
+ normalizedTokenOut: normalizedTokenOut,
1047
+ amount: amount,
1048
+ minAmountOut: minOut,
1049
+ originalTokenIn: originalTokenIn
1050
+ });
1051
+ }
1052
+
1053
+ /// @notice Quote via V4 TWAP/spot and execute swap. Separate function for stack isolation.
1054
+ /// @param config The swap configuration (pool manager, wrapped native token addresses).
1055
+ /// @param key The V4 pool key to swap through.
1056
+ /// @param normalizedTokenIn The normalized input token address.
1057
+ /// @param normalizedTokenOut The normalized output token address.
1058
+ /// @param amount The amount of input tokens to swap.
1059
+ /// @return amountOut The amount of output tokens received.
1060
+ function _quoteAndSwapV4(
1061
+ SwapConfig memory config,
1062
+ PoolKey memory key,
1063
+ address normalizedTokenIn,
1064
+ address normalizedTokenOut,
1065
+ address originalTokenIn,
1066
+ uint256 amount
1067
+ )
1068
+ internal
1069
+ returns (uint256 amountOut)
1070
+ {
1071
+ // Get the TWAP-based minimum output for slippage protection.
1072
+ uint256 minOut = _getV4Quote({
1073
+ config: config,
1074
+ key: key,
1075
+ normalizedTokenIn: normalizedTokenIn,
1076
+ normalizedTokenOut: normalizedTokenOut,
1077
+ amount: amount
1078
+ });
1079
+
1080
+ // Execute the swap through the V4 PoolManager.
1081
+ amountOut = _executeV4Swap({
1082
+ config: config,
1083
+ key: key,
1084
+ normalizedTokenIn: normalizedTokenIn,
1085
+ originalTokenIn: originalTokenIn,
1086
+ amount: amount,
1087
+ minAmountOut: minOut
1088
+ });
1089
+ }
1090
+
1091
1091
  /// @notice Get the Uniswap V4 fee and tick spacing for a given tier index.
1092
1092
  /// @param index The fee tier index (0 = 0.3%/60, 1 = 0.05%/10, 2 = 1%/200, 3 = 0.01%/1).
1093
1093
  /// @return fee The fee in hundredths of a basis point.
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @notice Scratch space used while aggregating one value per peer chain across a project's suckers.
5
+ /// @dev Sized to the project's sucker count for in-memory de-duplication; `chainCount` tracks populated entries.
6
+ /// @custom:member chainIds The peer chain IDs that have been observed.
7
+ /// @custom:member values The selected aggregate value for each observed peer chain.
8
+ /// @custom:member snapshotTimestamps The freshness key associated with each selected value.
9
+ /// @custom:member hasActiveValue Whether the selected value came from an active sucker instead of a deprecated
10
+ /// fallback.
11
+ /// @custom:member chainCount The number of populated peer-chain entries.
12
+ struct PeerValueScratch {
13
+ uint256[] chainIds;
14
+ uint256[] values;
15
+ uint256[] snapshotTimestamps;
16
+ bool[] hasActiveValue;
17
+ uint256 chainCount;
18
+ }