@zoralabs/limit-orders 0.2.0 → 0.2.2

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 (154) hide show
  1. package/.turbo/turbo-build$colon$js.log +50 -49
  2. package/CHANGELOG.md +73 -0
  3. package/abis/ISetLimitOrderConfig.json +27 -0
  4. package/abis/IWETH.json +118 -0
  5. package/abis/IZoraLimitOrderBook.json +5 -0
  6. package/abis/LimitOrderLiquidity.json +7 -0
  7. package/abis/LimitOrderViews.json +62 -0
  8. package/abis/{SimpleAccessManaged.json → Ownable.json} +29 -10
  9. package/abis/Ownable2Step.json +115 -0
  10. package/abis/PermittedCallers.json +181 -0
  11. package/abis/SwapWithLimitOrders.json +134 -14
  12. package/abis/ZoraLimitOrderBook.json +187 -35
  13. package/cache/solidity-files-cache.json +1 -1
  14. package/dist/index.cjs +219 -34
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +219 -34
  17. package/dist/index.js.map +1 -1
  18. package/dist/wagmiGenerated.d.ts +254 -41
  19. package/dist/wagmiGenerated.d.ts.map +1 -1
  20. package/out/BalanceDelta.sol/BalanceDeltaLibrary.json +1 -1
  21. package/out/BeforeSwapDelta.sol/BeforeSwapDeltaLibrary.json +1 -1
  22. package/out/BitMath.sol/BitMath.json +1 -1
  23. package/out/BytesLib.sol/BytesLib.json +1 -1
  24. package/out/CoinCommon.sol/CoinCommon.json +1 -1
  25. package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
  26. package/out/CoinConstants.sol/CoinConstants.json +1 -1
  27. package/out/Context.sol/Context.json +1 -1
  28. package/out/Currency.sol/CurrencyLibrary.json +1 -1
  29. package/out/CurrencyReserves.sol/CurrencyReserves.json +1 -1
  30. package/out/CustomRevert.sol/CustomRevert.json +1 -1
  31. package/out/DopplerMath.sol/DopplerMath.json +1 -1
  32. package/out/FixedPoint128.sol/FixedPoint128.json +1 -1
  33. package/out/FixedPoint96.sol/FixedPoint96.json +1 -1
  34. package/out/FullMath.sol/FullMath.json +1 -1
  35. package/out/IAllowanceTransfer.sol/IAllowanceTransfer.json +1 -1
  36. package/out/ICoin.sol/ICoin.json +1 -1
  37. package/out/ICoin.sol/IHasCoinType.json +1 -1
  38. package/out/ICoin.sol/IHasPoolKey.json +1 -1
  39. package/out/ICoin.sol/IHasSwapPath.json +1 -1
  40. package/out/ICoin.sol/IHasTotalSupplyForPositions.json +1 -1
  41. package/out/IDeployedCoinVersionLookup.sol/IDeployedCoinVersionLookup.json +1 -1
  42. package/out/IDopplerErrors.sol/IDopplerErrors.json +1 -1
  43. package/out/IEIP712.sol/IEIP712.json +1 -1
  44. package/out/IERC1363.sol/IERC1363.json +1 -1
  45. package/out/IERC165.sol/IERC165.json +1 -1
  46. package/out/IERC20.sol/IERC20.json +1 -1
  47. package/out/IERC20Minimal.sol/IERC20Minimal.json +1 -1
  48. package/out/IERC6909Claims.sol/IERC6909Claims.json +1 -1
  49. package/out/IERC7572.sol/IERC7572.json +1 -1
  50. package/out/IExtsload.sol/IExtsload.json +1 -1
  51. package/out/IExttload.sol/IExttload.json +1 -1
  52. package/out/IHasRewardsRecipients.sol/IHasRewardsRecipients.json +1 -1
  53. package/out/IHooks.sol/IHooks.json +1 -1
  54. package/out/IMsgSender.sol/IMsgSender.json +1 -1
  55. package/out/IPoolManager.sol/IPoolManager.json +1 -1
  56. package/out/IProtocolFees.sol/IProtocolFees.json +1 -1
  57. package/out/ISetLimitOrderConfig.sol/ISetLimitOrderConfig.json +1 -0
  58. package/out/ISupportsLimitOrderFill.sol/ISupportsLimitOrderFill.json +1 -1
  59. package/out/ISwapPathRouter.sol/ISwapPathRouter.json +1 -1
  60. package/out/ISwapRouter.sol/ISwapRouter.json +1 -1
  61. package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -1
  62. package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4Hook.json +1 -1
  63. package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4HookWithUpdateableFee.json +1 -1
  64. package/out/IUpgradeableV4Hook.sol/IUpgradeableV4Hook.json +1 -1
  65. package/out/IWETH.sol/IWETH.json +1 -0
  66. package/out/IZoraHookRegistry.sol/IZoraHookRegistry.json +1 -1
  67. package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -1
  68. package/out/IZoraLimitOrderBookCoinsInterface.sol/IZoraLimitOrderBookCoinsInterface.json +1 -1
  69. package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.json +1 -1
  70. package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -1
  71. package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -1
  72. package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -1
  73. package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -1
  74. package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -1
  75. package/out/LimitOrderQueues.sol/LimitOrderQueues.json +1 -1
  76. package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -1
  77. package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -1
  78. package/out/LimitOrderViews.sol/LimitOrderViews.json +1 -0
  79. package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
  80. package/out/LiquidityAmounts.sol/LiquidityAmounts.json +1 -1
  81. package/out/LiquidityMath.sol/LiquidityMath.json +1 -1
  82. package/out/Lock.sol/Lock.json +1 -1
  83. package/out/NonzeroDeltaCount.sol/NonzeroDeltaCount.json +1 -1
  84. package/out/Ownable.sol/Ownable.json +1 -0
  85. package/out/Ownable2Step.sol/Ownable2Step.json +1 -0
  86. package/out/Path.sol/Path.json +1 -1
  87. package/out/PathKey.sol/PathKeyLibrary.json +1 -1
  88. package/out/Permit2Payments.sol/Permit2Payments.json +1 -1
  89. package/out/PermittedCallers.sol/PermittedCallers.json +1 -0
  90. package/out/PoolId.sol/PoolIdLibrary.json +1 -1
  91. package/out/Position.sol/Position.json +1 -1
  92. package/out/SafeCast.sol/SafeCast.json +1 -1
  93. package/out/SafeCast160.sol/SafeCast160.json +1 -1
  94. package/out/SafeERC20.sol/SafeERC20.json +1 -1
  95. package/out/SqrtPriceMath.sol/SqrtPriceMath.json +1 -1
  96. package/out/StateLibrary.sol/StateLibrary.json +1 -1
  97. package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
  98. package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
  99. package/out/TickBitmap.sol/TickBitmap.json +1 -1
  100. package/out/TickMath.sol/TickMath.json +1 -1
  101. package/out/TransientSlot.sol/TransientSlot.json +1 -1
  102. package/out/TransientStateLibrary.sol/TransientStateLibrary.json +1 -1
  103. package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -1
  104. package/out/UnsafeMath.sol/UnsafeMath.json +1 -1
  105. package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -1
  106. package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
  107. package/out/build-info/37e0124d88d60569.json +1 -0
  108. package/out/uniswap/BitMath.sol/BitMath.json +1 -1
  109. package/out/uniswap/CustomRevert.sol/CustomRevert.json +1 -1
  110. package/out/uniswap/FullMath.sol/FullMath.json +1 -1
  111. package/out/uniswap/SafeCast.sol/SafeCast.json +1 -1
  112. package/out/uniswap/TickMath.sol/TickMath.json +1 -1
  113. package/package/wagmiGenerated.ts +218 -33
  114. package/package.json +1 -1
  115. package/src/IZoraLimitOrderBook.sol +5 -5
  116. package/src/ZoraLimitOrderBook.sol +24 -41
  117. package/src/access/PermittedCallers.sol +41 -0
  118. package/src/libs/LimitOrderBitmap.sol +0 -51
  119. package/src/libs/LimitOrderCommon.sol +48 -30
  120. package/src/libs/LimitOrderCreate.sol +5 -18
  121. package/src/libs/LimitOrderFill.sol +32 -161
  122. package/src/libs/LimitOrderLiquidity.sol +92 -71
  123. package/src/libs/LimitOrderViews.sol +168 -0
  124. package/src/libs/LimitOrderWithdraw.sol +13 -4
  125. package/src/libs/SwapLimitOrders.sol +14 -7
  126. package/src/router/ISetLimitOrderConfig.sol +12 -0
  127. package/src/router/SwapWithLimitOrders.sol +46 -33
  128. package/test/LimitOrderAccessControl.t.sol +173 -156
  129. package/test/LimitOrderBitmap.t.sol +13 -7
  130. package/test/LimitOrderFill.t.sol +42 -4
  131. package/test/LimitOrderLibraries.t.sol +18 -10
  132. package/test/LimitOrderLiquidityPayouts.t.sol +280 -3
  133. package/test/LimitOrderWithdraw.t.sol +28 -1
  134. package/test/SwapWithLimitOrders.t.sol +3 -5
  135. package/test/SwapWithLimitOrdersRouter.t.sol +108 -13
  136. package/test/gas/LimitOrderFillGas.t.sol +0 -7
  137. package/test/gas/LimitOrderSwapGas.t.sol +0 -6
  138. package/test/unit/LimitOrderBitmapUnit.t.sol +0 -134
  139. package/test/unit/LimitOrderCreateUnit.t.sol +32 -0
  140. package/test/unit/SwapLimitOrdersUnit.t.sol +231 -33
  141. package/test/unit/SwapLimitOrdersValidation.t.sol +28 -42
  142. package/test/unit/SwapWithLimitOrdersUnit.t.sol +21 -88
  143. package/test/utils/BaseTest.sol +34 -22
  144. package/test/utils/MockWETH.sol +39 -0
  145. package/test/utils/TestableZoraLimitOrderBook.sol +5 -7
  146. package/abis/IAuthority.json +0 -31
  147. package/abis/SimpleAccessManager.json +0 -351
  148. package/out/IAuthority.sol/IAuthority.json +0 -1
  149. package/out/SimpleAccessManaged.sol/SimpleAccessManaged.json +0 -1
  150. package/out/SimpleAccessManager.sol/SimpleAccessManager.json +0 -1
  151. package/out/build-info/69718f10d1dc37f0.json +0 -1
  152. package/src/access/SimpleAccessManaged.sol +0 -76
  153. package/src/access/SimpleAccessManager.sol +0 -268
  154. package/test/SimpleAccessManager.t.sol +0 -420
