@bananapus/suckers-v6 0.0.72 → 0.0.73

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.
@@ -1,1142 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // External packages (alphabetized).
5
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6
- import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7
- import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
8
- import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
9
- import {IUniswapV3PoolState} from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolState.sol";
10
- import {TickMath as V3TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
11
- import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";
12
- import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
13
- import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
14
- import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
15
- import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
16
- import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
17
- import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
18
- import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
19
- import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
20
-
21
- // Local: libraries (alphabetized).
22
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
23
-
24
- // Local: interfaces (alphabetized).
25
- import {IGeomeanOracle} from "../interfaces/IGeomeanOracle.sol";
26
- import {IWrappedNativeToken} from "../interfaces/IWrappedNativeToken.sol";
27
-
28
- // Local: libraries (alphabetized).
29
- import {JBSwapLib} from "./JBSwapLib.sol";
30
-
31
- /// @notice Library with Uniswap pool discovery, TWAP quoting, and swap execution logic extracted from
32
- /// JBSwapCCIPSucker to reduce child contract sizes.
33
- /// @dev These are `external` library functions, deployed as a separate contract and called via DELEGATECALL.
34
- /// Swap callbacks (`uniswapV3SwapCallback`, `unlockCallback`) remain on the calling contract.
35
- library JBSwapPoolLib {
36
- // A library for converting pool keys to pool IDs.
37
- using PoolIdLibrary for PoolKey;
38
- // A library for reading pool state from the pool manager.
39
- using StateLibrary for IPoolManager;
40
- // A library for extracting individual amounts from balance deltas.
41
- using BalanceDeltaLibrary for BalanceDelta;
42
- // A library for safe ERC-20 transfers.
43
- using SafeERC20 for IERC20;
44
-
45
- //*********************************************************************//
46
- // --------------------------- custom errors ------------------------- //
47
- //*********************************************************************//
48
-
49
- /// @notice Thrown when a swap amount exceeds the `uint128` range required by the pool interface.
50
- error JBSwapPoolLib_AmountOverflow(uint256 amount);
51
-
52
- /// @notice Thrown when a swap callback is invoked by an address other than the expected pool.
53
- error JBSwapPoolLib_CallerNotPool(address caller);
54
-
55
- /// @notice Thrown when a pool's oracle history is too short to serve the required TWAP window.
56
- error JBSwapPoolLib_InsufficientTwapHistory(address pool, uint256 availableWindow, uint256 requiredWindow);
57
-
58
- /// @notice Thrown when the selected pool has no liquidity to swap against.
59
- error JBSwapPoolLib_NoLiquidity(address pool, PoolId poolId);
60
-
61
- /// @notice Thrown when no eligible V3 or V4 pool can be found for the given token pair.
62
- error JBSwapPoolLib_NoPool(address tokenIn, address tokenOut);
63
-
64
- /// @notice Thrown when a swap consumes less than the full requested input amount.
65
- error JBSwapPoolLib_PartialFill(uint256 consumed, uint256 requested);
66
-
67
- /// @notice Thrown when a swap's output is below the minimum acceptable amount after slippage.
68
- error JBSwapPoolLib_SlippageExceeded(uint256 amountOut, uint256 minAmountOut);
69
-
70
- //*********************************************************************//
71
- // ------------------------ private constants ------------------------ //
72
- //*********************************************************************//
73
-
74
- /// @dev The default TWAP observation window in seconds (10 minutes).
75
- uint32 private constant _DEFAULT_TWAP_WINDOW = 600;
76
-
77
- /// @dev The TWAP observation window used for V4 geomean oracle queries in seconds (2 minutes).
78
- uint32 private constant _V4_TWAP_WINDOW = 120;
79
-
80
- /// @dev The denominator for slippage tolerance calculations (basis points).
81
- uint256 private constant _SLIPPAGE_DENOMINATOR = 10_000;
82
-
83
- //*********************************************************************//
84
- // ------------------------------ structs ---------------------------- //
85
- //*********************************************************************//
86
-
87
- /// @notice Configuration context for swap execution, packed into a struct to avoid stack-too-deep.
88
- /// @custom:member v3Factory The Uniswap V3 factory used for pool discovery.
89
- /// @custom:member poolManager The Uniswap V4 pool manager used for V4 pool queries and swaps.
90
- /// @custom:member univ4Hook The address of the Uniswap V4 hook contract to search for hooked pools.
91
- /// @custom:member wrappedNativeToken The address of the ERC-20 wrapper for the chain's native token (e.g. WETH on
92
- /// Ethereum).
93
- struct SwapConfig {
94
- IUniswapV3Factory v3Factory;
95
- IPoolManager poolManager;
96
- address univ4Hook;
97
- address wrappedNativeToken;
98
- }
99
-
100
- //*********************************************************************//
101
- // ---------------------- external transactions ---------------------- //
102
- //*********************************************************************//
103
-
104
- /// @notice Execute a full swap: discover the best V3/V4 pool, quote via TWAP, execute the swap.
105
- /// @dev Runs via DELEGATECALL so the calling contract's balance and callbacks are used.
106
- /// @param config The swap configuration (factory, pool manager, hook, wrapped native token addresses).
107
- /// @param tokenIn The input token (raw address, may be NATIVE_TOKEN sentinel).
108
- /// @param tokenOut The output token (raw address).
109
- /// @param amount The amount of input tokens to swap.
110
- /// @param minAmountOut Caller-provided minimum output. When non-zero, TWAP quoting is skipped and this value
111
- /// is used directly as the slippage floor. When zero, the existing TWAP-based quoting logic applies.
112
- /// @return amountOut The amount of output tokens received.
113
- function executeSwap(
114
- SwapConfig memory config,
115
- address tokenIn,
116
- address tokenOut,
117
- uint256 amount,
118
- uint256 minAmountOut
119
- )
120
- external
121
- returns (uint256 amountOut)
122
- {
123
- // Normalize NATIVE_TOKEN sentinel to the wrapped native token for pool lookups.
124
- address normalizedIn = _normalize({token: tokenIn, wrappedNativeToken: config.wrappedNativeToken});
125
- address normalizedOut = _normalize({token: tokenOut, wrappedNativeToken: config.wrappedNativeToken});
126
-
127
- // No swap needed if tokens are the same after normalization (e.g., NATIVE_TOKEN and wrapped native token).
128
- if (normalizedIn == normalizedOut) return amount;
129
-
130
- // Discover the most liquid pool across V3 and V4.
131
- (bool isV4, IUniswapV3Pool v3Pool, PoolKey memory v4Key) =
132
- _discoverPool({config: config, normalizedTokenIn: normalizedIn, normalizedTokenOut: normalizedOut});
133
-
134
- // Revert if no pool was found on either protocol.
135
- if (!isV4 && address(v3Pool) == address(0)) {
136
- revert JBSwapPoolLib_NoPool({tokenIn: normalizedIn, tokenOut: normalizedOut});
137
- }
138
-
139
- if (isV4) {
140
- if (minAmountOut > 0) {
141
- // Caller-provided quote — skip TWAP, execute directly.
142
- amountOut = _executeV4Swap({
143
- config: config,
144
- key: v4Key,
145
- normalizedTokenIn: normalizedIn,
146
- originalTokenIn: tokenIn,
147
- amount: amount,
148
- minAmountOut: minAmountOut
149
- });
150
- } else {
151
- // Quote via V4 TWAP/spot and execute swap through PoolManager.
152
- amountOut = _quoteAndSwapV4({
153
- config: config,
154
- key: v4Key,
155
- normalizedTokenIn: normalizedIn,
156
- normalizedTokenOut: normalizedOut,
157
- originalTokenIn: tokenIn,
158
- amount: amount
159
- });
160
- }
161
- } else {
162
- if (minAmountOut > 0) {
163
- // Caller-provided quote — skip TWAP, execute directly.
164
- amountOut = _executeV3Swap({
165
- pool: v3Pool,
166
- normalizedTokenIn: normalizedIn,
167
- normalizedTokenOut: normalizedOut,
168
- amount: amount,
169
- minAmountOut: minAmountOut,
170
- originalTokenIn: tokenIn
171
- });
172
- } else {
173
- // Quote via V3 TWAP and execute swap through the V3 pool.
174
- amountOut = _quoteAndSwapV3({
175
- pool: v3Pool,
176
- normalizedTokenIn: normalizedIn,
177
- normalizedTokenOut: normalizedOut,
178
- amount: amount,
179
- originalTokenIn: tokenIn
180
- });
181
- }
182
- // V3 outputs wrapped native token for native pairs — unwrap to raw native token.
183
- if (tokenOut == JBConstants.NATIVE_TOKEN) {
184
- IWrappedNativeToken(config.wrappedNativeToken).withdraw(amountOut);
185
- }
186
- }
187
-
188
- // V4 outputs native tokens for wrapped-native-paired pools. If the caller requested the wrapped form
189
- // (not NATIVE_TOKEN), wrap the received native tokens so the caller gets the token they expect.
190
- if (isV4 && tokenOut != JBConstants.NATIVE_TOKEN && normalizedOut == config.wrappedNativeToken) {
191
- // Wrap through the configured wrapped native token contract.
192
- IWrappedNativeToken(config.wrappedNativeToken).deposit{value: amountOut}();
193
- }
194
- }
195
-
196
- /// @notice Execute the body of a V3 swap callback. Called via DELEGATECALL from the sucker's
197
- /// `uniswapV3SwapCallback` so the V3 callback logic lives in library bytecode.
198
- /// @dev DELEGATECALL preserves msg.sender (the V3 pool), allowing pool verification.
199
- /// @param v3Factory The Uniswap V3 factory for pool verification.
200
- /// @param amount0Delta The amount of token0 used for the swap.
201
- /// @param amount1Delta The amount of token1 used for the swap.
202
- /// @param data Encoded (originalTokenIn, normalizedTokenIn, normalizedTokenOut).
203
- function executeV3SwapCallback(
204
- IUniswapV3Factory v3Factory,
205
- int256 amount0Delta,
206
- int256 amount1Delta,
207
- bytes calldata data
208
- )
209
- external
210
- {
211
- // Decode the callback data packed during _executeV3Swap.
212
- (address originalTokenIn, address normalizedIn, address normalizedOut) =
213
- abi.decode(data, (address, address, address));
214
-
215
- // Verify caller is a legitimate V3 pool via the factory.
216
- uint24 fee = IUniswapV3Pool(msg.sender).fee();
217
- address expectedPool = v3Factory.getPool({tokenA: normalizedIn, tokenB: normalizedOut, fee: fee});
218
- if (msg.sender != expectedPool) revert JBSwapPoolLib_CallerNotPool(msg.sender);
219
-
220
- // The positive delta is what we owe to the pool.
221
- // The V3 pool callback guarantees exactly one positive delta for exact-input swaps.
222
- // forge-lint: disable-next-line(unsafe-typecast)
223
- uint256 amountToSend = amount0Delta < 0 ? uint256(amount1Delta) : uint256(amount0Delta);
224
-
225
- // If input is the native token, wrap for V3.
226
- // When originalTokenIn == NATIVE_TOKEN, normalizedIn is already the wrapped native address.
227
- if (originalTokenIn == JBConstants.NATIVE_TOKEN) {
228
- IWrappedNativeToken(normalizedIn).deposit{value: amountToSend}();
229
- }
230
-
231
- // Transfer the owed tokens to the V3 pool.
232
- IERC20(normalizedIn).safeTransfer({to: msg.sender, value: amountToSend});
233
- }
234
-
235
- /// @notice Execute the body of a V4 unlock callback. Called via DELEGATECALL from the sucker's
236
- /// `unlockCallback` so the V4 swap logic lives in library bytecode instead of the sucker's.
237
- /// @dev DELEGATECALL preserves msg.sender, address(this), and the sucker's token balances.
238
- /// @param poolManager The Uniswap V4 PoolManager.
239
- /// @param data The encoded swap parameters from PoolManager.unlock().
240
- /// @return Encoded output amount.
241
- function executeV4UnlockCallback(IPoolManager poolManager, bytes calldata data) external returns (bytes memory) {
242
- // Decode the swap parameters packed during _executeV4Swap.
243
- (
244
- PoolKey memory key,
245
- bool zeroForOne,
246
- int256 amountSpecified,
247
- uint160 sqrtPriceLimitX96,
248
- uint256 minAmountOut,
249
- address wrappedNativeToken
250
- ) = abi.decode(data, (PoolKey, bool, int256, uint160, uint256, address));
251
-
252
- uint256 amountIn;
253
- uint256 amountOut;
254
-
255
- {
256
- // Execute the swap through the V4 PoolManager.
257
- BalanceDelta delta = poolManager.swap({
258
- key: key,
259
- params: SwapParams({
260
- zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: sqrtPriceLimitX96
261
- }),
262
- hookData: ""
263
- });
264
-
265
- // V4 sign convention: negative = we owe (input), positive = we're owed (output).
266
- int128 delta0 = delta.amount0();
267
- int128 delta1 = delta.amount1();
268
-
269
- // Extract input and output amounts based on swap direction.
270
- if (zeroForOne) {
271
- // The PoolManager returns the exact-input delta as negative and output delta as positive.
272
- // forge-lint: disable-next-line(unsafe-typecast)
273
- amountIn = uint256(uint128(-delta0));
274
- // forge-lint: disable-next-line(unsafe-typecast)
275
- amountOut = uint256(uint128(delta1));
276
- } else {
277
- // The PoolManager returns the exact-input delta as negative and output delta as positive.
278
- // forge-lint: disable-next-line(unsafe-typecast)
279
- amountIn = uint256(uint128(-delta1));
280
- // forge-lint: disable-next-line(unsafe-typecast)
281
- amountOut = uint256(uint128(delta0));
282
- }
283
-
284
- // Exact-input V4 swaps are encoded with a negative amount.
285
- // forge-lint: disable-next-line(unsafe-typecast)
286
- uint256 requestedAmount = uint256(-amountSpecified);
287
- if (amountIn < requestedAmount) {
288
- revert JBSwapPoolLib_PartialFill({consumed: amountIn, requested: requestedAmount});
289
- }
290
-
291
- // Enforce the minimum output from the TWAP quote.
292
- if (amountOut < minAmountOut) {
293
- revert JBSwapPoolLib_SlippageExceeded({amountOut: amountOut, minAmountOut: minAmountOut});
294
- }
295
- }
296
-
297
- // Settle input (pay what we owe to the PoolManager).
298
- Currency inputCurrency = zeroForOne ? key.currency0 : key.currency1;
299
- if (Currency.unwrap(inputCurrency) == address(0)) {
300
- // Native token: unwrap wrapped native token if the caller holds it, then settle directly.
301
- // The buyback hook holds wrapped native tokens (needs unwrap), while suckers hold raw ETH (skip unwrap).
302
- if (wrappedNativeToken != address(0)) {
303
- uint256 wrappedBalance = IERC20(wrappedNativeToken).balanceOf(address(this));
304
- if (wrappedBalance >= amountIn) {
305
- IWrappedNativeToken(wrappedNativeToken).withdraw(amountIn);
306
- }
307
- }
308
- poolManager.settle{value: amountIn}();
309
- } else {
310
- // ERC-20: sync the currency balance, transfer tokens, then settle.
311
- poolManager.sync(inputCurrency);
312
- IERC20(Currency.unwrap(inputCurrency)).safeTransfer({to: address(poolManager), value: amountIn});
313
- poolManager.settle();
314
- }
315
-
316
- // Take output (receive what the PoolManager owes us).
317
- Currency outputCurrency = zeroForOne ? key.currency1 : key.currency0;
318
- poolManager.take({currency: outputCurrency, to: address(this), amount: amountOut});
319
-
320
- // Return the output amount to the caller.
321
- return abi.encode(amountOut);
322
- }
323
-
324
- //*********************************************************************//
325
- // ----------------------- external views ---------------------------- //
326
- //*********************************************************************//
327
-
328
- /// @notice Externally accessible pool discovery for testing and off-chain queries.
329
- /// @param config The swap configuration (factory, pool manager, etc.).
330
- /// @param normalizedTokenIn The normalized input token address (wrapped native token, not NATIVE_TOKEN).
331
- /// @param normalizedTokenOut The normalized output token address.
332
- /// @return isV4 Whether the best pool is a V4 pool.
333
- /// @return v3Pool The best V3 pool (or address(0) if V4 is better).
334
- /// @return v4Key The best V4 pool key (if V4 is better).
335
- function discoverPool(
336
- SwapConfig memory config,
337
- address normalizedTokenIn,
338
- address normalizedTokenOut
339
- )
340
- external
341
- view
342
- returns (bool isV4, IUniswapV3Pool v3Pool, PoolKey memory v4Key)
343
- {
344
- return
345
- _discoverPool({
346
- config: config, normalizedTokenIn: normalizedTokenIn, normalizedTokenOut: normalizedTokenOut
347
- });
348
- }
349
-
350
- //*********************************************************************//
351
- // ----------------------- internal views ---------------------------- //
352
- //*********************************************************************//
353
-
354
- /// @notice Find the highest liquidity pool across all V3 fee tiers and V4 pool configurations.
355
- /// @param config The swap configuration (factory, pool manager, hook, wrapped native token addresses).
356
- /// @param normalizedTokenIn The normalized input token address (wrapped native token, not NATIVE_TOKEN).
357
- /// @param normalizedTokenOut The normalized output token address.
358
- /// @return isV4 Whether the best pool is a V4 pool.
359
- /// @return v3Pool The best V3 pool (or address(0) if V4 is better).
360
- /// @return v4Key The best V4 pool key (if V4 is better).
361
- function _discoverPool(
362
- SwapConfig memory config,
363
- address normalizedTokenIn,
364
- address normalizedTokenOut
365
- )
366
- internal
367
- view
368
- returns (bool isV4, IUniswapV3Pool v3Pool, PoolKey memory v4Key)
369
- {
370
- // Track the best TWAP-ready liquidity found across both protocols.
371
- uint128 bestLiquidity;
372
-
373
- // Search V3 pools across all fee tiers. Only pools with the full TWAP window are eligible.
374
- (v3Pool, bestLiquidity) = _discoverV3Pool({
375
- v3Factory: config.v3Factory, normalizedTokenIn: normalizedTokenIn, normalizedTokenOut: normalizedTokenOut
376
- });
377
-
378
- // If a V4 pool manager is configured, also search V4 pools.
379
- if (address(config.poolManager) != address(0)) {
380
- (PoolKey memory v4Candidate, uint128 v4Liquidity, bool v4UsesTwap) = _discoverV4Pool({
381
- config: config, normalizedTokenIn: normalizedTokenIn, normalizedTokenOut: normalizedTokenOut
382
- });
383
-
384
- // Select V4 if no TWAP-ready V3 exists, or if a TWAP-ready V4 beats the V3 route.
385
- // Hookless V4 spot pools remain a last-resort fallback and cannot outrank V3 TWAP.
386
- if (v4Liquidity != 0 && (bestLiquidity == 0 || (v4UsesTwap && v4Liquidity > bestLiquidity))) {
387
- isV4 = true;
388
- v3Pool = IUniswapV3Pool(address(0));
389
- v4Key = v4Candidate;
390
- }
391
- }
392
- }
393
-
394
- /// @notice Search V3 pools across 4 fee tiers for the highest liquidity.
395
- /// @param v3Factory The Uniswap V3 factory to query for pools.
396
- /// @param normalizedTokenIn The normalized input token address.
397
- /// @param normalizedTokenOut The normalized output token address.
398
- /// @return bestPool The V3 pool with the highest liquidity.
399
- /// @return bestLiquidity The liquidity of the best pool found.
400
- function _discoverV3Pool(
401
- IUniswapV3Factory v3Factory,
402
- address normalizedTokenIn,
403
- address normalizedTokenOut
404
- )
405
- internal
406
- view
407
- returns (IUniswapV3Pool bestPool, uint128 bestLiquidity)
408
- {
409
- // Return early if no V3 factory is configured.
410
- if (address(v3Factory) == address(0)) return (bestPool, bestLiquidity);
411
-
412
- // Iterate over all 4 standard fee tiers.
413
- for (uint256 i; i < 4;) {
414
- address poolAddr =
415
- v3Factory.getPool({tokenA: normalizedTokenIn, tokenB: normalizedTokenOut, fee: _feeTier(i)});
416
-
417
- if (poolAddr != address(0)) {
418
- // Skip young V3 pools instead of falling back to spot. No-quote programmatic swaps rely on the TWAP
419
- // floor, so a pool must already cover the full observation window before it can route funds.
420
- if (!_v3PoolHasFullTwapHistory(IUniswapV3Pool(poolAddr))) {
421
- unchecked {
422
- ++i;
423
- }
424
- continue;
425
- }
426
-
427
- // Query the pool's current in-range liquidity.
428
- uint128 poolLiquidity = IUniswapV3Pool(poolAddr).liquidity();
429
-
430
- // Track the pool with the highest liquidity.
431
- if (poolLiquidity > bestLiquidity) {
432
- bestLiquidity = poolLiquidity;
433
- bestPool = IUniswapV3Pool(poolAddr);
434
- }
435
- }
436
-
437
- unchecked {
438
- ++i;
439
- }
440
- }
441
- }
442
-
443
- /// @notice Search V4 pools across 4 fee tiers and 2 hook configs for the best eligible liquidity.
444
- /// @dev TWAP-capable hooked pools are preferred over hookless spot pools. Broken hooked pools are skipped.
445
- /// @param config The swap configuration (pool manager, hook, wrapped native token addresses).
446
- /// @param normalizedTokenIn The normalized input token address.
447
- /// @param normalizedTokenOut The normalized output token address.
448
- /// @return bestKey The selected V4 pool key.
449
- /// @return bestLiquidity The liquidity of the best V4 pool found.
450
- /// @return bestUsesTwap Whether the selected V4 pool has a working TWAP hook.
451
- function _discoverV4Pool(
452
- SwapConfig memory config,
453
- address normalizedTokenIn,
454
- address normalizedTokenOut
455
- )
456
- internal
457
- view
458
- returns (PoolKey memory bestKey, uint128 bestLiquidity, bool bestUsesTwap)
459
- {
460
- // Keep the best hookless pool separate. Hookless V4 pools only provide spot pricing, so they are a fallback
461
- // candidate and should not beat a hooked pool that can serve the TWAP window.
462
- PoolKey memory bestSpotKey;
463
- uint128 bestSpotLiquidity;
464
-
465
- // Convert to V4 convention before sorting: the wrapped native token represents native tokens in the calling
466
- // code, while V4 pool keys use address(0) for native tokens.
467
- address sorted0;
468
- address sorted1;
469
- {
470
- address v4In = normalizedTokenIn == config.wrappedNativeToken ? address(0) : normalizedTokenIn;
471
- address v4Out = normalizedTokenOut == config.wrappedNativeToken ? address(0) : normalizedTokenOut;
472
-
473
- // Sort tokens to match the V4 currency ordering convention.
474
- (sorted0, sorted1) = v4In < v4Out ? (v4In, v4Out) : (v4Out, v4In);
475
- }
476
-
477
- // Iterate over all 4 standard fee tiers.
478
- for (uint256 i; i < 4;) {
479
- // For each fee tier, probe both hookless and hooked pools.
480
- for (uint256 j; j < 2;) {
481
- // Use no hook for j==0, configured hook for j==1.
482
- address hookAddr = j == 0 ? address(0) : config.univ4Hook;
483
-
484
- // Skip the hooked probe if no hook address is configured.
485
- if (j != 0 && hookAddr == address(0)) {
486
- unchecked {
487
- ++j;
488
- }
489
- continue;
490
- }
491
-
492
- // Probe this specific pool configuration for liquidity.
493
- (PoolKey memory key, uint128 liq) = _probeV4Pool({
494
- poolManager: config.poolManager,
495
- sorted0: sorted0,
496
- sorted1: sorted1,
497
- hookAddr: hookAddr,
498
- tierIndex: i
499
- });
500
-
501
- if (liq != 0) {
502
- if (hookAddr == address(0)) {
503
- // Hookless pools can only be quoted at spot. Save the deepest one, but leave `bestKey`
504
- // reserved for TWAP-capable hooked pools.
505
- if (liq > bestSpotLiquidity) {
506
- bestSpotLiquidity = liq;
507
- bestSpotKey = key;
508
- }
509
- } else if (_v4PoolHasTwap(key)) {
510
- // Hooked pools are only eligible if their hook can serve the configured TWAP window. Among
511
- // those, route through the deepest pool.
512
- if (liq > bestLiquidity) {
513
- bestLiquidity = liq;
514
- bestKey = key;
515
- bestUsesTwap = true;
516
- }
517
- }
518
- }
519
-
520
- unchecked {
521
- ++j;
522
- }
523
- }
524
-
525
- unchecked {
526
- ++i;
527
- }
528
- }
529
-
530
- if (bestLiquidity == 0) {
531
- // No hooked pool could serve TWAP, so return the deepest hookless spot pool as the V4 fallback. The
532
- // caller still compares this against V3 TWAP liquidity before choosing a route.
533
- bestKey = bestSpotKey;
534
- bestLiquidity = bestSpotLiquidity;
535
- }
536
- }
537
-
538
- /// @notice Compute the sigmoid slippage tolerance for a given swap.
539
- /// @param amountIn The amount of input tokens.
540
- /// @param liquidity The pool's in-range liquidity.
541
- /// @param tokenOut The output token address.
542
- /// @param tokenIn The input token address.
543
- /// @param arithmeticMeanTick The arithmetic mean tick from the TWAP.
544
- /// @param poolFeeBps The pool's fee in basis points.
545
- /// @return The slippage tolerance in basis points (out of _SLIPPAGE_DENOMINATOR).
546
- function _getSlippageTolerance(
547
- uint256 amountIn,
548
- uint128 liquidity,
549
- address tokenOut,
550
- address tokenIn,
551
- int24 arithmeticMeanTick,
552
- uint256 poolFeeBps
553
- )
554
- internal
555
- pure
556
- returns (uint256)
557
- {
558
- // Sort the tokens to determine swap direction.
559
- address token0 = tokenOut < tokenIn ? tokenOut : tokenIn;
560
- bool zeroForOne = tokenIn == token0;
561
-
562
- // Get the sqrt price at the mean tick for impact calculation.
563
- uint160 sqrtP = V3TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
564
-
565
- // If sqrtP is zero, return maximum slippage (accept any output).
566
- if (sqrtP == 0) return _SLIPPAGE_DENOMINATOR;
567
-
568
- // Calculate the price impact of the swap.
569
- uint256 impact =
570
- JBSwapLib.calculateImpact({amountIn: amountIn, liquidity: liquidity, sqrtP: sqrtP, zeroForOne: zeroForOne});
571
-
572
- // Map the impact to a sigmoid slippage tolerance.
573
- return JBSwapLib.getSlippageTolerance({impact: impact, poolFeeBps: poolFeeBps});
574
- }
575
-
576
- /// @notice Get a TWAP-based quote with dynamic slippage for a V3 pool.
577
- /// @param pool The V3 pool to get the TWAP quote from.
578
- /// @param normalizedTokenIn The normalized input token address.
579
- /// @param normalizedTokenOut The normalized output token address.
580
- /// @param amount The amount of input tokens to quote.
581
- /// @return minAmountOut The minimum acceptable output amount after slippage.
582
- function _getV3TwapQuote(
583
- IUniswapV3Pool pool,
584
- address normalizedTokenIn,
585
- address normalizedTokenOut,
586
- uint256 amount
587
- )
588
- internal
589
- view
590
- returns (uint256 minAmountOut)
591
- {
592
- // Convert the pool fee from hundredths-of-a-bip to basis points.
593
- uint256 feeBps = uint256(pool.fee()) / 100;
594
-
595
- // Get the oldest observation available in the pool's oracle.
596
- uint32 oldestObservation = OracleLibrary.getOldestObservationSecondsAgo(address(pool));
597
-
598
- // Revert if the pool has no TWAP history at all.
599
- if (oldestObservation == 0) {
600
- revert JBSwapPoolLib_InsufficientTwapHistory({
601
- pool: address(pool), availableWindow: oldestObservation, requiredWindow: _DEFAULT_TWAP_WINDOW
602
- });
603
- }
604
-
605
- // Revert if the available history cannot serve the full default TWAP window.
606
- if (oldestObservation < _DEFAULT_TWAP_WINDOW) {
607
- revert JBSwapPoolLib_InsufficientTwapHistory({
608
- pool: address(pool), availableWindow: oldestObservation, requiredWindow: _DEFAULT_TWAP_WINDOW
609
- });
610
- }
611
-
612
- // Consult the V3 oracle for the arithmetic mean tick and harmonic mean liquidity.
613
- (int24 arithmeticMeanTick, uint128 liquidity) =
614
- OracleLibrary.consult({pool: address(pool), secondsAgo: _DEFAULT_TWAP_WINDOW});
615
-
616
- // Revert if the pool has no in-range liquidity.
617
- if (liquidity == 0) {
618
- revert JBSwapPoolLib_NoLiquidity({pool: address(pool), poolId: PoolId.wrap(bytes32(0))});
619
- }
620
-
621
- // Compute the minimum output with sigmoid-based dynamic slippage.
622
- minAmountOut = _quoteWithSlippage({
623
- amount: amount,
624
- liquidity: liquidity,
625
- tokenIn: normalizedTokenIn,
626
- tokenOut: normalizedTokenOut,
627
- tick: arithmeticMeanTick,
628
- poolFeeBps: feeBps
629
- });
630
- }
631
-
632
- /// @notice Get a V4 quote with dynamic slippage. Hooked pools must serve TWAP for BOTH the price tick and the
633
- /// liquidity input into sigmoid slippage; hookless pools fall back to spot for both.
634
- /// @dev Matches the buyback hook's TWAP pattern (`JBSwapLib.getQuoteFromOracle`): for hooked routes we derive
635
- /// `harmonicMeanLiquidity` from the oracle's `secondsPerLiquidityCumulativeX128s`, not from
636
- /// `PoolManager.getLiquidity` (which returns current spot and is JIT-LP-manipulable across a single block).
637
- /// Sigmoid slippage tolerance is driven by `amountIn / liquidity`; feeding spot liquidity into a TWAP-derived
638
- /// tick lets an LP shrink the denominator in the same block as a CCIP delivery, ballooning the tolerance to
639
- /// `MAX_SLIPPAGE` (88%) for a one-shot per-batch immutable conversion rate that all claimers then inherit.
640
- /// @param config The swap configuration (pool manager, wrapped native token addresses).
641
- /// @param key The V4 pool key to quote against.
642
- /// @param normalizedTokenIn The normalized input token address.
643
- /// @param normalizedTokenOut The normalized output token address.
644
- /// @param amount The amount of input tokens to quote.
645
- /// @return minAmountOut The minimum acceptable output amount after slippage.
646
- function _getV4Quote(
647
- SwapConfig memory config,
648
- PoolKey memory key,
649
- address normalizedTokenIn,
650
- address normalizedTokenOut,
651
- uint256 amount
652
- )
653
- internal
654
- view
655
- returns (uint256 minAmountOut)
656
- {
657
- // Convert the pool fee from hundredths-of-a-bip to basis points.
658
- uint256 feeBps = uint256(key.fee) / 100;
659
- int24 tick;
660
- uint128 liquidity;
661
- PoolId id = key.toId();
662
-
663
- {
664
- // If the pool has a hook, require a TWAP from the geomean oracle for both price AND liquidity.
665
- if (address(key.hooks) != address(0)) {
666
- // Build the observation window: [_V4_TWAP_WINDOW seconds ago, now].
667
- uint32[] memory secondsAgos = new uint32[](2);
668
- secondsAgos[0] = _V4_TWAP_WINDOW;
669
- secondsAgos[1] = 0;
670
-
671
- // Read both the TWAP tick and the seconds-per-liquidity series so liquidity is also time-averaged.
672
- (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s) =
673
- IGeomeanOracle(address(key.hooks)).observe({key: key, secondsAgos: secondsAgos});
674
- if (tickCumulatives.length < 2 || secondsPerLiquidityCumulativeX128s.length < 2) {
675
- revert JBSwapPoolLib_InsufficientTwapHistory({
676
- pool: address(key.hooks), availableWindow: tickCumulatives.length, requiredWindow: 2
677
- });
678
- }
679
-
680
- // Compute the arithmetic mean tick from the cumulative tick difference, rounding negative values
681
- // toward negative infinity to match Uniswap's oracle pattern and the buyback hook.
682
- int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
683
- // forge-lint: disable-next-line(unsafe-typecast)
684
- tick = int24(tickCumulativesDelta / int56(int32(_V4_TWAP_WINDOW)));
685
- // forge-lint: disable-next-line(unsafe-typecast)
686
- if (tickCumulativesDelta < 0 && tickCumulativesDelta % int56(int32(_V4_TWAP_WINDOW)) != 0) {
687
- tick--;
688
- }
689
-
690
- // Derive harmonic-mean liquidity from the seconds-per-liquidity delta. This is the same shape the
691
- // buyback hook uses (`JBSwapLib.getQuoteFromOracle`) and resists JIT-LP liquidity removal in the
692
- // delivery block — the manipulation has to persist across the full TWAP window to move the average.
693
- uint160 secondsPerLiquidityDelta =
694
- secondsPerLiquidityCumulativeX128s[1] - secondsPerLiquidityCumulativeX128s[0];
695
-
696
- if (secondsPerLiquidityDelta > 0) {
697
- // Safe: `(uint256(_V4_TWAP_WINDOW) << 128) / secondsPerLiquidityDelta` fits in uint128 because
698
- // _V4_TWAP_WINDOW is at most a uint32 (~4.3B) and the divisor is > 0 in this branch.
699
- // forge-lint: disable-next-line(unsafe-typecast)
700
- liquidity = uint128((uint256(_V4_TWAP_WINDOW) << 128) / uint256(secondsPerLiquidityDelta));
701
- }
702
- // If `secondsPerLiquidityDelta == 0`, liquidity stays 0 and the no-liquidity revert below fires —
703
- // refuse to quote a hooked route whose averaged liquidity is degenerate.
704
- } else {
705
- // Hookless V4 spot pools are only selected when no TWAP-capable route exists.
706
- (, tick,,) = config.poolManager.getSlot0(id);
707
- liquidity = config.poolManager.getLiquidity(id);
708
- }
709
- }
710
-
711
- // Revert if the pool has no usable in-range liquidity (spot for hookless, TWAP-derived for hooked).
712
- if (liquidity == 0) revert JBSwapPoolLib_NoLiquidity({pool: address(0), poolId: id});
713
-
714
- // V4 uses address(0) for native ETH — compute quoting addresses inline to save stack slots.
715
- minAmountOut = _quoteWithSlippage({
716
- amount: amount,
717
- liquidity: liquidity,
718
- tokenIn: normalizedTokenIn == config.wrappedNativeToken ? address(0) : normalizedTokenIn,
719
- tokenOut: normalizedTokenOut == config.wrappedNativeToken ? address(0) : normalizedTokenOut,
720
- tick: tick,
721
- poolFeeBps: feeBps
722
- });
723
- }
724
-
725
- /// @notice Check whether an oracle observation is old enough to cover a TWAP window ending at the current block.
726
- /// @dev Current or future timestamps are rejected before subtracting so the age check cannot underflow.
727
- /// @param observationTimestamp The timestamp recorded in the pool's oracle observation.
728
- /// @param window The required observation age, in seconds.
729
- /// @return True if the observation is before the current block and at least `window` seconds old.
730
- function _observationIsOldEnough(uint32 observationTimestamp, uint256 window) internal view returns (bool) {
731
- // TWAP freshness is intentionally time-based.
732
- // forge-lint: disable-next-line(block-timestamp)
733
- if (observationTimestamp >= block.timestamp) return false;
734
- // forge-lint: disable-next-line(block-timestamp)
735
- return block.timestamp - observationTimestamp >= window;
736
- }
737
-
738
- /// @notice Probe a single V4 pool configuration for liquidity.
739
- /// @param poolManager The Uniswap V4 pool manager to query.
740
- /// @param sorted0 The lower-address token in the pair (sorted).
741
- /// @param sorted1 The higher-address token in the pair (sorted).
742
- /// @param hookAddr The hook address to use for this pool configuration.
743
- /// @param tierIndex The fee tier index (0-3) to probe.
744
- /// @return key The constructed pool key for this configuration.
745
- /// @return poolLiquidity The current in-range liquidity of the pool, or 0 if uninitialized.
746
- function _probeV4Pool(
747
- IPoolManager poolManager,
748
- address sorted0,
749
- address sorted1,
750
- address hookAddr,
751
- uint256 tierIndex
752
- )
753
- internal
754
- view
755
- returns (PoolKey memory key, uint128 poolLiquidity)
756
- {
757
- // Look up fee and tick spacing for this tier index.
758
- (uint24 fee, int24 tickSpacing) = _v4FeeAndTickSpacing(tierIndex);
759
-
760
- // Construct the pool key from the sorted tokens and tier parameters.
761
- key = PoolKey({
762
- currency0: Currency.wrap(sorted0),
763
- currency1: Currency.wrap(sorted1),
764
- fee: fee,
765
- tickSpacing: tickSpacing,
766
- hooks: IHooks(hookAddr)
767
- });
768
-
769
- // Derive the pool ID from the key.
770
- PoolId id = key.toId();
771
-
772
- // Check if pool is initialized (sqrtPriceX96 != 0).
773
- (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(id);
774
- if (sqrtPriceX96 == 0) return (key, 0);
775
-
776
- // Query the pool's current in-range liquidity.
777
- poolLiquidity = poolManager.getLiquidity(id);
778
- }
779
-
780
- /// @notice Compute the minimum acceptable output using sigmoid slippage at the given tick.
781
- /// @param amount The amount of input tokens.
782
- /// @param liquidity The pool's in-range liquidity.
783
- /// @param tokenIn The input token address (for quoting).
784
- /// @param tokenOut The output token address (for quoting).
785
- /// @param tick The arithmetic mean tick from the TWAP or current spot.
786
- /// @param poolFeeBps The pool's fee in basis points.
787
- /// @return minAmountOut The minimum acceptable output amount after slippage.
788
- function _quoteWithSlippage(
789
- uint256 amount,
790
- uint128 liquidity,
791
- address tokenIn,
792
- address tokenOut,
793
- int24 tick,
794
- uint256 poolFeeBps
795
- )
796
- internal
797
- pure
798
- returns (uint256 minAmountOut)
799
- {
800
- // Compute the dynamic slippage tolerance based on price impact.
801
- uint256 slippageTolerance = _getSlippageTolerance({
802
- amountIn: amount,
803
- liquidity: liquidity,
804
- tokenOut: tokenOut,
805
- tokenIn: tokenIn,
806
- arithmeticMeanTick: tick,
807
- poolFeeBps: poolFeeBps
808
- });
809
-
810
- // If the slippage tolerance is 100% or more, accept any output.
811
- if (slippageTolerance >= _SLIPPAGE_DENOMINATOR) return 0;
812
-
813
- // Revert if amount exceeds uint128 (required by OracleLibrary.getQuoteAtTick).
814
- if (amount > type(uint128).max) revert JBSwapPoolLib_AmountOverflow(amount);
815
-
816
- // Get the expected output at the TWAP tick.
817
- minAmountOut = OracleLibrary.getQuoteAtTick({
818
- tick: tick,
819
- // The overflow check above bounds `amount` for `getQuoteAtTick`.
820
- // forge-lint: disable-next-line(unsafe-typecast)
821
- baseAmount: uint128(amount),
822
- baseToken: tokenIn,
823
- quoteToken: tokenOut
824
- });
825
-
826
- // Reduce by the slippage tolerance to get the minimum acceptable output.
827
- minAmountOut -= (minAmountOut * slippageTolerance) / _SLIPPAGE_DENOMINATOR;
828
- }
829
-
830
- /// @notice Reads one V3 observation.
831
- /// @dev Uses `staticcall` so a bad candidate pool cannot interrupt pool discovery.
832
- /// @param pool The V3 pool candidate to inspect.
833
- /// @param index The observation ring index to read.
834
- /// @return ok True if the observation returned enough data to decode.
835
- /// @return observationTimestamp The timestamp stored at `index`.
836
- /// @return initialized True if the observation slot has been initialized.
837
- function _v3ObservationOf(
838
- IUniswapV3Pool pool,
839
- uint256 index
840
- )
841
- internal
842
- view
843
- returns (bool ok, uint32 observationTimestamp, bool initialized)
844
- {
845
- // Pool discovery intentionally probes candidate pools in a bounded fee-tier list; failed probes mean "skip".
846
- (bool success, bytes memory data) =
847
- address(pool).staticcall(abi.encodeWithSelector(IUniswapV3PoolState.observations.selector, index));
848
- if (!success || data.length < 128) return (false, 0, false);
849
-
850
- (observationTimestamp,,, initialized) = abi.decode(data, (uint32, int56, uint160, bool));
851
- ok = true;
852
- }
853
-
854
- /// @notice Reads the observation cursor and cardinality from a V3 pool's slot0.
855
- /// @dev Uses `staticcall` instead of the typed interface so malformed or hooklike candidate pools are rejected
856
- /// as unusable candidates without reverting the whole bounded pool-discovery scan.
857
- /// @param pool The V3 pool candidate to inspect.
858
- /// @return ok True if `slot0()` returned enough data to decode.
859
- /// @return observationIndex The pool's current observation cursor.
860
- /// @return observationCardinality The number of initialized/available observation slots.
861
- function _v3ObservationStateOf(IUniswapV3Pool pool)
862
- internal
863
- view
864
- returns (bool ok, uint16 observationIndex, uint16 observationCardinality)
865
- {
866
- // Pool discovery intentionally probes candidate pools in a bounded fee-tier list; failed probes mean "skip".
867
- (bool success, bytes memory data) =
868
- address(pool).staticcall(abi.encodeWithSelector(IUniswapV3PoolState.slot0.selector));
869
- if (!success || data.length < 224) return (false, 0, 0);
870
-
871
- (,, observationIndex, observationCardinality,,,) =
872
- abi.decode(data, (uint160, int24, uint16, uint16, uint16, uint8, bool));
873
- ok = true;
874
- }
875
-
876
- /// @notice Checks whether a V3 pool can serve the full default TWAP window.
877
- /// @dev Reads the observation ring directly so discovery can skip young pools without reverting. The oldest
878
- /// initialized observation must be at least `_DEFAULT_TWAP_WINDOW` seconds old.
879
- /// @param pool The V3 pool to inspect.
880
- /// @return True if the pool has enough initialized history for a default-window TWAP.
881
- function _v3PoolHasFullTwapHistory(IUniswapV3Pool pool) internal view returns (bool) {
882
- // slot0 gives the current observation cursor and total initialized/available observation slots.
883
- (bool slot0Ok, uint16 observationIndex, uint16 observationCardinality) = _v3ObservationStateOf(pool);
884
- if (!slot0Ok || observationCardinality == 0) return false;
885
-
886
- // In a full ring, the next slot after the cursor is the oldest observation.
887
- uint256 oldestIndex = (uint256(observationIndex) + 1) % uint256(observationCardinality);
888
- (bool observationOk, uint32 observationTimestamp, bool initialized) =
889
- _v3ObservationOf({pool: pool, index: oldestIndex});
890
- if (!observationOk) return false;
891
-
892
- // If the ring has not wrapped yet, slot 0 is the oldest initialized observation.
893
- if (!initialized) {
894
- (observationOk, observationTimestamp, initialized) = _v3ObservationOf({pool: pool, index: 0});
895
- if (!observationOk || !initialized) return false;
896
- }
897
-
898
- return _observationIsOldEnough({observationTimestamp: observationTimestamp, window: _DEFAULT_TWAP_WINDOW});
899
- }
900
-
901
- /// @notice Check whether a V4 hooked pool can return TWAP price and liquidity for the required window.
902
- /// @dev Hookless pools return false. Reverting hooks and hooks that return incomplete or degenerate oracle data
903
- /// are treated as unusable for TWAP routing.
904
- /// @param key The V4 pool key whose hook should be probed.
905
- /// @return True if the hook can serve both the historical and current cumulative tick observations.
906
- function _v4PoolHasTwap(PoolKey memory key) internal view returns (bool) {
907
- if (address(key.hooks) == address(0)) return false;
908
-
909
- uint32[] memory secondsAgos = new uint32[](2);
910
- secondsAgos[0] = _V4_TWAP_WINDOW;
911
- secondsAgos[1] = 0;
912
-
913
- // Pool discovery intentionally probes candidate hooks in a bounded pool list.
914
- try IGeomeanOracle(address(key.hooks)).observe({key: key, secondsAgos: secondsAgos}) returns (
915
- int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s
916
- ) {
917
- if (tickCumulatives.length < 2 || secondsPerLiquidityCumulativeX128s.length < 2) return false;
918
- return secondsPerLiquidityCumulativeX128s[1] > secondsPerLiquidityCumulativeX128s[0];
919
- } catch {
920
- return false;
921
- }
922
- }
923
-
924
- //*********************************************************************//
925
- // ----------------------- internal helpers -------------------------- //
926
- //*********************************************************************//
927
-
928
- /// @notice Execute a swap through a V3 pool.
929
- /// @param pool The V3 pool to execute the swap on.
930
- /// @param normalizedTokenIn The normalized input token address.
931
- /// @param normalizedTokenOut The normalized output token address.
932
- /// @param amount The amount of input tokens to swap.
933
- /// @param minAmountOut The minimum acceptable output amount.
934
- /// @param originalTokenIn The original (pre-normalization) input token address.
935
- /// @return amountOut The amount of output tokens received.
936
- function _executeV3Swap(
937
- IUniswapV3Pool pool,
938
- address normalizedTokenIn,
939
- address normalizedTokenOut,
940
- uint256 amount,
941
- uint256 minAmountOut,
942
- address originalTokenIn
943
- )
944
- internal
945
- returns (uint256 amountOut)
946
- {
947
- // Determine swap direction based on token ordering.
948
- bool zeroForOne = normalizedTokenIn < normalizedTokenOut;
949
-
950
- // Execute the V3 swap with a price limit derived from the expected amounts.
951
- (int256 amount0, int256 amount1) = pool.swap({
952
- recipient: address(this),
953
- zeroForOne: zeroForOne,
954
- // Exact-input swap amounts are provided as positive ints to Uniswap V3.
955
- // forge-lint: disable-next-line(unsafe-typecast)
956
- amountSpecified: int256(amount),
957
- sqrtPriceLimitX96: JBSwapLib.sqrtPriceLimitFromAmounts({
958
- amountIn: amount, minimumAmountOut: minAmountOut, zeroForOne: zeroForOne
959
- }),
960
- data: abi.encode(originalTokenIn, normalizedTokenIn, normalizedTokenOut)
961
- });
962
-
963
- // Reject partial fills: when the V3 pool's price limit is hit before the full input is
964
- // consumed, the pool returns only the consumed portion of `amount` and the caller is left
965
- // holding the unconsumed remainder. The sucker's accounting assumes the full bridge amount
966
- // was either swapped or made retryable, so a silent partial fill strands input tokens and
967
- // breaks cross-chain solvency. Revert so the caller can either retry with a smaller size
968
- // or wait for liquidity to return.
969
- // forge-lint: disable-next-line(unsafe-typecast)
970
- uint256 consumedAmount = uint256(zeroForOne ? amount0 : amount1);
971
- if (consumedAmount < amount) {
972
- revert JBSwapPoolLib_PartialFill({consumed: consumedAmount, requested: amount});
973
- }
974
-
975
- // Extract the output amount from the signed delta (negative = tokens received).
976
- amountOut = uint256(-(zeroForOne ? amount1 : amount0));
977
-
978
- // Enforce the minimum output from the TWAP quote.
979
- if (amountOut < minAmountOut) {
980
- revert JBSwapPoolLib_SlippageExceeded({amountOut: amountOut, minAmountOut: minAmountOut});
981
- }
982
- }
983
-
984
- /// @notice Execute a swap through a V4 pool via `PoolManager.unlock()`.
985
- /// @param config The swap configuration (pool manager, wrapped native token addresses).
986
- /// @param key The V4 pool key to swap through.
987
- /// @param normalizedTokenIn The normalized input token address.
988
- /// @param amount The amount of input tokens to swap.
989
- /// @param minAmountOut The minimum acceptable output amount.
990
- /// @return amountOut The amount of output tokens received.
991
- function _executeV4Swap(
992
- SwapConfig memory config,
993
- PoolKey memory key,
994
- address normalizedTokenIn,
995
- address originalTokenIn,
996
- uint256 amount,
997
- uint256 minAmountOut
998
- )
999
- internal
1000
- returns (uint256 amountOut)
1001
- {
1002
- // Convert wrapped native token to address(0) for V4's native token convention.
1003
- address v4In = normalizedTokenIn == config.wrappedNativeToken ? address(0) : normalizedTokenIn;
1004
-
1005
- // Determine swap direction based on currency ordering in the pool key.
1006
- bool zeroForOne = Currency.unwrap(key.currency0) == v4In;
1007
-
1008
- // Tell the unlock callback whether to consume any wrapped-native-token balance the caller may hold.
1009
- // The pool's input side is native iff the swap input came from the wrapped-native ERC-20 (not the
1010
- // NATIVE_TOKEN sentinel). If the caller's input was already native (NATIVE_TOKEN sentinel), the caller
1011
- // holds raw ETH for THIS swap; any wrapped balance it holds is for unrelated reasons (e.g., backing other
1012
- // claims) and must not be consumed here.
1013
- address callbackWrappedNativeToken;
1014
- if (v4In == address(0) && originalTokenIn != JBConstants.NATIVE_TOKEN) {
1015
- callbackWrappedNativeToken = config.wrappedNativeToken;
1016
- }
1017
-
1018
- // Build the encoded unlock data in a scoped block to avoid stack-too-deep.
1019
- bytes memory unlockData;
1020
- {
1021
- // Compute the sqrt price limit from the expected amounts.
1022
- uint160 sqrtPriceLimitX96 = JBSwapLib.sqrtPriceLimitFromAmounts({
1023
- amountIn: amount, minimumAmountOut: minAmountOut, zeroForOne: zeroForOne
1024
- });
1025
-
1026
- // V4 uses negative amounts for exact-input swaps.
1027
- // Exact-input swap amounts are negated for Uniswap V4.
1028
- // forge-lint: disable-next-line(unsafe-typecast)
1029
- int256 exactInputAmount = -int256(amount);
1030
-
1031
- unlockData = abi.encode(
1032
- key, zeroForOne, exactInputAmount, sqrtPriceLimitX96, minAmountOut, callbackWrappedNativeToken
1033
- );
1034
- }
1035
-
1036
- // Unlock the PoolManager and encode the swap parameters for the callback.
1037
- bytes memory result = config.poolManager.unlock(unlockData);
1038
-
1039
- // Decode the output amount returned by the unlock callback.
1040
- amountOut = abi.decode(result, (uint256));
1041
- }
1042
-
1043
- /// @notice Get the Uniswap V3 fee tier for a given index.
1044
- /// @param index The fee tier index (0 = 0.3%, 1 = 0.05%, 2 = 1%, 3 = 0.01%).
1045
- /// @return fee The fee tier in hundredths of a basis point.
1046
- function _feeTier(uint256 index) internal pure returns (uint24 fee) {
1047
- if (index == 0) return 3000;
1048
- if (index == 1) return 500;
1049
- if (index == 2) return 10_000;
1050
- return 100;
1051
- }
1052
-
1053
- /// @notice Normalize a token address, converting the NATIVE_TOKEN sentinel to the wrapped native token.
1054
- /// @param token The token address to normalize.
1055
- /// @param wrappedNativeToken The wrapped native token address on this chain.
1056
- /// @return The normalized token address.
1057
- function _normalize(address token, address wrappedNativeToken) internal pure returns (address) {
1058
- return token == JBConstants.NATIVE_TOKEN ? wrappedNativeToken : token;
1059
- }
1060
-
1061
- /// @notice Quote via V3 TWAP and execute swap. Separate function for stack isolation.
1062
- /// @param pool The V3 pool to swap through.
1063
- /// @param normalizedTokenIn The normalized input token address.
1064
- /// @param normalizedTokenOut The normalized output token address.
1065
- /// @param amount The amount of input tokens to swap.
1066
- /// @param originalTokenIn The original (pre-normalization) input token address.
1067
- /// @return amountOut The amount of output tokens received.
1068
- function _quoteAndSwapV3(
1069
- IUniswapV3Pool pool,
1070
- address normalizedTokenIn,
1071
- address normalizedTokenOut,
1072
- uint256 amount,
1073
- address originalTokenIn
1074
- )
1075
- internal
1076
- returns (uint256 amountOut)
1077
- {
1078
- // Get the TWAP-based minimum output for slippage protection.
1079
- uint256 minOut = _getV3TwapQuote({
1080
- pool: pool, normalizedTokenIn: normalizedTokenIn, normalizedTokenOut: normalizedTokenOut, amount: amount
1081
- });
1082
-
1083
- // Execute the swap through the V3 pool.
1084
- amountOut = _executeV3Swap({
1085
- pool: pool,
1086
- normalizedTokenIn: normalizedTokenIn,
1087
- normalizedTokenOut: normalizedTokenOut,
1088
- amount: amount,
1089
- minAmountOut: minOut,
1090
- originalTokenIn: originalTokenIn
1091
- });
1092
- }
1093
-
1094
- /// @notice Quote via V4 TWAP/spot and execute swap. Separate function for stack isolation.
1095
- /// @param config The swap configuration (pool manager, wrapped native token addresses).
1096
- /// @param key The V4 pool key to swap through.
1097
- /// @param normalizedTokenIn The normalized input token address.
1098
- /// @param normalizedTokenOut The normalized output token address.
1099
- /// @param amount The amount of input tokens to swap.
1100
- /// @return amountOut The amount of output tokens received.
1101
- function _quoteAndSwapV4(
1102
- SwapConfig memory config,
1103
- PoolKey memory key,
1104
- address normalizedTokenIn,
1105
- address normalizedTokenOut,
1106
- address originalTokenIn,
1107
- uint256 amount
1108
- )
1109
- internal
1110
- returns (uint256 amountOut)
1111
- {
1112
- // Get the TWAP-based minimum output for slippage protection.
1113
- uint256 minOut = _getV4Quote({
1114
- config: config,
1115
- key: key,
1116
- normalizedTokenIn: normalizedTokenIn,
1117
- normalizedTokenOut: normalizedTokenOut,
1118
- amount: amount
1119
- });
1120
-
1121
- // Execute the swap through the V4 PoolManager.
1122
- amountOut = _executeV4Swap({
1123
- config: config,
1124
- key: key,
1125
- normalizedTokenIn: normalizedTokenIn,
1126
- originalTokenIn: originalTokenIn,
1127
- amount: amount,
1128
- minAmountOut: minOut
1129
- });
1130
- }
1131
-
1132
- /// @notice Get the Uniswap V4 fee and tick spacing for a given tier index.
1133
- /// @param index The fee tier index (0 = 0.3%/60, 1 = 0.05%/10, 2 = 1%/200, 3 = 0.01%/1).
1134
- /// @return fee The fee in hundredths of a basis point.
1135
- /// @return tickSpacing The tick spacing for this fee tier.
1136
- function _v4FeeAndTickSpacing(uint256 index) internal pure returns (uint24 fee, int24 tickSpacing) {
1137
- if (index == 0) return (3000, 60);
1138
- if (index == 1) return (500, 10);
1139
- if (index == 2) return (10_000, 200);
1140
- return (100, 1);
1141
- }
1142
- }