@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.
- package/.turbo/turbo-build$colon$js.log +50 -49
- package/CHANGELOG.md +73 -0
- package/abis/ISetLimitOrderConfig.json +27 -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/{SimpleAccessManaged.json → Ownable.json} +29 -10
- package/abis/Ownable2Step.json +115 -0
- package/abis/PermittedCallers.json +181 -0
- package/abis/SwapWithLimitOrders.json +134 -14
- package/abis/ZoraLimitOrderBook.json +187 -35
- package/cache/solidity-files-cache.json +1 -1
- package/dist/index.cjs +219 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +219 -34
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +254 -41
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/out/BalanceDelta.sol/BalanceDeltaLibrary.json +1 -1
- package/out/BeforeSwapDelta.sol/BeforeSwapDeltaLibrary.json +1 -1
- package/out/BitMath.sol/BitMath.json +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/Context.sol/Context.json +1 -1
- package/out/Currency.sol/CurrencyLibrary.json +1 -1
- package/out/CurrencyReserves.sol/CurrencyReserves.json +1 -1
- package/out/CustomRevert.sol/CustomRevert.json +1 -1
- package/out/DopplerMath.sol/DopplerMath.json +1 -1
- package/out/FixedPoint128.sol/FixedPoint128.json +1 -1
- package/out/FixedPoint96.sol/FixedPoint96.json +1 -1
- package/out/FullMath.sol/FullMath.json +1 -1
- package/out/IAllowanceTransfer.sol/IAllowanceTransfer.json +1 -1
- package/out/ICoin.sol/ICoin.json +1 -1
- package/out/ICoin.sol/IHasCoinType.json +1 -1
- package/out/ICoin.sol/IHasPoolKey.json +1 -1
- package/out/ICoin.sol/IHasSwapPath.json +1 -1
- package/out/ICoin.sol/IHasTotalSupplyForPositions.json +1 -1
- package/out/IDeployedCoinVersionLookup.sol/IDeployedCoinVersionLookup.json +1 -1
- package/out/IDopplerErrors.sol/IDopplerErrors.json +1 -1
- package/out/IEIP712.sol/IEIP712.json +1 -1
- package/out/IERC1363.sol/IERC1363.json +1 -1
- package/out/IERC165.sol/IERC165.json +1 -1
- package/out/IERC20.sol/IERC20.json +1 -1
- package/out/IERC20Minimal.sol/IERC20Minimal.json +1 -1
- package/out/IERC6909Claims.sol/IERC6909Claims.json +1 -1
- package/out/IERC7572.sol/IERC7572.json +1 -1
- package/out/IExtsload.sol/IExtsload.json +1 -1
- package/out/IExttload.sol/IExttload.json +1 -1
- package/out/IHasRewardsRecipients.sol/IHasRewardsRecipients.json +1 -1
- package/out/IHooks.sol/IHooks.json +1 -1
- package/out/IMsgSender.sol/IMsgSender.json +1 -1
- package/out/IPoolManager.sol/IPoolManager.json +1 -1
- package/out/IProtocolFees.sol/IProtocolFees.json +1 -1
- package/out/ISetLimitOrderConfig.sol/ISetLimitOrderConfig.json +1 -0
- package/out/ISupportsLimitOrderFill.sol/ISupportsLimitOrderFill.json +1 -1
- package/out/ISwapPathRouter.sol/ISwapPathRouter.json +1 -1
- package/out/ISwapRouter.sol/ISwapRouter.json +1 -1
- package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -1
- package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4Hook.json +1 -1
- package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4HookWithUpdateableFee.json +1 -1
- package/out/IUpgradeableV4Hook.sol/IUpgradeableV4Hook.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/LiquidityMath.sol/LiquidityMath.json +1 -1
- package/out/Lock.sol/Lock.json +1 -1
- package/out/NonzeroDeltaCount.sol/NonzeroDeltaCount.json +1 -1
- package/out/Ownable.sol/Ownable.json +1 -0
- package/out/Ownable2Step.sol/Ownable2Step.json +1 -0
- package/out/Path.sol/Path.json +1 -1
- package/out/PathKey.sol/PathKeyLibrary.json +1 -1
- package/out/Permit2Payments.sol/Permit2Payments.json +1 -1
- package/out/PermittedCallers.sol/PermittedCallers.json +1 -0
- package/out/PoolId.sol/PoolIdLibrary.json +1 -1
- package/out/Position.sol/Position.json +1 -1
- package/out/SafeCast.sol/SafeCast.json +1 -1
- package/out/SafeCast160.sol/SafeCast160.json +1 -1
- package/out/SafeERC20.sol/SafeERC20.json +1 -1
- package/out/SqrtPriceMath.sol/SqrtPriceMath.json +1 -1
- package/out/StateLibrary.sol/StateLibrary.json +1 -1
- package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
- package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
- package/out/TickBitmap.sol/TickBitmap.json +1 -1
- package/out/TickMath.sol/TickMath.json +1 -1
- package/out/TransientSlot.sol/TransientSlot.json +1 -1
- package/out/TransientStateLibrary.sol/TransientStateLibrary.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/37e0124d88d60569.json +1 -0
- 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 +218 -33
- package/package.json +1 -1
- package/src/IZoraLimitOrderBook.sol +5 -5
- package/src/ZoraLimitOrderBook.sol +24 -41
- package/src/access/PermittedCallers.sol +41 -0
- 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/ISetLimitOrderConfig.sol +12 -0
- package/src/router/SwapWithLimitOrders.sol +46 -33
- package/test/LimitOrderAccessControl.t.sol +173 -156
- package/test/LimitOrderBitmap.t.sol +13 -7
- package/test/LimitOrderFill.t.sol +42 -4
- 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 -5
- package/test/SwapWithLimitOrdersRouter.t.sol +108 -13
- package/test/gas/LimitOrderFillGas.t.sol +0 -7
- package/test/gas/LimitOrderSwapGas.t.sol +0 -6
- 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 +28 -42
- package/test/unit/SwapWithLimitOrdersUnit.t.sol +21 -88
- package/test/utils/BaseTest.sol +34 -22
- package/test/utils/MockWETH.sol +39 -0
- package/test/utils/TestableZoraLimitOrderBook.sol +5 -7
- package/abis/IAuthority.json +0 -31
- package/abis/SimpleAccessManager.json +0 -351
- package/out/IAuthority.sol/IAuthority.json +0 -1
- package/out/SimpleAccessManaged.sol/SimpleAccessManaged.json +0 -1
- package/out/SimpleAccessManager.sol/SimpleAccessManager.json +0 -1
- package/out/build-info/69718f10d1dc37f0.json +0 -1
- package/src/access/SimpleAccessManaged.sol +0 -76
- package/src/access/SimpleAccessManager.sol +0 -268
- 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
|
|
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 {
|
|
@@ -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 {
|
|
29
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 &&
|
|
228
|
-
_fillOrders(targetPool,
|
|
238
|
+
if (!hookSupportsFill && tickBeforeSwap != tickAfterSwap) {
|
|
239
|
+
_fillOrders(targetPool, isCoinCurrency0, tickBeforeSwap, tickAfterSwap);
|
|
229
240
|
}
|
|
230
241
|
|
|
231
|
-
emit SwapWithLimitOrdersExecuted(
|
|
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
|
|
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;
|
|
432
|
+
function _fillOrders(PoolKey memory poolKey, bool, int24 tickBeforeSwap, int24 tickAfterSwap) internal {
|
|
433
|
+
if (tickAfterSwap == tickBeforeSwap) {
|
|
434
|
+
return;
|
|
424
435
|
}
|
|
425
436
|
|
|
426
|
-
//
|
|
427
|
-
|
|
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
|