@@ -0,0 +1,168 @@
1
+ // SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
2
+ // This software is licensed under the Zora Delayed Open Source License.
3
+ // Under this license, you may use, copy, modify, and distribute this software for
4
+ // non-commercial purposes only. Commercial use and competitive products are prohibited
5
+ // until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
6
+ // at which point this software automatically becomes available under the MIT License.
7
+ // Full license terms available at: https://docs.zora.co/coins/license
8
+ pragma solidity ^0.8.28;
9
+
10
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
12
+ import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
13
+ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
14
+ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
15
+ import {LiquidityAmounts} from "@zoralabs/coins/src/utils/uniswap/LiquidityAmounts.sol";
16
+
17
+ import {LimitOrderStorage} from "./LimitOrderStorage.sol";
18
+ import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
19
+ import {CoinCommon} from "@zoralabs/coins/src/libs/CoinCommon.sol";
20
+
21
+ /// @title LimitOrderViews
22
+ /// @notice External library for view/pure functions to reduce contract size via library linking
23
+ library LimitOrderViews {
24
+ using PoolIdLibrary for PoolKey;
25
+
26
+ int24 internal constant TICK_SENTINEL = type(int24).max;
27
+
28
+ /// @notice Snaps the current pool tick to the nearest aligned tick and returns its neighbors.
29
+ /// @dev Uniswap v4 ticks must lie on multiples of `tickSpacing`. We round the live
30
+ /// tick down to the nearest aligned value (handling negatives), clamp it inside
31
+ /// the pool's usable range, then compute the next/previous aligned ticks for
32
+ /// callers that need to build deterministic tick windows.
33
+ function _alignedTicks(
34
+ IPoolManager poolManager,
35
+ PoolKey memory key,
36
+ int24 spacing
37
+ ) private view returns (int24 anchorTick, int24 nextAligned, int24 prevAligned) {
38
+ (, int24 currentTick, , ) = StateLibrary.getSlot0(poolManager, key.toId());
39
+ int256 remainder;
40
+ assembly ("memory-safe") {
41
+ remainder := smod(currentTick, spacing)
42
+ }
43
+
44
+ if (remainder == 0) {
45
+ anchorTick = currentTick;
46
+ } else if (currentTick >= 0) {
47
+ anchorTick = int24(int256(currentTick) - remainder);
48
+ } else {
49
+ anchorTick = int24(int256(currentTick) - remainder - spacing);
50
+ }
51
+
52
+ int24 minUsable = TickMath.minUsableTick(spacing);
53
+ int24 maxUsable = TickMath.maxUsableTick(spacing);
54
+
55
+ if (anchorTick < minUsable) {
56
+ anchorTick = minUsable;
57
+ } else if (anchorTick > maxUsable) {
58
+ anchorTick = maxUsable;
59
+ }
60
+
61
+ nextAligned = anchorTick + spacing;
62
+ if (nextAligned > maxUsable) {
63
+ nextAligned = maxUsable;
64
+ }
65
+
66
+ prevAligned = anchorTick - spacing;
67
+ if (prevAligned < minUsable) {
68
+ prevAligned = minUsable;
69
+ }
70
+ }
71
+
72
+ /// @notice Calculate liquidity for a limit order given size and tick range.
73
+ /// @param isCurrency0 Whether the order is for currency0.
74
+ /// @param size The size of the order.
75
+ /// @param tickLower Lower tick of the position.
76
+ /// @param tickUpper Upper tick of the position.
77
+ /// @return The liquidity amount for the order.
78
+ function liquidityForOrder(bool isCurrency0, uint256 size, int24 tickLower, int24 tickUpper) external pure returns (uint128) {
79
+ uint160 sqrtPriceLower = TickMath.getSqrtPriceAtTick(tickLower);
80
+ uint160 sqrtPriceUpper = TickMath.getSqrtPriceAtTick(tickUpper);
81
+ return
82
+ isCurrency0
83
+ ? LiquidityAmounts.getLiquidityForAmount0(sqrtPriceLower, sqrtPriceUpper, size)
84
+ : LiquidityAmounts.getLiquidityForAmount1(sqrtPriceLower, sqrtPriceUpper, size);
85
+ }
86
+
87
+ /// @notice Validates and resolves tick range for fill operations.
88
+ /// @param state The limit order storage layout.
89
+ /// @param poolManager The Uniswap v4 pool manager.
90
+ /// @param providedKey The pool key provided by the caller.
91
+ /// @param isCurrency0 Whether targeting currency0 orders.
92
+ /// @param startTick User-provided start tick or sentinel.
93
+ /// @param endTick User-provided end tick or sentinel.
94
+ /// @return canonicalKey The canonical pool key from storage or provided key.
95
+ /// @return resolvedStart Concrete start tick after resolving sentinels.
96
+ /// @return resolvedEnd Concrete end tick after resolving sentinels.
97
+ function validateTickRange(
98
+ LimitOrderStorage.Layout storage state,
99
+ IPoolManager poolManager,
100
+ PoolKey calldata providedKey,
101
+ bool isCurrency0,
102
+ int24 startTick,
103
+ int24 endTick
104
+ ) external view returns (PoolKey memory canonicalKey, int24 resolvedStart, int24 resolvedEnd) {
105
+ bytes32 poolKeyHash = CoinCommon.hashPoolKey(providedKey);
106
+ canonicalKey = state.poolKeys[poolKeyHash];
107
+ if (canonicalKey.tickSpacing == 0) {
108
+ canonicalKey = providedKey;
109
+ if (canonicalKey.tickSpacing == 0) revert IZoraLimitOrderBook.InvalidPoolKey();
110
+ }
111
+
112
+ (resolvedStart, resolvedEnd) = _resolveTickRange(poolManager, canonicalKey, isCurrency0, startTick, endTick);
113
+ _validateTickRange(isCurrency0, resolvedStart, resolvedEnd);
114
+ }
115
+
116
+ /// @notice Derives concrete tick bounds from user input and current pool state.
117
+ /// @dev Callers may pass sentinel values (`-TICK_SENTINEL` / `TICK_SENTINEL`) to mean
118
+ /// "start at the current tick" or "extend one spacing away". This helper translates
119
+ /// those sentinels into real ticks by snapping to the pool's aligned tick grid and
120
+ /// offsetting one spacing in the appropriate direction so fills never include
121
+ /// orders created in the same transaction.
122
+ function _resolveTickRange(
123
+ IPoolManager poolManager,
124
+ PoolKey memory key,
125
+ bool isCurrency0,
126
+ int24 startTick,
127
+ int24 endTick
128
+ ) private view returns (int24 resolvedStart, int24 resolvedEnd) {
129
+ int24 spacing = key.tickSpacing;
130
+
131
+ bool startSentinel = startTick == -TICK_SENTINEL;
132
+ bool endSentinel = endTick == TICK_SENTINEL;
133
+
134
+ (int24 anchorTick, int24 nextAligned, int24 prevAligned) = _alignedTicks(poolManager, key, spacing);
135
+
136
+ if (startSentinel) {
137
+ // Treat sentinel start as anchoring the window at the aligned tick.
138
+ resolvedStart = anchorTick;
139
+ resolvedEnd = endSentinel ? (isCurrency0 ? nextAligned : prevAligned) : endTick;
140
+ return (resolvedStart, resolvedEnd);
141
+ }
142
+
143
+ if (endSentinel) {
144
+ resolvedStart = startTick;
145
+ resolvedEnd = isCurrency0 ? nextAligned : anchorTick;
146
+ return (resolvedStart, resolvedEnd);
147
+ }
148
+
149
+ resolvedStart = startTick;
150
+ resolvedEnd = endTick;
151
+ return (resolvedStart, resolvedEnd);
152
+ }
153
+
154
+ function _validateTickRange(bool isCurrency0, int24 startTick, int24 endTick) private pure {
155
+ if (startTick < TickMath.MIN_TICK || startTick > TickMath.MAX_TICK) {
156
+ revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
157
+ }
158
+ if (endTick < TickMath.MIN_TICK || endTick > TickMath.MAX_TICK) {
159
+ revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
160
+ }
161
+
162
+ if (isCurrency0) {
163
+ if (startTick > endTick) revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
164
+ } else {
165
+ if (startTick < endTick) revert IZoraLimitOrderBook.InvalidFillWindow(startTick, endTick, isCurrency0);
166
+ }
167
+ }
168
+ }
@@ -9,6 +9,8 @@ pragma solidity ^0.8.28;
9
9
 
