@zoralabs/limit-orders 0.2.0 → 0.2.1

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.
Files changed (84) hide show
  1. package/.turbo/turbo-build$colon$js.log +47 -45
  2. package/CHANGELOG.md +61 -0
  3. package/abis/IWETH.json +118 -0
  4. package/abis/IZoraLimitOrderBook.json +5 -0
  5. package/abis/LimitOrderLiquidity.json +7 -0
  6. package/abis/LimitOrderViews.json +62 -0
  7. package/abis/SwapWithLimitOrders.json +18 -11
  8. package/abis/ZoraLimitOrderBook.json +28 -0
  9. package/cache/solidity-files-cache.json +1 -1
  10. package/dist/index.cjs +29 -8
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +29 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/wagmiGenerated.d.ts +37 -9
  15. package/dist/wagmiGenerated.d.ts.map +1 -1
  16. package/out/BytesLib.sol/BytesLib.json +1 -1
  17. package/out/CoinCommon.sol/CoinCommon.json +1 -1
  18. package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
  19. package/out/CoinConstants.sol/CoinConstants.json +1 -1
  20. package/out/DopplerMath.sol/DopplerMath.json +1 -1
  21. package/out/FixedPoint96.sol/FixedPoint96.json +1 -1
  22. package/out/ISwapRouter.sol/ISwapRouter.json +1 -1
  23. package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -1
  24. package/out/IWETH.sol/IWETH.json +1 -0
  25. package/out/IZoraHookRegistry.sol/IZoraHookRegistry.json +1 -1
  26. package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -1
  27. package/out/IZoraLimitOrderBookCoinsInterface.sol/IZoraLimitOrderBookCoinsInterface.json +1 -1
  28. package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.json +1 -1
  29. package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -1
  30. package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -1
  31. package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -1
  32. package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -1
  33. package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -1
  34. package/out/LimitOrderQueues.sol/LimitOrderQueues.json +1 -1
  35. package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -1
  36. package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -1
  37. package/out/LimitOrderViews.sol/LimitOrderViews.json +1 -0
  38. package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
  39. package/out/LiquidityAmounts.sol/LiquidityAmounts.json +1 -1
  40. package/out/Path.sol/Path.json +1 -1
  41. package/out/Permit2Payments.sol/Permit2Payments.json +1 -1
  42. package/out/SimpleAccessManaged.sol/SimpleAccessManaged.json +1 -1
  43. package/out/SimpleAccessManager.sol/SimpleAccessManager.json +1 -1
  44. package/out/SqrtPriceMath.sol/SqrtPriceMath.json +1 -1
  45. package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
  46. package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
  47. package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -1
  48. package/out/UnsafeMath.sol/UnsafeMath.json +1 -1
  49. package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -1
  50. package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
  51. package/out/build-info/{69718f10d1dc37f0.json → 876cc09bc44cc8a7.json} +1 -1
  52. package/out/uniswap/BitMath.sol/BitMath.json +1 -1
  53. package/out/uniswap/CustomRevert.sol/CustomRevert.json +1 -1
  54. package/out/uniswap/FullMath.sol/FullMath.json +1 -1
  55. package/out/uniswap/SafeCast.sol/SafeCast.json +1 -1
  56. package/out/uniswap/TickMath.sol/TickMath.json +1 -1
  57. package/package/wagmiGenerated.ts +28 -7
  58. package/package.json +1 -1
  59. package/src/IZoraLimitOrderBook.sol +2 -0
  60. package/src/ZoraLimitOrderBook.sol +22 -8
  61. package/src/libs/LimitOrderBitmap.sol +0 -51
  62. package/src/libs/LimitOrderCommon.sol +48 -30
  63. package/src/libs/LimitOrderCreate.sol +5 -18
  64. package/src/libs/LimitOrderFill.sol +32 -161
  65. package/src/libs/LimitOrderLiquidity.sol +92 -71
  66. package/src/libs/LimitOrderViews.sol +168 -0
  67. package/src/libs/LimitOrderWithdraw.sol +13 -4
  68. package/src/libs/SwapLimitOrders.sol +14 -7
  69. package/src/router/SwapWithLimitOrders.sol +40 -26
  70. package/test/LimitOrderBitmap.t.sol +13 -7
  71. package/test/LimitOrderFill.t.sol +43 -0
  72. package/test/LimitOrderLibraries.t.sol +18 -10
  73. package/test/LimitOrderLiquidityPayouts.t.sol +280 -3
  74. package/test/LimitOrderWithdraw.t.sol +28 -1
  75. package/test/SwapWithLimitOrders.t.sol +3 -3
  76. package/test/SwapWithLimitOrdersRouter.t.sol +108 -11
  77. package/test/unit/LimitOrderBitmapUnit.t.sol +0 -134
  78. package/test/unit/LimitOrderCreateUnit.t.sol +32 -0
  79. package/test/unit/SwapLimitOrdersUnit.t.sol +231 -33
  80. package/test/unit/SwapLimitOrdersValidation.t.sol +20 -34
  81. package/test/unit/SwapWithLimitOrdersUnit.t.sol +21 -88
  82. package/test/utils/BaseTest.sol +29 -8
  83. package/test/utils/MockWETH.sol +39 -0
  84. package/test/utils/TestableZoraLimitOrderBook.sol +5 -7
