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