10
10
  import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
11
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
12
+ import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
13
+ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
12
14
 
13
15
  import {LimitOrderStorage} from "./LimitOrderStorage.sol";
14
16
  import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
@@ -17,15 +19,16 @@ import {LimitOrderCommon} from "./LimitOrderCommon.sol";
17
19
  import {LimitOrderLiquidity} from "./LimitOrderLiquidity.sol";
18
20
 
19
21
  library LimitOrderWithdraw {
20
- function handleWithdrawOrdersCallback(LimitOrderStorage.Layout storage state, IPoolManager poolManager, bytes memory payload) internal {
22
+ function handleWithdrawOrdersCallback(LimitOrderStorage.Layout storage state, IPoolManager poolManager, address weth, bytes memory payload) internal {
21
23
  IZoraLimitOrderBook.WithdrawOrdersCallbackData memory data = abi.decode(payload, (IZoraLimitOrderBook.WithdrawOrdersCallbackData));
22
24
 
23
- withdrawOrders(state, poolManager, data.maker, data.orderIds, data.coin, data.minAmountOut, data.recipient);
25
+ withdrawOrders(state, poolManager, weth, data.maker, data.orderIds, data.coin, data.minAmountOut, data.recipient);
24
26
  }
25
27
 
26
28
  function withdrawOrders(
27
29
  LimitOrderStorage.Layout storage state,
28
30
  IPoolManager poolManager,
31
+ address weth,
29
32
  address maker,
30
33
  bytes32[] memory orderIds,
31
34
  address coin,
@@ -47,7 +50,7 @@ library LimitOrderWithdraw {
47
50
  require(order.status == LimitOrderTypes.OrderStatus.OPEN, IZoraLimitOrderBook.OrderClosed());
48
51
 
49
52
  uint128 orderSize = order.orderSize; // Cache before cancellation
50
- address currentCoin = _cancelOrder(state, poolManager, maker, orderId, order, recipient);
53
+ address currentCoin = _cancelOrder(state, poolManager, weth, maker, orderId, order, recipient);
51
54
 
52
55
  // Validate order coin matches expected coin
53
56
  if (currentCoin != coin) {
@@ -69,6 +72,7 @@ library LimitOrderWithdraw {
69
72
  function _cancelOrder(
70
73
  LimitOrderStorage.Layout storage state,
71
74
  IPoolManager poolManager,
75
+ address weth,
72
76
  address maker,
73
77
  bytes32 orderId,
74
78
  LimitOrderTypes.LimitOrder storage order,
@@ -77,6 +81,11 @@ library LimitOrderWithdraw {
77
81
  PoolKey memory key = state.poolKeys[order.poolKeyHash];
78
82
  require(key.tickSpacing != 0, IZoraLimitOrderBook.InvalidOrder());
79
83
 
84
+ // Prevent withdrawal of fillable orders - they must be filled instead
85
+ (, int24 currentTick, , ) = StateLibrary.getSlot0(poolManager, PoolIdLibrary.toId(key));
86
+ bool fillable = order.isCurrency0 ? currentTick >= order.tickUpper : currentTick <= order.tickLower;
87
+ require(!fillable, IZoraLimitOrderBook.OrderFillable());
88
+
80
89
  int24 orderTick = LimitOrderCommon.getOrderTick(order);
81
90
  coin = LimitOrderCommon.getOrderCoin(key, order.isCurrency0);
82
91
 
@@ -93,7 +102,7 @@ library LimitOrderWithdraw {
93
102
  LimitOrderCommon.removeOrder(state, key, coin, tickQueue, order);
94
103
 
95
104
  // External call after state is updated
96
- LimitOrderLiquidity.burnAndRefund(poolManager, key, tickLower, tickUpper, liquidity, orderId, recipient, isCurrency0);
105
+ LimitOrderLiquidity.burnAndRefund(poolManager, key, tickLower, tickUpper, liquidity, orderId, recipient, isCurrency0, weth);
97
106
 
98
107
  emit IZoraLimitOrderBook.LimitOrderUpdated(maker, coin, order.poolKeyHash, isCurrency0, orderTick, 0, orderId, true);
99
108
  }
@@ -44,9 +44,6 @@ library SwapLimitOrders {
44
44
  /// @dev sqrt(1e18) - scales sqrt calculations without precision loss
45
45
  uint256 internal constant SQRT_MULTIPLE_SCALE = 1e9;
46
46
 
47
- /// @dev Minimum coins to create orders - prevents dust
48
- uint256 internal constant MIN_LIMIT_ORDER_SIZE = 1e18;
49
-
50
47
  /// @notice Multiples and percentages arrays have different lengths
51
48
  error LengthMismatch();
52
49
 
@@ -95,7 +92,6 @@ library SwapLimitOrders {
95
92
  /// @return unallocated Amount of totalSize not allocated (dust or partial fill)
96
93
  /// @dev Orders are sized sequentially: each order takes its percentage of remaining balance.
97
94
  /// Orders with zero size after rounding are skipped - arrays shrink to match.
98
- /// Returns empty arrays if totalSize < MIN_LIMIT_ORDER_SIZE.
99
95
  function computeOrders(
100
96
  PoolKey memory key,
101
97
  bool isCurrency0,
@@ -104,8 +100,18 @@ library SwapLimitOrders {
104
100
  uint160 sqrtPriceX96,
105
101
  LimitOrderConfig memory config
106
102
  ) internal pure returns (Orders memory o, uint128 allocated, uint128 unallocated) {
107
- if (totalSize < MIN_LIMIT_ORDER_SIZE) {
108
- unallocated = uint128(totalSize);
103
+ if (totalSize == 0) {
104
+ return (o, allocated, unallocated);
105
+ }
106
+
107
+ // Skip order creation when at tick boundaries
108
+ // For currency0 (buy orders): cannot place if baseTick is at maxTick
109
+ // For currency1 (sell orders): cannot place if baseTick is at minTick
110
+ int24 maxTick = TickMath.maxUsableTick(key.tickSpacing);
111
+ int24 alignedBaseTick = DopplerMath.alignTickToTickSpacing(isCurrency0, baseTick, key.tickSpacing);
112
+
113
+ if (isCurrency0 ? alignedBaseTick >= maxTick : alignedBaseTick <= -maxTick) {
114
+ unallocated = totalSize;
109
115
  return (o, allocated, unallocated);
110
116
  }
111
117
 
@@ -185,7 +191,8 @@ library SwapLimitOrders {
185
191
  aligned = minTick;
186
192
  }
187
193
 
188
- int24 minAway = baseTick + (isCurrency0 ? key.tickSpacing : -key.tickSpacing);
194
+ int24 alignedBaseTick = DopplerMath.alignTickToTickSpacing(isCurrency0, baseTick, key.tickSpacing);
195
+ int24 minAway = alignedBaseTick + (isCurrency0 ? key.tickSpacing : -key.tickSpacing);
189
196
  if (isCurrency0) {
190
197
  if (aligned < minAway) aligned = minAway;
191
198
  } else {
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.20;
3
+
4
+ import {LimitOrderConfig} from "../libs/SwapLimitOrders.sol";
5
+
6
+ /// @title ISetLimitOrderConfig
7
+ /// @notice Interface for setting limit order configuration
8
+ interface ISetLimitOrderConfig {
9
+ /// @notice Sets the canonical limit order configuration
10
+ /// @param config The new limit order configuration
11
+ function setLimitOrderConfig(LimitOrderConfig memory config) external;
12
+ }
@@ -18,6 +18,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
18
18
 
19
19
  import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
20
20
  import {SwapLimitOrders, LimitOrderConfig, Orders} from "../libs/SwapLimitOrders.sol";
21
+ import {ISetLimitOrderConfig} from "./ISetLimitOrderConfig.sol";
21
22
  import {ISwapRouter} from "@zoralabs/shared-contracts/interfaces/uniswap/ISwapRouter.sol";
22
23
  import {ISupportsLimitOrderFill} from "@zoralabs/coins/src/interfaces/ISupportsLimitOrderFill.sol";
23
24
  import {IMsgSender} from "@zoralabs/coins/src/interfaces/IMsgSender.sol";
@@ -25,8 +26,9 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
25
26
  import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol";
26
27
  import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
27
28
  import {V3ToV4SwapLib} from "@zoralabs/coins/src/libs/V3ToV4SwapLib.sol";
28
- import {SimpleAccessManaged} from "../access/SimpleAccessManaged.sol";
29
- import {Permit2Payments} from "../libs/Permit2Payments.sol";
29
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
30
+ import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
31
+ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
30
32
 
31
33
  /// @title SwapWithLimitOrders
32
34
  /// @notice Standalone router contract that executes swaps with automatic limit order placement and filling.
@@ -35,7 +37,7 @@ import {Permit2Payments} from "../libs/Permit2Payments.sol";
35
37
  /// Users call swapWithLimitOrders() directly, which triggers the unlock callback flow.
36
38
  /// Uses Permit2 for token approvals, matching the universal-router pattern.
37
39
  /// @author oveddan
38
- contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
40
+ contract SwapWithLimitOrders is ISetLimitOrderConfig, Ownable2Step, IMsgSender {
39
41
  using SafeERC20 for IERC20;
40
42
  using BalanceDeltaLibrary for BalanceDelta;
41
43
  using CurrencyLibrary for Currency;
@@ -51,6 +53,9 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
51
53
  /// @notice The Uniswap V3 swap router
52
54
  ISwapRouter public immutable swapRouter;
53
55
 
56
+ /// @notice The Permit2 contract (immutable, same address on all chains)
57
+ IAllowanceTransfer internal immutable PERMIT2;
58
+
54
59
  /// @notice Canonical limit order configuration
55
60
  LimitOrderConfig private _limitOrderConfig;
56
61
 
@@ -97,13 +102,18 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
97
102
  /// @notice Emitted when a swap with limit order placement is executed
98
103
  /// @param orders Array of created orders with their configuration. Only includes orders
99
104
  /// that were actually created (skipped rungs due to rounding are omitted).
105
+ /// @param amount0 The amount of currency0 swapped (negative = paid, positive = received)
106
+ /// @param amount1 The amount of currency1 swapped (negative = paid, positive = received)
107
+ /// @param sqrtPriceX96 The sqrt price after the swap, used for USD price computation
100
108
  event SwapWithLimitOrdersExecuted(
101
109
  address indexed sender,
102
110
  address indexed recipient,
103
111
  PoolKey poolKey,
104
- BalanceDelta delta,
105
112
  int24 tickBeforeSwap,
106
113
  int24 tickAfterSwap,
114
+ int128 amount0,
115
+ int128 amount1,
116
+ uint160 sqrtPriceX96,
107
117
  CreatedOrder[] orders
108
118
  );
109
119
 
@@ -113,9 +123,6 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
113
123
  /// @notice Error thrown when caller is not the pool manager
114
124
  error OnlyPoolManager();
115
125
 
116
- /// @notice Error thrown when caller is not the authority
117
- error OnlyAuthority();
118
-
119
126
  /// @notice Error thrown when config does not match canonical config
120
127
  error InvalidLimitOrderConfig();
121
128
 
@@ -133,7 +140,8 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
133
140
  /// @param zoraLimitOrderBook_ The limit order book contract
134
141
  /// @param swapRouter_ The Uniswap V3 swap router
135
142
  /// @param permit2_ The Permit2 contract address (0x000000000022D473030F116dDEE9F6B43aC78BA3)
136
- constructor(IPoolManager poolManager_, IZoraLimitOrderBook zoraLimitOrderBook_, ISwapRouter swapRouter_, address permit2_) Permit2Payments(permit2_) {
143
+ /// @param owner_ The owner address
144
+ constructor(IPoolManager poolManager_, IZoraLimitOrderBook zoraLimitOrderBook_, ISwapRouter swapRouter_, address permit2_, address owner_) Ownable(owner_) {
137
145
  require(address(poolManager_) != address(0), "PoolManager cannot be zero");
138
146
  require(address(zoraLimitOrderBook_) != address(0), "ZoraLimitOrderBook cannot be zero");
139
147
  require(address(swapRouter_) != address(0), "SwapRouter cannot be zero");
@@ -141,6 +149,7 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
141
149
  poolManager = poolManager_;
142
150
  zoraLimitOrderBook = zoraLimitOrderBook_;
143
151
  swapRouter = swapRouter_;
152
+ PERMIT2 = IAllowanceTransfer(permit2_);
144
153
  }
145
154
 
146
155
  /// @inheritdoc IMsgSender
@@ -150,10 +159,9 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
150
159
  }
151
160
 
152
161
  /// @notice Sets the canonical limit order configuration
153
- /// @dev Only callable by zoraLimitOrderBook.authority()
162
+ /// @dev Only callable by the owner
154
163
  /// @param config The new limit order configuration
155
- function setLimitOrderConfig(LimitOrderConfig memory config) external {
156
- require(msg.sender == SimpleAccessManaged(address(zoraLimitOrderBook)).authority(), OnlyAuthority());
164
+ function setLimitOrderConfig(LimitOrderConfig memory config) external onlyOwner {
157
165
  SwapLimitOrders.validate(config);
158
166
  _limitOrderConfig = config;
159
167
  emit LimitOrderConfigUpdated(config.multiples, config.percentages);
@@ -218,17 +226,30 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
218
226
  // Execute V4 swaps + create orders via unlock callback
219
227
  bytes memory result = poolManager.unlock(abi.encode(callbackData));
220
228
 
221
- (CreatedOrder[] memory orders, bool isCoinCurrency0, int24 tickAfterSwap) = abi.decode(result, (CreatedOrder[], bool, int24));
229
+ (CreatedOrder[] memory orders, bool isCoinCurrency0, int24 tickAfterSwap, uint160 sqrtPriceX96, BalanceDelta targetPoolDelta) = abi.decode(
230
+ result,
231
+ (CreatedOrder[], bool, int24, uint160, BalanceDelta)
232
+ );
222
233
 
223
234
  // Check if hook supports limit order filling using ERC165
224
235
  bool hookSupportsFill = IERC165(address(targetPool.hooks)).supportsInterface(type(ISupportsLimitOrderFill).interfaceId);
225
236
 
226
237
  // Router-based filling for legacy hooks
227
- if (!hookSupportsFill && orders.length > 0 && tickBeforeSwap != tickAfterSwap) {
228
- _fillOrders(targetPool, !isCoinCurrency0, tickBeforeSwap, tickAfterSwap);
238
+ if (!hookSupportsFill && tickBeforeSwap != tickAfterSwap) {
239
+ _fillOrders(targetPool, isCoinCurrency0, tickBeforeSwap, tickAfterSwap);
229
240
  }
230
241
 
231
- emit SwapWithLimitOrdersExecuted(msg.sender, params.recipient, targetPool, BalanceDelta.wrap(0), tickBeforeSwap, tickAfterSwap, orders);
242
+ emit SwapWithLimitOrdersExecuted(
243
+ msg.sender,
244
+ params.recipient,
245
+ targetPool,
246
+ tickBeforeSwap,
247
+ tickAfterSwap,
248
+ targetPoolDelta.amount0(),
249
+ targetPoolDelta.amount1(),
250
+ sqrtPriceX96,
251
+ orders
252
+ );
232
253
 
233
254
  // Clear maker from transient storage
234
255
  TransientSlot.tstore(slot, address(0));
@@ -271,7 +292,7 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
271
292
  // Settle currencies with pool manager
272
293
  _settleCurrencies(callbackData.currencyReceived, callbackData.currencyAmount, coinAddress, unallocated, callbackData.recipient);
273
294
 
274
- return abi.encode(createdOrders, isCoinCurrency0, currentTick);
295
+ return abi.encode(createdOrders, isCoinCurrency0, currentTick, sqrtPriceX96, swapResult.targetPoolDelta);
275
296
  }
276
297
 
277
298
  /// @notice Executes V4 multi-hop swaps and validates output
@@ -398,33 +419,25 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
398
419
 
399
420
  poolManager.settle();
400
421
  } else {
422
+ // sync before settle for native ETH
423
+ poolManager.sync(currency);
401
424
  poolManager.settle{value: amount}();
402
425
  }
403
426
  }
404
427
 
405
428
  /// @notice Fills limit orders within the tick range crossed by the swap
406
429
  /// @param poolKey The pool key
407
- /// @param isCurrency0 Whether to fill currency0 orders
408
430
  /// @param tickBeforeSwap The tick before the swap
409
431
  /// @param tickAfterSwap The tick after the swap
410
- function _fillOrders(PoolKey memory poolKey, bool isCurrency0, int24 tickBeforeSwap, int24 tickAfterSwap) internal {
411
- // Ensure ticks are in the correct order for fill validation
412
- // For currency0 orders: startTick <= endTick (ascending)
413
- // For currency1 orders: startTick >= endTick (descending)
414
- int24 startTick;
415
- int24 endTick;
416
- if (isCurrency0) {
417
- // Currency0 orders need ascending tick range
418
- startTick = tickBeforeSwap < tickAfterSwap ? tickBeforeSwap : tickAfterSwap;
419
- endTick = tickBeforeSwap < tickAfterSwap ? tickAfterSwap : tickBeforeSwap;
420
- } else {
421
- // Currency1 orders need descending tick range
422
- startTick = tickBeforeSwap > tickAfterSwap ? tickBeforeSwap : tickAfterSwap;
423
- endTick = tickBeforeSwap > tickAfterSwap ? tickAfterSwap : tickBeforeSwap;
432
+ function _fillOrders(PoolKey memory poolKey, bool, int24 tickBeforeSwap, int24 tickAfterSwap) internal {
433
+ if (tickAfterSwap == tickBeforeSwap) {
434
+ return;
424
435
  }
425
436
 
426
- // Call fill in locked mode - will trigger unlock/callback flow in ZoraLimitOrderBook
427
- zoraLimitOrderBook.fill(poolKey, isCurrency0, startTick, endTick, 0, address(0));
437
+ // Derive fill direction from tick movement
438
+ bool isCurrency0 = tickAfterSwap > tickBeforeSwap;
439
+
440
+ zoraLimitOrderBook.fill(poolKey, isCurrency0, tickBeforeSwap, tickAfterSwap, 0, address(0));
428
441
  }
429
442
 
430
443
  /// @notice Validates that the provided config matches the canonical config