@@ -11,7 +11,6 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
11
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
12
12
  import {TickBitmap} from "@uniswap/v4-core/src/libraries/TickBitmap.sol";
13
13
  import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
14
- import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
15
14
  import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
16
15
  import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
17
16
 
@@ -20,36 +19,17 @@ import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
20
19
  import {LimitOrderTypes} from "./LimitOrderTypes.sol";
21
20
  import {LimitOrderLiquidity} from "./LimitOrderLiquidity.sol";
22
21
  import {LimitOrderCommon} from "./LimitOrderCommon.sol";
22
+ import {LimitOrderViews} from "./LimitOrderViews.sol";
23
23
  import {CoinCommon} from "@zoralabs/coins/src/libs/CoinCommon.sol";
24
24
  import {IDeployedCoinVersionLookup} from "@zoralabs/coins/src/interfaces/IDeployedCoinVersionLookup.sol";
25
25
 
26
26
  library LimitOrderFill {
27
27
  using PoolIdLibrary for PoolKey;
28
28
 
29
- int24 internal constant TICK_SENTINEL = type(int24).max;
30
-
31
29
  struct Context {
32
30
  IPoolManager poolManager;
33
31
  IDeployedCoinVersionLookup versionLookup;
34
- }
35
-
36
- function validateTickRange(
37
- LimitOrderStorage.Layout storage state,
38
- Context memory ctx,
39
- PoolKey calldata providedKey,
40
- bool isCurrency0,
41
- int24 startTick,
42
- int24 endTick
43
- ) internal view returns (PoolKey memory canonicalKey, int24 resolvedStart, int24 resolvedEnd) {
44
- bytes32 poolKeyHash = CoinCommon.hashPoolKey(providedKey);
45
- canonicalKey = state.poolKeys[poolKeyHash];
46
- if (canonicalKey.tickSpacing == 0) {
47
- canonicalKey = providedKey;
48
- if (canonicalKey.tickSpacing == 0) revert IZoraLimitOrderBook.InvalidPoolKey();
49
- }
50
-
51
- (resolvedStart, resolvedEnd) = _resolveTickRange(ctx.poolManager, canonicalKey, isCurrency0, startTick, endTick);
52
- _validateTickRange(isCurrency0, resolvedStart, resolvedEnd);
32
+ address weth;
53
33
  }
54
34
 
55
35
  function handleFillCallback(LimitOrderStorage.Layout storage state, Context memory ctx, bytes memory callbackData) internal {
@@ -68,10 +48,8 @@ library LimitOrderFill {
68
48
  if (key.tickSpacing == 0) revert IZoraLimitOrderBook.InvalidPoolKey();
69
49
  }
70
50
 
71
- int24 currentTick = _currentPoolTick(ctx.poolManager, key);
72
-
73
51
  if (data.orderIds.length == 0) {
74
- _fillAcrossRange(state, ctx, data, currentTick);
52
+ _fillAcrossRange(state, ctx, data);
75
53
  return;
76
54
  }
77
55
 
@@ -89,9 +67,12 @@ library LimitOrderFill {
89
67
  order.status == LimitOrderTypes.OrderStatus.OPEN &&
90
68
  order.poolKeyHash == poolKeyHash &&
91
69
  order.isCurrency0 == isCurrency0 &&
92
- order.createdEpoch < currentEpoch &&
93
- _hasCrossed(order, currentTick)
70
+ order.createdEpoch < currentEpoch
94
71
  ) {
72
+ if (!_hasCrossed(order, _currentPoolTick(ctx.poolManager, key))) {
73
+ continue;
74
+ }
75
+
95
76
  int24 orderTick = LimitOrderCommon.getOrderTick(order);
96
77
  LimitOrderTypes.Queue storage tickQueue = tickQueues[orderTick];
97
78
 
@@ -104,12 +85,7 @@ library LimitOrderFill {
104
85
  }
105
86
  }
106
87
 
107
- function _fillAcrossRange(
108
- LimitOrderStorage.Layout storage state,
109
- Context memory ctx,
110
- IZoraLimitOrderBook.FillCallbackData memory data,
111
- int24 currentTick
112
- ) private {
88
+ function _fillAcrossRange(LimitOrderStorage.Layout storage state, Context memory ctx, IZoraLimitOrderBook.FillCallbackData memory data) private {
113
89
  if (data.maxFillCount == 0) {
114
90
  return;
115
91
  }
@@ -122,6 +98,7 @@ library LimitOrderFill {
122
98
  mapping(int24 => LimitOrderTypes.Queue) storage tickQueues = state.tickQueues[poolKeyHash][coin];
123
99
  mapping(bytes32 => LimitOrderTypes.LimitOrder) storage orders = state.limitOrders;
124
100
  uint256 currentEpoch = state.poolEpochs[poolKeyHash];
101
+
125
102
  bytes32 ordersSlot;
126
103
  assembly ("memory-safe") {
127
104
  ordersSlot := orders.slot
@@ -133,13 +110,16 @@ library LimitOrderFill {
133
110
  int24 cursor = data.startTick;
134
111
  int24 target = data.endTick;
135
112
  int24 tickSpacing = data.poolKey.tickSpacing;
136
-
137
113
  while (processed < fillCap) {
138
- if (zeroDirection ? cursor <= target : cursor >= target) break;
114
+ if (zeroDirection ? cursor <= target : cursor >= target) {
115
+ break;
116
+ }
139
117
 
140
118
  (int24 nextTick, bool initialized) = TickBitmap.nextInitializedTickWithinOneWord(bitmap, cursor, tickSpacing, zeroDirection);
141
119
  bool crossesTarget = zeroDirection ? nextTick <= target : nextTick > target;
142
- if (crossesTarget) break;
120
+ if (crossesTarget) {
121
+ break;
122
+ }
143
123
 
144
124
  if (!initialized) {
145
125
  cursor = zeroDirection ? nextTick - 1 : nextTick;
@@ -177,10 +157,11 @@ library LimitOrderFill {
177
157
  orderId = nextOrderId;
178
158
  continue;
179
159
  }
180
- if (order.createdEpoch == currentEpoch) break;
181
- if (!_hasCrossed(order, currentTick)) {
182
- orderId = nextOrderId;
183
- continue;
160
+ if (order.createdEpoch == currentEpoch) {
161
+ break;
162
+ }
163
+ if (!_hasCrossed(order, _currentPoolTick(ctx.poolManager, data.poolKey))) {
164
+ return;
184
165
  }
185
166
 
186
167
  _fillOrder(ctx, state, data.poolKey, tickQueue, order, orderId, data.fillReferral);
@@ -207,23 +188,29 @@ library LimitOrderFill {
207
188
  ) private {
208
189
  order.status = LimitOrderTypes.OrderStatus.FILLED;
209
190
 
210
- address coin = LimitOrderCommon.getOrderCoin(key, order.isCurrency0);
191
+ // Get both input and output currencies
192
+ address coinIn = LimitOrderCommon.getOrderCoin(key, order.isCurrency0);
193
+ address coinOut = LimitOrderCommon.getOrderCoin(key, !order.isCurrency0);
211
194
 
195
+ // Pass output currency to burnAndPayout (not input currency)
196
+ // This ensures payout uses the OUTPUT coin's configured payout path
212
197
  (Currency coinOutCurrency, uint128 makerAmount, uint128 referralAmount) = LimitOrderLiquidity.burnAndPayout(
213
198
  ctx.poolManager,
214
199
  key,
215
200
  order,
216
201
  orderId,
217
202
  fillReferral,
218
- coin,
219
- ctx.versionLookup
203
+ coinOut,
204
+ ctx.versionLookup,
205
+ ctx.weth
220
206
  );
221
207
 
222
- int24 orderTick = LimitOrderCommon.removeOrder(state, key, coin, tickQueue, order);
208
+ // Use input currency for removing from order book
209
+ int24 orderTick = LimitOrderCommon.removeOrder(state, key, coinIn, tickQueue, order);
223
210
 
224
211
  emit IZoraLimitOrderBook.LimitOrderFilled(
225
212
  order.maker,
226
- coin,
213
+ coinIn,
227
214
  Currency.unwrap(coinOutCurrency),
228
215
  order.orderSize,
229
216
  makerAmount,
@@ -235,122 +222,6 @@ library LimitOrderFill {
235
222
  );
236
223
  }
237
224
 
238
- /**
239
- * @notice Derives concrete tick bounds from user input and current pool state.
240
- * @dev Callers may pass sentinel values (`-TICK_SENTINEL` / `TICK_SENTINEL`) to mean
241
- * “start at the current tick” or “extend one spacing away”. This helper translates
242
- * those sentinels into real ticks by snapping to the pool’s aligned tick grid and
243
- * offsetting one spacing in the appropriate direction so fills never include
244
- * orders created in the same transaction.
245
- *
246
- * @param poolManager Pool manager used to read the live tick.
247
- * @param key Pool whose tick spacing/current tick drive alignment.
248
- * @param isCurrency0 True when targeting currency0 orders (prices above anchor).
249
- * @param startTick User-provided start tick or sentinel.
250
- * @param endTick User-provided end tick or sentinel.
251
- * @return resolvedStart Concrete start tick after resolving sentinels.
252
- * @return resolvedEnd Concrete end tick after resolving sentinels.
253
- */
254
- function _resolveTickRange(
255
- IPoolManager poolManager,
256
- PoolKey memory key,
257
- bool isCurrency0,
258
- int24 startTick,
259
- int24 endTick
260
- ) private view returns (int24 resolvedStart, int24 resolvedEnd) {
261
- int24 spacing = key.tickSpacing;
262
-
263
- bool startSentinel = startTick == -TICK_SENTINEL;
264
- bool endSentinel = endTick == TICK_SENTINEL;
265
-
266
- (int24 anchorTick, int24 nextAligned, int24 prevAligned) = _alignedTicks(poolManager, key, spacing);
267
-
268
- if (startSentinel) {
269
- // Treat sentinel start as anchoring the window at the aligned tick.
270
- resolvedStart = anchorTick;
271
- resolvedEnd = endSentinel ? (isCurrency0 ? nextAligned : prevAligned) : endTick;
272
- return (resolvedStart, resolvedEnd);
273
- }
274
-
275
- if (endSentinel) {
276
- resolvedStart = startTick;
277
- resolvedEnd = isCurrency0 ? nextAligned : anchorTick;
278
- return (resolvedStart, resolvedEnd);
279
- }
280
-
281
- resolvedStart = startTick;
282
- resolvedEnd = endTick;
283
- return (resolvedStart, resolvedEnd);
284
- }
285
-
286
- /**
287
- * @notice Snaps the current pool tick to the nearest aligned tick and returns its neighbors.
288
- * @dev Uniswap v4 ticks must lie on multiples of `tickSpacing`. We round the live
289
- * tick down to the nearest aligned value (handling negatives), clamp it inside
290
- * the pool’s usable range, then compute the next/previous aligned ticks for
291
- * callers that need to build deterministic tick windows.
292
- *
293
- * @param poolManager Pool manager used to read slot0.
294
- * @param key Pool key describing the pair and spacing.
295
- * @param spacing Tick spacing for this pool.
296
- * @return anchorTick Current tick rounded down to the aligned grid.
297
- * @return nextAligned Next aligned tick above the anchor (clamped to max usable).
298
- * @return prevAligned Previous aligned tick below the anchor (clamped to min usable).
299
- */
300
- function _alignedTicks(
301
- IPoolManager poolManager,
302
- PoolKey memory key,
303
- int24 spacing
304
- ) private view returns (int24 anchorTick, int24 nextAligned, int24 prevAligned) {
305
- int24 currentTick = _currentPoolTick(poolManager, key);
306
- int256 remainder;
307
- assembly ("memory-safe") {
308
- remainder := smod(currentTick, spacing)
309
- }
310
-
311
- if (remainder == 0) {
312
- anchorTick = currentTick;
313
- } else if (currentTick >= 0) {
314
- anchorTick = int24(int256(currentTick) - remainder);
315
- } else {
316
- anchorTick = int24(int256(currentTick) - remainder - spacing);
317
- }
318
-
319
- int24 minUsable = TickMath.minUsableTick(spacing);
320
- int24 maxUsable = TickMath.maxUsableTick(spacing);
321
-
322
- if (anchorTick < minUsable) {
323
- anchorTick = minUsable;
324
- } else if (anchorTick > maxUsable) {
325
- anchorTick = maxUsable;
326
- }
327
-
328
- nextAligned = anchorTick + spacing;
329
- if (nextAligned > maxUsable) {
330
- nextAligned = maxUsable;
331
- }
332
-
333
- prevAligned = anchorTick - spacing;
334
- if (prevAligned < minUsable) {
335
- prevAligned = minUsable;
336
- }
337
- }
338
-
339
- function _validateTickRange(bool isCurrency0, int24 startTick, int24 endTick) private pure {
340
- if (startTick < TickMath.MIN_TICK || startTick > TickMath.MAX_TICK) {
341
- revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
342
- }
343
- if (endTick < TickMath.MIN_TICK || endTick > TickMath.MAX_TICK) {
344
- revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
345
- }
346
-
347
- if (isCurrency0) {
348
- if (startTick > endTick) revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
349
- } else {
350
- if (startTick < endTick) revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
351
- }
352
- }
353
-
354
225
  function _currentPoolTick(IPoolManager poolManager, PoolKey memory key) private view returns (int24 tick) {
355
226
  (, tick, , ) = StateLibrary.getSlot0(poolManager, key.toId());
356
227
  }
@@ -13,26 +13,19 @@ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol
13
13
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
14
14
  import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
15
15
  import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
16
- import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
17
16
  import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
18
- import {LiquidityAmounts} from "@zoralabs/coins/src/utils/uniswap/LiquidityAmounts.sol";
19
17
 
20
18
  import {IDeployedCoinVersionLookup} from "@zoralabs/coins/src/interfaces/IDeployedCoinVersionLookup.sol";
21
19
  import {LimitOrderTypes} from "./LimitOrderTypes.sol";
22
20
  import {IHasSwapPath} from "@zoralabs/coins/src/interfaces/ICoin.sol";
23
21
  import {UniV4SwapToCurrency} from "@zoralabs/coins/src/libs/UniV4SwapToCurrency.sol";
22
+ import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
23
+ import {IWETH} from "@zoralabs/coins/src/interfaces/IWETH.sol";
24
24
 
25
25
  library LimitOrderLiquidity {
26
26
  using CurrencyLibrary for Currency;
27
27
 
28
- function liquidityForOrder(bool isCurrency0, uint256 size, int24 tickLower, int24 tickUpper) internal pure returns (uint128) {
29
- uint160 sqrtPriceLower = TickMath.getSqrtPriceAtTick(tickLower);
30
- uint160 sqrtPriceUpper = TickMath.getSqrtPriceAtTick(tickUpper);
31
- return
32
- isCurrency0
33
- ? LiquidityAmounts.getLiquidityForAmount0(sqrtPriceLower, sqrtPriceUpper, size)
34
- : LiquidityAmounts.getLiquidityForAmount1(sqrtPriceLower, sqrtPriceUpper, size);
35
- }
28
+ error WethTransferFailed();
36
29
 
37
30
  function refundResidual(PoolKey memory key, bool isCurrency0, address maker, uint128 amount) internal {
38
31
  if (amount == 0) {
@@ -59,16 +52,18 @@ library LimitOrderLiquidity {
59
52
  bytes32 orderId,
60
53
  address feeRecipient,
61
54
  address coinIn,
62
- IDeployedCoinVersionLookup coinLookup
55
+ IDeployedCoinVersionLookup coinLookup,
56
+ address weth
63
57
  ) internal returns (Currency makerCoinOut, uint128 makerAmountOut, uint128 referralAmountOut) {
64
- (BalanceDelta liqDelta, BalanceDelta feesDelta) = poolManager.modifyLiquidity(
58
+ // Note: callerDelta is a sum of both fee and liquidity deltas
59
+ (BalanceDelta callerDelta, BalanceDelta feesDelta) = poolManager.modifyLiquidity(
65
60
  key,
66
61
  ModifyLiquidityParams({tickLower: order.tickLower, tickUpper: order.tickUpper, liquidityDelta: -int256(uint256(order.liquidity)), salt: orderId}),
67
62
  ""
68
63
  );
69
64
 
70
- int128 liquidity0 = liqDelta.amount0();
71
- int128 liquidity1 = liqDelta.amount1();
65
+ int128 liquidity0 = callerDelta.amount0();
66
+ int128 liquidity1 = callerDelta.amount1();
72
67
  int128 fee0Initial = feesDelta.amount0();
73
68
  int128 fee1Initial = feesDelta.amount1();
74
69
 
@@ -77,15 +72,14 @@ library LimitOrderLiquidity {
77
72
  int128 referralShareLiquidity0 = feeRecipient == address(0) ? int128(0) : int128(fee0Initial);
78
73
  int128 referralShareLiquidity1 = feeRecipient == address(0) ? int128(0) : int128(fee1Initial);
79
74
 
80
- (bool usePath, IHasSwapPath.PayoutSwapPath memory payoutPath) = _resolvePayoutPath(coinIn, coinLookup);
75
+ Currency payoutCurrency = order.isCurrency0 ? key.currency1 : key.currency0;
76
+ IHasSwapPath.PayoutSwapPath memory payoutPath = _resolvePayoutPath(coinIn, coinLookup, key, payoutCurrency);
81
77
 
82
- (makerCoinOut, makerAmountOut) = _payoutRecipient(poolManager, key, order.maker, makerShareLiquidity0, makerShareLiquidity1, usePath, payoutPath);
78
+ (makerCoinOut, makerAmountOut) = _payoutRecipient(poolManager, order.maker, makerShareLiquidity0, makerShareLiquidity1, payoutPath, weth);
83
79
 
84
80
  if (referralShareLiquidity0 > 0 || referralShareLiquidity1 > 0) {
85
- (, referralAmountOut) = _payoutRecipient(poolManager, key, feeRecipient, referralShareLiquidity0, referralShareLiquidity1, usePath, payoutPath);
81
+ (, referralAmountOut) = _payoutRecipient(poolManager, feeRecipient, referralShareLiquidity0, referralShareLiquidity1, payoutPath, weth);
86
82
  }
87
-
88
- _settleNegativeDeltas(poolManager, key, liquidity0 + fee0Initial, liquidity1 + fee1Initial);
89
83
  }
90
84
 
91
85
  function burnAndRefund(
@@ -96,19 +90,25 @@ library LimitOrderLiquidity {
96
90
  uint128 liquidity,
97
91
  bytes32 salt,
98
92
  address recipient,
99
- bool isCurrency0
93
+ bool isCurrency0,
94
+ address weth
100
95
  ) internal returns (uint128 amountOut) {
101
96
  (int128 amount0, int128 amount1) = _burnLiquidity(poolManager, key, tickLower, tickUpper, liquidity, salt);
102
97
 
103
- if (isCurrency0 && amount0 > 0) {
104
- amountOut = uint128(amount0);
105
- poolManager.take(key.currency0, recipient, amountOut);
106
- } else if (!isCurrency0 && amount1 > 0) {
107
- amountOut = uint128(amount1);
108
- poolManager.take(key.currency1, recipient, amountOut);
98
+ if (amount0 > 0) {
99
+ uint128 amount0Out = uint128(amount0);
100
+ _takeCurrency(poolManager, key.currency0, recipient, amount0Out, weth);
101
+ if (isCurrency0) {
102
+ amountOut = amount0Out;
103
+ }
104
+ }
105
+ if (amount1 > 0) {
106
+ uint128 amount1Out = uint128(amount1);
107
+ _takeCurrency(poolManager, key.currency1, recipient, amount1Out, weth);
108
+ if (!isCurrency0) {
109
+ amountOut = amount1Out;
110
+ }
109
111
  }
110
-
111
- _settleNegativeDeltas(poolManager, key, amount0, amount1);
112
112
  }
113
113
 
114
114
  function settleDeltas(IPoolManager poolManager, PoolKey memory key, int256 d0, int256 d1, address payout0, address payout1) internal {
@@ -116,18 +116,28 @@ library LimitOrderLiquidity {
116
116
  poolManager.take(key.currency0, payout0, uint256(d0));
117
117
  }
118
118
  if (d0 < 0) {
119
+ uint256 amount = uint256(uint256(-d0));
119
120
  poolManager.sync(key.currency0);
120
- key.currency0.transfer(address(poolManager), uint256(uint256(-d0)));
121
- poolManager.settle();
121
+ if (key.currency0.isAddressZero()) {
122
+ poolManager.settle{value: amount}();
123
+ } else {
124
+ key.currency0.transfer(address(poolManager), amount);
125
+ poolManager.settle();
126
+ }
122
127
  }
123
128
 
124
129
  if (d1 > 0 && payout1 != address(0)) {
125
130
  poolManager.take(key.currency1, payout1, uint256(d1));
126
131
  }
127
132
  if (d1 < 0) {
133
+ uint256 amount = uint256(uint256(-d1));
128
134
  poolManager.sync(key.currency1);
129
- key.currency1.transfer(address(poolManager), uint256(uint256(-d1)));
130
- poolManager.settle();
135
+ if (key.currency1.isAddressZero()) {
136
+ poolManager.settle{value: amount}();
137
+ } else {
138
+ key.currency1.transfer(address(poolManager), amount);
139
+ poolManager.settle();
140
+ }
131
141
  }
132
142
  }
133
143
 
@@ -150,22 +160,28 @@ library LimitOrderLiquidity {
150
160
 
151
161
  function _resolvePayoutPath(
152
162
  address coinIn,
153
- IDeployedCoinVersionLookup coinLookup
154
- ) private view returns (bool hasPath, IHasSwapPath.PayoutSwapPath memory payoutPath) {
155
- if (coinIn == address(0)) {
156
- return (false, payoutPath);
157
- }
158
-
159
- try coinLookup.getVersionForDeployedCoin(coinIn) returns (uint8 version) {
160
- if (version >= 4 && _supportsSwapPath(coinIn)) {
161
- payoutPath = IHasSwapPath(coinIn).getPayoutSwapPath(coinLookup);
162
- if (payoutPath.path.length > 0) {
163
- return (true, payoutPath);
163
+ IDeployedCoinVersionLookup coinLookup,
164
+ PoolKey memory key,
165
+ Currency payoutCurrency
166
+ ) private view returns (IHasSwapPath.PayoutSwapPath memory payoutPath) {
167
+ // Try to get multi-hop path from coin
168
+ if (coinIn != address(0)) {
169
+ try coinLookup.getVersionForDeployedCoin(coinIn) returns (uint8 version) {
170
+ if (version >= 4 && _supportsSwapPath(coinIn)) {
171
+ payoutPath = IHasSwapPath(coinIn).getPayoutSwapPath(coinLookup);
172
+ // Validate first hop matches expected payout currency
173
+ if (payoutPath.path.length > 0 && payoutPath.path[0].intermediateCurrency == payoutCurrency) {
174
+ return payoutPath;
175
+ }
164
176
  }
165
- }
166
- } catch {}
177
+ } catch {}
178
+ }
167
179
 
168
- return (false, payoutPath);
180
+ // Fallback: construct simple single-hop path
181
+ Currency coinCurrency = payoutCurrency == key.currency0 ? key.currency1 : key.currency0;
182
+ payoutPath.currencyIn = coinCurrency;
183
+ payoutPath.path = new PathKey[](1);
184
+ payoutPath.path[0] = PathKey({intermediateCurrency: payoutCurrency, fee: key.fee, tickSpacing: key.tickSpacing, hooks: key.hooks, hookData: bytes("")});
169
185
  }
170
186
 
171
187
  function _supportsSwapPath(address coin) private view returns (bool) {
@@ -185,38 +201,43 @@ library LimitOrderLiquidity {
185
201
  }
186
202
  }
187
203
 
188
- function _payoutRecipient(
189
- IPoolManager poolManager,
190
- PoolKey memory key,
191
- address recipient,
192
- int128 amount0,
193
- int128 amount1,
194
- bool usePath,
195
- IHasSwapPath.PayoutSwapPath memory payoutPath
196
- ) private returns (Currency coinOut, uint128 amountOut) {
197
- if (usePath) {
198
- (coinOut, amountOut) = UniV4SwapToCurrency.swapToPath(poolManager, uint128(amount0), uint128(amount1), payoutPath.currencyIn, payoutPath.path);
199
- poolManager.take(coinOut, recipient, amountOut);
200
- } else {
201
- (coinOut, amountOut) = _payCounterAsset(poolManager, key, amount0, amount1, recipient);
204
+ function _takeCurrency(IPoolManager poolManager, Currency currency, address recipient, uint128 amount, address weth) private {
205
+ if (!currency.isAddressZero()) {
206
+ poolManager.take(currency, recipient, amount);
207
+ return;
208
+ }
209
+
210
+ poolManager.take(currency, address(this), amount);
211
+ IWETH(weth).deposit{value: amount}();
212
+ if (!IWETH(weth).transfer(recipient, amount)) {
213
+ revert WethTransferFailed();
202
214
  }
203
215
  }
204
216
 
205
- function _payCounterAsset(
217
+ function _payoutRecipient(
206
218
  IPoolManager poolManager,
207
- PoolKey memory key,
219
+ address recipient,
208
220
  int128 amount0,
209
221
  int128 amount1,
210
- address recipient
222
+ IHasSwapPath.PayoutSwapPath memory payoutPath,
223
+ address weth
211
224
  ) private returns (Currency coinOut, uint128 amountOut) {
212
- if (amount0 > 0) {
213
- coinOut = key.currency0;
214
- amountOut = uint128(amount0);
215
- poolManager.take(coinOut, recipient, amountOut);
216
- } else if (amount1 > 0) {
217
- coinOut = key.currency1;
218
- amountOut = uint128(amount1);
219
- poolManager.take(coinOut, recipient, amountOut);
225
+ // Convert to uint128, treating negative/zero as zero
226
+ uint128 amt0 = amount0 > 0 ? uint128(amount0) : 0;
227
+ uint128 amt1 = amount1 > 0 ? uint128(amount1) : 0;
228
+
229
+ // Use swapToPath which handles all cases:
230
+ // - Single positive delta: returns that currency
231
+ // - Dual positive deltas: swaps one to the other and returns combined amount
232
+ // - Multi-hop paths: handles coin -> backingCoin -> backingCoin's currency
233
+ (coinOut, amountOut) = UniV4SwapToCurrency.swapToPath(poolManager, amt0, amt1, payoutPath.currencyIn, payoutPath.path);
234
+
235
+ if (amountOut > 0) {
236
+ Currency payoutCurrency = coinOut;
237
+ _takeCurrency(poolManager, payoutCurrency, recipient, amountOut, weth);
238
+ if (payoutCurrency.isAddressZero()) {
239
+ coinOut = Currency.wrap(weth);
240
+ }
220
241
  }
221
242
  }
222
243
  }