@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.
- package/.turbo/turbo-build$colon$js.log +47 -45
- package/CHANGELOG.md +61 -0
- package/abis/IWETH.json +118 -0
- package/abis/IZoraLimitOrderBook.json +5 -0
- package/abis/LimitOrderLiquidity.json +7 -0
- package/abis/LimitOrderViews.json +62 -0
- package/abis/SwapWithLimitOrders.json +18 -11
- package/abis/ZoraLimitOrderBook.json +28 -0
- package/cache/solidity-files-cache.json +1 -1
- package/dist/index.cjs +29 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +29 -8
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +37 -9
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/out/BytesLib.sol/BytesLib.json +1 -1
- package/out/CoinCommon.sol/CoinCommon.json +1 -1
- package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
- package/out/CoinConstants.sol/CoinConstants.json +1 -1
- package/out/DopplerMath.sol/DopplerMath.json +1 -1
- package/out/FixedPoint96.sol/FixedPoint96.json +1 -1
- package/out/ISwapRouter.sol/ISwapRouter.json +1 -1
- package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -1
- package/out/IWETH.sol/IWETH.json +1 -0
- package/out/IZoraHookRegistry.sol/IZoraHookRegistry.json +1 -1
- package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -1
- package/out/IZoraLimitOrderBookCoinsInterface.sol/IZoraLimitOrderBookCoinsInterface.json +1 -1
- package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.json +1 -1
- package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -1
- package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -1
- package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -1
- package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -1
- package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -1
- package/out/LimitOrderQueues.sol/LimitOrderQueues.json +1 -1
- package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -1
- package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -1
- package/out/LimitOrderViews.sol/LimitOrderViews.json +1 -0
- package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
- package/out/LiquidityAmounts.sol/LiquidityAmounts.json +1 -1
- package/out/Path.sol/Path.json +1 -1
- package/out/Permit2Payments.sol/Permit2Payments.json +1 -1
- package/out/SimpleAccessManaged.sol/SimpleAccessManaged.json +1 -1
- package/out/SimpleAccessManager.sol/SimpleAccessManager.json +1 -1
- package/out/SqrtPriceMath.sol/SqrtPriceMath.json +1 -1
- package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
- package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
- package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -1
- package/out/UnsafeMath.sol/UnsafeMath.json +1 -1
- package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -1
- package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
- package/out/build-info/{69718f10d1dc37f0.json → 876cc09bc44cc8a7.json} +1 -1
- package/out/uniswap/BitMath.sol/BitMath.json +1 -1
- package/out/uniswap/CustomRevert.sol/CustomRevert.json +1 -1
- package/out/uniswap/FullMath.sol/FullMath.json +1 -1
- package/out/uniswap/SafeCast.sol/SafeCast.json +1 -1
- package/out/uniswap/TickMath.sol/TickMath.json +1 -1
- package/package/wagmiGenerated.ts +28 -7
- package/package.json +1 -1
- package/src/IZoraLimitOrderBook.sol +2 -0
- package/src/ZoraLimitOrderBook.sol +22 -8
- package/src/libs/LimitOrderBitmap.sol +0 -51
- package/src/libs/LimitOrderCommon.sol +48 -30
- package/src/libs/LimitOrderCreate.sol +5 -18
- package/src/libs/LimitOrderFill.sol +32 -161
- package/src/libs/LimitOrderLiquidity.sol +92 -71
- package/src/libs/LimitOrderViews.sol +168 -0
- package/src/libs/LimitOrderWithdraw.sol +13 -4
- package/src/libs/SwapLimitOrders.sol +14 -7
- package/src/router/SwapWithLimitOrders.sol +40 -26
- package/test/LimitOrderBitmap.t.sol +13 -7
- package/test/LimitOrderFill.t.sol +43 -0
- package/test/LimitOrderLibraries.t.sol +18 -10
- package/test/LimitOrderLiquidityPayouts.t.sol +280 -3
- package/test/LimitOrderWithdraw.t.sol +28 -1
- package/test/SwapWithLimitOrders.t.sol +3 -3
- package/test/SwapWithLimitOrdersRouter.t.sol +108 -11
- package/test/unit/LimitOrderBitmapUnit.t.sol +0 -134
- package/test/unit/LimitOrderCreateUnit.t.sol +32 -0
- package/test/unit/SwapLimitOrdersUnit.t.sol +231 -33
- package/test/unit/SwapLimitOrdersValidation.t.sol +20 -34
- package/test/unit/SwapWithLimitOrdersUnit.t.sol +21 -88
- package/test/utils/BaseTest.sol +29 -8
- package/test/utils/MockWETH.sol +39 -0
- package/test/utils/TestableZoraLimitOrderBook.sol +5 -7
|
@@ -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
|
|
108
|
-
|
|
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
|
|
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 {
|
|
@@ -26,7 +26,7 @@ import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol";
|
|
|
26
26
|
import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
|
|
27
27
|
import {V3ToV4SwapLib} from "@zoralabs/coins/src/libs/V3ToV4SwapLib.sol";
|
|
28
28
|
import {SimpleAccessManaged} from "../access/SimpleAccessManaged.sol";
|
|
29
|
-
import {
|
|
29
|
+
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
|
|
30
30
|
|
|
31
31
|
/// @title SwapWithLimitOrders
|
|
32
32
|
/// @notice Standalone router contract that executes swaps with automatic limit order placement and filling.
|
|
@@ -35,7 +35,7 @@ import {Permit2Payments} from "../libs/Permit2Payments.sol";
|
|
|
35
35
|
/// Users call swapWithLimitOrders() directly, which triggers the unlock callback flow.
|
|
36
36
|
/// Uses Permit2 for token approvals, matching the universal-router pattern.
|
|
37
37
|
/// @author oveddan
|
|
38
|
-
contract SwapWithLimitOrders is IMsgSender
|
|
38
|
+
contract SwapWithLimitOrders is IMsgSender {
|
|
39
39
|
using SafeERC20 for IERC20;
|
|
40
40
|
using BalanceDeltaLibrary for BalanceDelta;
|
|
41
41
|
using CurrencyLibrary for Currency;
|
|
@@ -51,6 +51,9 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
|
|
|
51
51
|
/// @notice The Uniswap V3 swap router
|
|
52
52
|
ISwapRouter public immutable swapRouter;
|
|
53
53
|
|
|
54
|
+
/// @notice The Permit2 contract (immutable, same address on all chains)
|
|
55
|
+
IAllowanceTransfer internal immutable PERMIT2;
|
|
56
|
+
|
|
54
57
|
/// @notice Canonical limit order configuration
|
|
55
58
|
LimitOrderConfig private _limitOrderConfig;
|
|
56
59
|
|
|
@@ -97,13 +100,18 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
|
|
|
97
100
|
/// @notice Emitted when a swap with limit order placement is executed
|
|
98
101
|
/// @param orders Array of created orders with their configuration. Only includes orders
|
|
99
102
|
/// that were actually created (skipped rungs due to rounding are omitted).
|
|
103
|
+
/// @param amount0 The amount of currency0 swapped (negative = paid, positive = received)
|
|
104
|
+
/// @param amount1 The amount of currency1 swapped (negative = paid, positive = received)
|
|
105
|
+
/// @param sqrtPriceX96 The sqrt price after the swap, used for USD price computation
|
|
100
106
|
event SwapWithLimitOrdersExecuted(
|
|
101
107
|
address indexed sender,
|
|
102
108
|
address indexed recipient,
|
|
103
109
|
PoolKey poolKey,
|
|
104
|
-
BalanceDelta delta,
|
|
105
110
|
int24 tickBeforeSwap,
|
|
106
111
|
int24 tickAfterSwap,
|
|
112
|
+
int128 amount0,
|
|
113
|
+
int128 amount1,
|
|
114
|
+
uint160 sqrtPriceX96,
|
|
107
115
|
CreatedOrder[] orders
|
|
108
116
|
);
|
|
109
117
|
|
|
@@ -133,7 +141,7 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
|
|
|
133
141
|
/// @param zoraLimitOrderBook_ The limit order book contract
|
|
134
142
|
/// @param swapRouter_ The Uniswap V3 swap router
|
|
135
143
|
/// @param permit2_ The Permit2 contract address (0x000000000022D473030F116dDEE9F6B43aC78BA3)
|
|
136
|
-
constructor(IPoolManager poolManager_, IZoraLimitOrderBook zoraLimitOrderBook_, ISwapRouter swapRouter_, address permit2_)
|
|
144
|
+
constructor(IPoolManager poolManager_, IZoraLimitOrderBook zoraLimitOrderBook_, ISwapRouter swapRouter_, address permit2_) {
|
|
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
|
|
@@ -218,17 +227,30 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
|
|
|
218
227
|
// Execute V4 swaps + create orders via unlock callback
|
|
219
228
|
bytes memory result = poolManager.unlock(abi.encode(callbackData));
|
|
220
229
|
|
|
221
|
-
(CreatedOrder[] memory orders, bool isCoinCurrency0, int24 tickAfterSwap) = abi.decode(
|
|
230
|
+
(CreatedOrder[] memory orders, bool isCoinCurrency0, int24 tickAfterSwap, uint160 sqrtPriceX96, BalanceDelta targetPoolDelta) = abi.decode(
|
|
231
|
+
result,
|
|
232
|
+
(CreatedOrder[], bool, int24, uint160, BalanceDelta)
|
|
233
|
+
);
|
|
222
234
|
|
|
223
235
|
// Check if hook supports limit order filling using ERC165
|
|
224
236
|
bool hookSupportsFill = IERC165(address(targetPool.hooks)).supportsInterface(type(ISupportsLimitOrderFill).interfaceId);
|
|
225
237
|
|
|
226
238
|
// Router-based filling for legacy hooks
|
|
227
|
-
if (!hookSupportsFill &&
|
|
228
|
-
_fillOrders(targetPool,
|
|
239
|
+
if (!hookSupportsFill && tickBeforeSwap != tickAfterSwap) {
|
|
240
|
+
_fillOrders(targetPool, isCoinCurrency0, tickBeforeSwap, tickAfterSwap);
|
|
229
241
|
}
|
|
230
242
|
|
|
231
|
-
emit SwapWithLimitOrdersExecuted(
|
|
243
|
+
emit SwapWithLimitOrdersExecuted(
|
|
244
|
+
msg.sender,
|
|
245
|
+
params.recipient,
|
|
246
|
+
targetPool,
|
|
247
|
+
tickBeforeSwap,
|
|
248
|
+
tickAfterSwap,
|
|
249
|
+
targetPoolDelta.amount0(),
|
|
250
|
+
targetPoolDelta.amount1(),
|
|
251
|
+
sqrtPriceX96,
|
|
252
|
+
orders
|
|
253
|
+
);
|
|
232
254
|
|
|
233
255
|
// Clear maker from transient storage
|
|
234
256
|
TransientSlot.tstore(slot, address(0));
|
|
@@ -271,7 +293,7 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
|
|
|
271
293
|
// Settle currencies with pool manager
|
|
272
294
|
_settleCurrencies(callbackData.currencyReceived, callbackData.currencyAmount, coinAddress, unallocated, callbackData.recipient);
|
|
273
295
|
|
|
274
|
-
return abi.encode(createdOrders, isCoinCurrency0, currentTick);
|
|
296
|
+
return abi.encode(createdOrders, isCoinCurrency0, currentTick, sqrtPriceX96, swapResult.targetPoolDelta);
|
|
275
297
|
}
|
|
276
298
|
|
|
277
299
|
/// @notice Executes V4 multi-hop swaps and validates output
|
|
@@ -398,33 +420,25 @@ contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
|
|
|
398
420
|
|
|
399
421
|
poolManager.settle();
|
|
400
422
|
} else {
|
|
423
|
+
// sync before settle for native ETH
|
|
424
|
+
poolManager.sync(currency);
|
|
401
425
|
poolManager.settle{value: amount}();
|
|
402
426
|
}
|
|
403
427
|
}
|
|
404
428
|
|
|
405
429
|
/// @notice Fills limit orders within the tick range crossed by the swap
|
|
406
430
|
/// @param poolKey The pool key
|
|
407
|
-
/// @param isCurrency0 Whether to fill currency0 orders
|
|
408
431
|
/// @param tickBeforeSwap The tick before the swap
|
|
409
432
|
/// @param tickAfterSwap The tick after the swap
|
|
410
|
-
function _fillOrders(PoolKey memory poolKey, bool
|
|
411
|
-
|
|
412
|
-
|
|
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;
|
|
433
|
+
function _fillOrders(PoolKey memory poolKey, bool, int24 tickBeforeSwap, int24 tickAfterSwap) internal {
|
|
434
|
+
if (tickAfterSwap == tickBeforeSwap) {
|
|
435
|
+
return;
|
|
424
436
|
}
|
|
425
437
|
|
|
426
|
-
//
|
|
427
|
-
|
|
438
|
+
// Derive fill direction from tick movement
|
|
439
|
+
bool isCurrency0 = tickAfterSwap > tickBeforeSwap;
|
|
440
|
+
|
|
441
|
+
zoraLimitOrderBook.fill(poolKey, isCurrency0, tickBeforeSwap, tickAfterSwap, 0, address(0));
|
|
428
442
|
}
|
|
429
443
|
|
|
430
444
|
/// @notice Validates that the provided config matches the canonical config
|
|
@@ -30,7 +30,8 @@ contract LimitOrderBitmapTest is BaseTest {
|
|
|
30
30
|
|
|
31
31
|
// Verify all ticks are initialized in bitmap
|
|
32
32
|
for (uint256 i; i < orderTicks.length; ++i) {
|
|
33
|
-
|
|
33
|
+
int24 fillableTick = _fillableTick(isCurrency0, orderTicks[i], key.tickSpacing);
|
|
34
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be initialized");
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -52,7 +53,8 @@ contract LimitOrderBitmapTest is BaseTest {
|
|
|
52
53
|
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
53
54
|
|
|
54
55
|
// Verify tick is initialized
|
|
55
|
-
|
|
56
|
+
int24 fillableTick = _fillableTick(isCurrency0, orderTicks[0], key.tickSpacing);
|
|
57
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be initialized after create");
|
|
56
58
|
|
|
57
59
|
// Withdraw order
|
|
58
60
|
bytes32[] memory orderIds = new bytes32[](1);
|
|
@@ -61,7 +63,7 @@ contract LimitOrderBitmapTest is BaseTest {
|
|
|
61
63
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
62
64
|
|
|
63
65
|
// Verify tick is cleared in bitmap
|
|
64
|
-
assertFalse(_isTickInitialized(poolKeyHash, orderCoin,
|
|
66
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be cleared after withdraw");
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function test_bitmap_RemainsSetWithPartialOrders() public {
|
|
@@ -95,7 +97,8 @@ contract LimitOrderBitmapTest is BaseTest {
|
|
|
95
97
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
96
98
|
|
|
97
99
|
// Bitmap should still be set because second order remains
|
|
98
|
-
|
|
100
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
101
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should remain initialized with remaining order");
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
function test_bitmap_wordBoundaries() public {
|
|
@@ -129,7 +132,8 @@ contract LimitOrderBitmapTest is BaseTest {
|
|
|
129
132
|
|
|
130
133
|
// Verify all boundary ticks are initialized
|
|
131
134
|
for (uint256 i = 0; i < boundaryTicks.length; i++) {
|
|
132
|
-
|
|
135
|
+
int24 fillableTick = _fillableTick(isCurrency0, boundaryTicks[i], spacing);
|
|
136
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, spacing), "boundary tick should be initialized");
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -171,8 +175,10 @@ contract LimitOrderBitmapTest is BaseTest {
|
|
|
171
175
|
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
172
176
|
|
|
173
177
|
// Verify extreme ticks are initialized
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
int24 fillableTick0 = _fillableTick(isCurrency0, extremeTicks[0], spacing);
|
|
179
|
+
int24 fillableTick1 = _fillableTick(isCurrency0, extremeTicks[1], spacing);
|
|
180
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick0, spacing), "far tick 1 should be initialized");
|
|
181
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick1, spacing), "far tick 2 should be initialized");
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
function _fundAndApprove(address user, address token, uint256 amount) internal {
|
|
@@ -814,6 +814,49 @@ contract LimitOrderFillTest is BaseTest {
|
|
|
814
814
|
assertEq(fills.length, 2, "should fill default max count of 2 orders");
|
|
815
815
|
}
|
|
816
816
|
|
|
817
|
+
/// @notice Tests fill() caps maxFillCount to configured default
|
|
818
|
+
function test_fill_maxFillCountExceedsDefault_isCapped() public {
|
|
819
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
820
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
821
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
822
|
+
|
|
823
|
+
// Set max fill count to 2
|
|
824
|
+
vm.prank(users.factoryOwner);
|
|
825
|
+
limitOrderBook.setMaxFillCount(2);
|
|
826
|
+
|
|
827
|
+
// Create 5 fillable orders
|
|
828
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 5, 20e18);
|
|
829
|
+
uint256 totalSize;
|
|
830
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
831
|
+
totalSize += orderSizes[i];
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (orderCoin == address(0)) {
|
|
835
|
+
vm.deal(users.seller, totalSize);
|
|
836
|
+
} else {
|
|
837
|
+
deal(orderCoin, users.seller, totalSize);
|
|
838
|
+
vm.startPrank(users.seller);
|
|
839
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
840
|
+
vm.stopPrank();
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
vm.recordLogs();
|
|
844
|
+
vm.prank(users.seller);
|
|
845
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
846
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
847
|
+
|
|
848
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
849
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
850
|
+
|
|
851
|
+
// Call fill with maxFillCount = 100 (way above default of 2)
|
|
852
|
+
vm.recordLogs();
|
|
853
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 100, address(0));
|
|
854
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
855
|
+
|
|
856
|
+
// Should cap to 2 orders (the configured max)
|
|
857
|
+
assertEq(fills.length, 2, "should cap to configured max of 2 orders");
|
|
858
|
+
}
|
|
859
|
+
|
|
817
860
|
/// @notice Tests batch fill with empty orderIds array (line 134)
|
|
818
861
|
/// @dev This tests the branch: if (batch.orderIds.length != 0)
|
|
819
862
|
function test_batchFill_emptyOrderIds_skips() public {
|
|
@@ -67,7 +67,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
67
67
|
assertEq(created.length, 3, "should create three orders");
|
|
68
68
|
|
|
69
69
|
// Verify tick queue linked list structure
|
|
70
|
-
|
|
70
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
71
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
71
72
|
assertEq(tickQueue.length, 3, "queue length should be 3");
|
|
72
73
|
assertEq(tickQueue.head, created[0].orderId, "head should be first order");
|
|
73
74
|
assertEq(tickQueue.tail, created[2].orderId, "tail should be last order");
|
|
@@ -108,7 +109,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
108
109
|
vm.prank(users.seller);
|
|
109
110
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
113
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
112
114
|
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
113
115
|
assertEq(tickQueue.head, created[1].orderId, "head should be second order");
|
|
114
116
|
assertEq(tickQueue.tail, created[2].orderId, "tail unchanged");
|
|
@@ -149,7 +151,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
149
151
|
vm.prank(users.seller);
|
|
150
152
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
155
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
153
156
|
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
154
157
|
assertEq(tickQueue.head, created[0].orderId, "head unchanged");
|
|
155
158
|
assertEq(tickQueue.tail, created[1].orderId, "tail should be second order");
|
|
@@ -190,7 +193,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
190
193
|
vm.prank(users.seller);
|
|
191
194
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
192
195
|
|
|
193
|
-
|
|
196
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
197
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
194
198
|
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
195
199
|
assertEq(tickQueue.head, created[0].orderId, "head unchanged");
|
|
196
200
|
assertEq(tickQueue.tail, created[2].orderId, "tail unchanged");
|
|
@@ -237,7 +241,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
237
241
|
|
|
238
242
|
// Verify bitmap is set
|
|
239
243
|
bytes32 poolKeyHash = keccak256(abi.encode(key));
|
|
240
|
-
|
|
244
|
+
int24 fillableTick = _fillableTick(isCurrency0, orderTicks[0], key.tickSpacing);
|
|
245
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be initialized");
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
function test_bitmapSetIfFirstWhenNonEmpty() public {
|
|
@@ -262,9 +267,10 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
262
267
|
|
|
263
268
|
// Verify bitmap is still set (second enqueue didn't break it)
|
|
264
269
|
bytes32 poolKeyHash = keccak256(abi.encode(key));
|
|
265
|
-
|
|
270
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
271
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be initialized");
|
|
266
272
|
|
|
267
|
-
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin,
|
|
273
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, fillableTick);
|
|
268
274
|
assertEq(tickQueue.length, 2, "should have 2 orders at same tick");
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -290,7 +296,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
290
296
|
vm.prank(users.seller);
|
|
291
297
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
292
298
|
|
|
293
|
-
|
|
299
|
+
int24 fillableTick = _fillableTick(isCurrency0, orderTicks[0], key.tickSpacing);
|
|
300
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be cleared");
|
|
294
301
|
}
|
|
295
302
|
|
|
296
303
|
function test_bitmapClearIfEmptyWhenStillHasOrders() public {
|
|
@@ -323,9 +330,10 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
323
330
|
vm.prank(users.seller);
|
|
324
331
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
325
332
|
|
|
326
|
-
|
|
333
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
334
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should still be initialized");
|
|
327
335
|
|
|
328
|
-
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin,
|
|
336
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, fillableTick);
|
|
329
337
|
assertEq(tickQueue.length, 1, "should have 1 order remaining");
|
|
330
338
|
}
|
|
331
339
|
|