@zoralabs/limit-orders 0.2.0
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 +85 -0
- package/AUDIT_NOTES.md +33 -0
- package/AUDIT_RFP.md +408 -0
- package/CHANGELOG.md +25 -0
- package/GAS_COMPARISON_RESULTS.md +194 -0
- package/LICENSE +21 -0
- package/README.md +650 -0
- package/SPEC.md +291 -0
- package/abis/BalanceDeltaLibrary.json +15 -0
- package/abis/BeforeSwapDeltaLibrary.json +15 -0
- package/abis/CurrencyLibrary.json +25 -0
- package/abis/CustomRevert.json +28 -0
- package/abis/IAllowanceTransfer.json +486 -0
- package/abis/IAuthority.json +31 -0
- package/abis/ICoin.json +1074 -0
- package/abis/IDeployedCoinVersionLookup.json +21 -0
- package/abis/IDopplerErrors.json +44 -0
- package/abis/IEIP712.json +15 -0
- package/abis/IERC1363.json +373 -0
- package/abis/IERC165.json +21 -0
- package/abis/IERC20.json +185 -0
- package/abis/IERC20Minimal.json +172 -0
- package/abis/IERC6909Claims.json +288 -0
- package/abis/IERC7572.json +21 -0
- package/abis/IExtsload.json +64 -0
- package/abis/IExttload.json +40 -0
- package/abis/IHasCoinType.json +15 -0
- package/abis/IHasPoolKey.json +42 -0
- package/abis/IHasRewardsRecipients.json +54 -0
- package/abis/IHasSwapPath.json +60 -0
- package/abis/IHasTotalSupplyForPositions.json +15 -0
- package/abis/IHooks.json +789 -0
- package/abis/IMsgSender.json +15 -0
- package/abis/IPoolManager.json +1286 -0
- package/abis/IProtocolFees.json +174 -0
- package/abis/ISupportsLimitOrderFill.json +15 -0
- package/abis/ISwapPathRouter.json +92 -0
- package/abis/ISwapRouter.json +219 -0
- package/abis/IUniswapV3SwapCallback.json +25 -0
- package/abis/IUpgradeableDestinationV4Hook.json +84 -0
- package/abis/IUpgradeableDestinationV4HookWithUpdateableFee.json +95 -0
- package/abis/IUpgradeableV4Hook.json +112 -0
- package/abis/IZoraHookRegistry.json +188 -0
- package/abis/IZoraLimitOrderBook.json +623 -0
- package/abis/IZoraLimitOrderBookCoinsInterface.json +67 -0
- package/abis/IZoraV4CoinHook.json +610 -0
- package/abis/Permit2Payments.json +7 -0
- package/abis/Position.json +7 -0
- package/abis/SafeCast.json +7 -0
- package/abis/SafeCast160.json +7 -0
- package/abis/SafeERC20.json +34 -0
- package/abis/SimpleAccessManaged.json +57 -0
- package/abis/SimpleAccessManager.json +351 -0
- package/abis/SqrtPriceMath.json +22 -0
- package/abis/StateLibrary.json +80 -0
- package/abis/SwapLimitOrders.json +22 -0
- package/abis/SwapWithLimitOrders.json +457 -0
- package/abis/TickBitmap.json +18 -0
- package/abis/TickMath.json +24 -0
- package/abis/V3ToV4SwapLib.json +28 -0
- package/abis/ZoraLimitOrderBook.json +771 -0
- package/cache/solidity-files-cache.json +1 -0
- package/dist/index.cjs +760 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +731 -0
- package/dist/index.js.map +1 -0
- package/dist/wagmiGenerated.d.ts +1012 -0
- package/dist/wagmiGenerated.d.ts.map +1 -0
- package/foundry.toml +29 -0
- package/gas_comparison.py +49 -0
- package/out/BalanceDelta.sol/BalanceDeltaLibrary.json +1 -0
- package/out/BeforeSwapDelta.sol/BeforeSwapDeltaLibrary.json +1 -0
- package/out/BitMath.sol/BitMath.json +1 -0
- package/out/BytesLib.sol/BytesLib.json +1 -0
- package/out/CoinCommon.sol/CoinCommon.json +1 -0
- package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -0
- package/out/CoinConstants.sol/CoinConstants.json +1 -0
- package/out/Context.sol/Context.json +1 -0
- package/out/Currency.sol/CurrencyLibrary.json +1 -0
- package/out/CurrencyReserves.sol/CurrencyReserves.json +1 -0
- package/out/CustomRevert.sol/CustomRevert.json +1 -0
- package/out/DopplerMath.sol/DopplerMath.json +1 -0
- package/out/FixedPoint128.sol/FixedPoint128.json +1 -0
- package/out/FixedPoint96.sol/FixedPoint96.json +1 -0
- package/out/FullMath.sol/FullMath.json +1 -0
- package/out/IAllowanceTransfer.sol/IAllowanceTransfer.json +1 -0
- package/out/IAuthority.sol/IAuthority.json +1 -0
- package/out/ICoin.sol/ICoin.json +1 -0
- package/out/ICoin.sol/IHasCoinType.json +1 -0
- package/out/ICoin.sol/IHasPoolKey.json +1 -0
- package/out/ICoin.sol/IHasSwapPath.json +1 -0
- package/out/ICoin.sol/IHasTotalSupplyForPositions.json +1 -0
- package/out/IDeployedCoinVersionLookup.sol/IDeployedCoinVersionLookup.json +1 -0
- package/out/IDopplerErrors.sol/IDopplerErrors.json +1 -0
- package/out/IEIP712.sol/IEIP712.json +1 -0
- package/out/IERC1363.sol/IERC1363.json +1 -0
- package/out/IERC165.sol/IERC165.json +1 -0
- package/out/IERC20.sol/IERC20.json +1 -0
- package/out/IERC20Minimal.sol/IERC20Minimal.json +1 -0
- package/out/IERC6909Claims.sol/IERC6909Claims.json +1 -0
- package/out/IERC7572.sol/IERC7572.json +1 -0
- package/out/IExtsload.sol/IExtsload.json +1 -0
- package/out/IExttload.sol/IExttload.json +1 -0
- package/out/IHasRewardsRecipients.sol/IHasRewardsRecipients.json +1 -0
- package/out/IHooks.sol/IHooks.json +1 -0
- package/out/IMsgSender.sol/IMsgSender.json +1 -0
- package/out/IPoolManager.sol/IPoolManager.json +1 -0
- package/out/IProtocolFees.sol/IProtocolFees.json +1 -0
- package/out/ISupportsLimitOrderFill.sol/ISupportsLimitOrderFill.json +1 -0
- package/out/ISwapPathRouter.sol/ISwapPathRouter.json +1 -0
- package/out/ISwapRouter.sol/ISwapRouter.json +1 -0
- package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -0
- package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4Hook.json +1 -0
- package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4HookWithUpdateableFee.json +1 -0
- package/out/IUpgradeableV4Hook.sol/IUpgradeableV4Hook.json +1 -0
- package/out/IZoraHookRegistry.sol/IZoraHookRegistry.json +1 -0
- package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -0
- package/out/IZoraLimitOrderBookCoinsInterface.sol/IZoraLimitOrderBookCoinsInterface.json +1 -0
- package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.json +1 -0
- package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -0
- package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -0
- package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -0
- package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -0
- package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -0
- package/out/LimitOrderQueues.sol/LimitOrderQueues.json +1 -0
- package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -0
- package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -0
- package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -0
- package/out/LiquidityAmounts.sol/LiquidityAmounts.json +1 -0
- package/out/LiquidityMath.sol/LiquidityMath.json +1 -0
- package/out/Lock.sol/Lock.json +1 -0
- package/out/NonzeroDeltaCount.sol/NonzeroDeltaCount.json +1 -0
- package/out/Path.sol/Path.json +1 -0
- package/out/PathKey.sol/PathKeyLibrary.json +1 -0
- package/out/Permit2Payments.sol/Permit2Payments.json +1 -0
- package/out/PoolId.sol/PoolIdLibrary.json +1 -0
- package/out/Position.sol/Position.json +1 -0
- package/out/SafeCast.sol/SafeCast.json +1 -0
- package/out/SafeCast160.sol/SafeCast160.json +1 -0
- package/out/SafeERC20.sol/SafeERC20.json +1 -0
- package/out/SimpleAccessManaged.sol/SimpleAccessManaged.json +1 -0
- package/out/SimpleAccessManager.sol/SimpleAccessManager.json +1 -0
- package/out/SqrtPriceMath.sol/SqrtPriceMath.json +1 -0
- package/out/StateLibrary.sol/StateLibrary.json +1 -0
- package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -0
- package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -0
- package/out/TickBitmap.sol/TickBitmap.json +1 -0
- package/out/TickMath.sol/TickMath.json +1 -0
- package/out/TransientSlot.sol/TransientSlot.json +1 -0
- package/out/TransientStateLibrary.sol/TransientStateLibrary.json +1 -0
- package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -0
- package/out/UnsafeMath.sol/UnsafeMath.json +1 -0
- package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -0
- package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -0
- package/out/build-info/69718f10d1dc37f0.json +1 -0
- package/out/uniswap/BitMath.sol/BitMath.json +1 -0
- package/out/uniswap/CustomRevert.sol/CustomRevert.json +1 -0
- package/out/uniswap/FullMath.sol/FullMath.json +1 -0
- package/out/uniswap/SafeCast.sol/SafeCast.json +1 -0
- package/out/uniswap/TickMath.sol/TickMath.json +1 -0
- package/package/index.ts +1 -0
- package/package/wagmiGenerated.ts +738 -0
- package/package.json +57 -0
- package/remappings.txt +11 -0
- package/src/IZoraLimitOrderBook.sol +195 -0
- package/src/ZoraLimitOrderBook.sol +220 -0
- package/src/access/SimpleAccessManaged.sol +76 -0
- package/src/access/SimpleAccessManager.sol +268 -0
- package/src/libs/LimitOrderBitmap.sol +84 -0
- package/src/libs/LimitOrderCommon.sol +91 -0
- package/src/libs/LimitOrderCreate.sol +277 -0
- package/src/libs/LimitOrderFill.sol +362 -0
- package/src/libs/LimitOrderLiquidity.sol +222 -0
- package/src/libs/LimitOrderQueues.sol +101 -0
- package/src/libs/LimitOrderStorage.sol +34 -0
- package/src/libs/LimitOrderTypes.sol +41 -0
- package/src/libs/LimitOrderWithdraw.sol +100 -0
- package/src/libs/Permit2Payments.sol +41 -0
- package/src/libs/SwapLimitOrders.sol +209 -0
- package/src/router/SwapWithLimitOrders.sol +454 -0
- package/test/LimitOrderAccessControl.t.sol +461 -0
- package/test/LimitOrderBitmap.t.sol +194 -0
- package/test/LimitOrderCreate.t.sol +348 -0
- package/test/LimitOrderFill.t.sol +1005 -0
- package/test/LimitOrderLibraries.t.sol +354 -0
- package/test/LimitOrderLiquidityPayouts.t.sol +333 -0
- package/test/LimitOrderV4Pools.t.sol +157 -0
- package/test/LimitOrderWithdraw.t.sol +653 -0
- package/test/SimpleAccessManager.t.sol +420 -0
- package/test/SwapWithLimitOrders.t.sol +107 -0
- package/test/SwapWithLimitOrdersRouter.t.sol +1073 -0
- package/test/gas/LimitOrderFillGas.t.sol +1008 -0
- package/test/gas/LimitOrderSwapGas.t.sol +403 -0
- package/test/gas/logs/gas_benchmarks_fill_20251201.log +30 -0
- package/test/gas/logs/gas_benchmarks_swap_20251201.log +27 -0
- package/test/unit/LimitOrderBitmapUnit.t.sol +276 -0
- package/test/unit/LimitOrderCreateUnit.t.sol +358 -0
- package/test/unit/SwapLimitOrdersUnit.t.sol +672 -0
- package/test/unit/SwapLimitOrdersValidation.t.sol +423 -0
- package/test/unit/SwapWithLimitOrdersUnit.t.sol +321 -0
- package/test/utils/BaseTest.sol +793 -0
- package/test/utils/TestableZoraLimitOrderBook.sol +54 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +11 -0
- package/wagmi.config.ts +18 -0
|
@@ -0,0 +1,209 @@
|
|
|
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 {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
11
|
+
import {TickMath} from "@zoralabs/coins/src/utils/uniswap/TickMath.sol";
|
|
12
|
+
import {FullMath} from "@zoralabs/coins/src/utils/uniswap/FullMath.sol";
|
|
13
|
+
import {DopplerMath} from "@zoralabs/coins/src/libs/DopplerMath.sol";
|
|
14
|
+
|
|
15
|
+
/// @dev Configuration for a limit order ladder
|
|
16
|
+
/// @param multiples Price multiples for each order (e.g., 2e18 = 2x current price)
|
|
17
|
+
/// @param percentages Percentage of total size for each order (basis points, must sum ≤ 10000)
|
|
18
|
+
struct LimitOrderConfig {
|
|
19
|
+
uint256[] multiples;
|
|
20
|
+
uint256[] percentages;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// @dev Computed limit orders ready for execution
|
|
24
|
+
/// @param sizes Amount of coins in each order
|
|
25
|
+
/// @param ticks Uniswap tick for each order
|
|
26
|
+
/// @param multiples Price multiple used for each order (tracks which config entry)
|
|
27
|
+
/// @param percentages Percentage used for each order (tracks which config entry)
|
|
28
|
+
struct Orders {
|
|
29
|
+
uint256[] sizes;
|
|
30
|
+
int24[] ticks;
|
|
31
|
+
uint256[] multiples;
|
|
32
|
+
uint256[] percentages;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// @title SwapLimitOrders
|
|
36
|
+
/// @notice Computes limit order ladders on coin swaps
|
|
37
|
+
library SwapLimitOrders {
|
|
38
|
+
/// @dev 1.0x price multiplier (e.g., 2e18 = 2x)
|
|
39
|
+
uint256 internal constant MULTIPLE_SCALE = 1e18;
|
|
40
|
+
|
|
41
|
+
/// @dev 100% in basis points (e.g., 5000 = 50%)
|
|
42
|
+
uint256 internal constant PERCENT_SCALE = 10_000;
|
|
43
|
+
|
|
44
|
+
/// @dev sqrt(1e18) - scales sqrt calculations without precision loss
|
|
45
|
+
uint256 internal constant SQRT_MULTIPLE_SCALE = 1e9;
|
|
46
|
+
|
|
47
|
+
/// @dev Minimum coins to create orders - prevents dust
|
|
48
|
+
uint256 internal constant MIN_LIMIT_ORDER_SIZE = 1e18;
|
|
49
|
+
|
|
50
|
+
/// @notice Multiples and percentages arrays have different lengths
|
|
51
|
+
error LengthMismatch();
|
|
52
|
+
|
|
53
|
+
/// @notice Percentages sum exceeds 100%
|
|
54
|
+
error PercentOverflow();
|
|
55
|
+
|
|
56
|
+
/// @notice A percentage is zero
|
|
57
|
+
error InvalidPercent();
|
|
58
|
+
|
|
59
|
+
/// @notice A multiple is ≤ 1.0x
|
|
60
|
+
error InvalidMultiple();
|
|
61
|
+
|
|
62
|
+
/// @notice Validates a limit order configuration
|
|
63
|
+
/// @param config The configuration to validate
|
|
64
|
+
/// @return totalPercent Sum of all percentages (for caller's use)
|
|
65
|
+
/// @dev Reverts if:
|
|
66
|
+
/// - Arrays are empty or mismatched length
|
|
67
|
+
/// - Any percentage is zero
|
|
68
|
+
/// - Any multiple is ≤ 1.0x
|
|
69
|
+
/// - Percentages sum > 100%
|
|
70
|
+
function validate(LimitOrderConfig memory config) internal pure returns (uint256 totalPercent) {
|
|
71
|
+
uint256 length = config.multiples.length;
|
|
72
|
+
|
|
73
|
+
require(length > 0 && length == config.percentages.length, LengthMismatch());
|
|
74
|
+
|
|
75
|
+
unchecked {
|
|
76
|
+
for (uint256 i; i < length; ++i) {
|
|
77
|
+
require(config.percentages[i] != 0, InvalidPercent());
|
|
78
|
+
require(config.multiples[i] > MULTIPLE_SCALE, InvalidMultiple());
|
|
79
|
+
totalPercent += config.percentages[i]; // Bounded by PERCENT_SCALE check below
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
require(totalPercent <= PERCENT_SCALE, PercentOverflow());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// @notice Computes limit order sizes and ticks from a configuration
|
|
87
|
+
/// @param key The Uniswap pool
|
|
88
|
+
/// @param isCurrency0 True if placing orders for currency0, false for currency1
|
|
89
|
+
/// @param totalSize Total coins to distribute across orders
|
|
90
|
+
/// @param baseTick Current pool tick (orders placed at least 1 tick spacing away)
|
|
91
|
+
/// @param sqrtPriceX96 Current pool sqrt price
|
|
92
|
+
/// @param config The limit order configuration
|
|
93
|
+
/// @return o Orders ready for creation (may have fewer entries than config if some rounded to zero)
|
|
94
|
+
/// @return allocated Amount of totalSize allocated to orders
|
|
95
|
+
/// @return unallocated Amount of totalSize not allocated (dust or partial fill)
|
|
96
|
+
/// @dev Orders are sized sequentially: each order takes its percentage of remaining balance.
|
|
97
|
+
/// Orders with zero size after rounding are skipped - arrays shrink to match.
|
|
98
|
+
/// Returns empty arrays if totalSize < MIN_LIMIT_ORDER_SIZE.
|
|
99
|
+
function computeOrders(
|
|
100
|
+
PoolKey memory key,
|
|
101
|
+
bool isCurrency0,
|
|
102
|
+
uint128 totalSize,
|
|
103
|
+
int24 baseTick,
|
|
104
|
+
uint160 sqrtPriceX96,
|
|
105
|
+
LimitOrderConfig memory config
|
|
106
|
+
) internal pure returns (Orders memory o, uint128 allocated, uint128 unallocated) {
|
|
107
|
+
if (totalSize < MIN_LIMIT_ORDER_SIZE) {
|
|
108
|
+
unallocated = uint128(totalSize);
|
|
109
|
+
return (o, allocated, unallocated);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
uint256 orderCount = config.multiples.length;
|
|
113
|
+
|
|
114
|
+
o.sizes = new uint256[](orderCount);
|
|
115
|
+
o.ticks = new int24[](orderCount);
|
|
116
|
+
o.multiples = new uint256[](orderCount);
|
|
117
|
+
o.percentages = new uint256[](orderCount);
|
|
118
|
+
|
|
119
|
+
uint128 remaining = totalSize;
|
|
120
|
+
uint256 count;
|
|
121
|
+
|
|
122
|
+
for (uint256 i; i < orderCount; ++i) {
|
|
123
|
+
uint256 orderSize = FullMath.mulDiv(uint256(remaining), config.percentages[i], PERCENT_SCALE);
|
|
124
|
+
if (orderSize == 0) continue;
|
|
125
|
+
|
|
126
|
+
allocated += uint128(orderSize);
|
|
127
|
+
remaining -= uint128(orderSize);
|
|
128
|
+
|
|
129
|
+
int24 targetTick = _tickForMultiple(key, isCurrency0, baseTick, sqrtPriceX96, config.multiples[i]);
|
|
130
|
+
|
|
131
|
+
o.sizes[count] = orderSize;
|
|
132
|
+
o.ticks[count] = targetTick;
|
|
133
|
+
o.multiples[count] = config.multiples[i];
|
|
134
|
+
o.percentages[count] = config.percentages[i];
|
|
135
|
+
|
|
136
|
+
unchecked {
|
|
137
|
+
++count;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
assembly ("memory-safe") {
|
|
142
|
+
// Shrink arrays in place so the caller only sees populated entries
|
|
143
|
+
mstore(mload(o), count)
|
|
144
|
+
mstore(mload(add(o, 0x20)), count)
|
|
145
|
+
mstore(mload(add(o, 0x40)), count)
|
|
146
|
+
mstore(mload(add(o, 0x60)), count)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
unallocated = remaining;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// @notice Converts a price multiple to a valid Uniswap tick
|
|
153
|
+
/// @param key The pool (for tick spacing and bounds)
|
|
154
|
+
/// @param isCurrency0 True for buys (tick > base), false for sells (tick < base)
|
|
155
|
+
/// @param baseTick Current pool tick
|
|
156
|
+
/// @param sqrtPriceX96 Current pool sqrt price
|
|
157
|
+
/// @param multiple Desired price multiple (e.g., 2e18 = 2x)
|
|
158
|
+
/// @return aligned Valid tick respecting spacing, bounds, and minimum separation from baseTick
|
|
159
|
+
function _tickForMultiple(
|
|
160
|
+
PoolKey memory key,
|
|
161
|
+
bool isCurrency0,
|
|
162
|
+
int24 baseTick,
|
|
163
|
+
uint160 sqrtPriceX96,
|
|
164
|
+
uint256 multiple
|
|
165
|
+
) private pure returns (int24 aligned) {
|
|
166
|
+
require(multiple > MULTIPLE_SCALE, InvalidMultiple());
|
|
167
|
+
|
|
168
|
+
uint256 sqrtMultiplier = _sqrtMultiple(multiple);
|
|
169
|
+
if (!isCurrency0) {
|
|
170
|
+
sqrtMultiplier = (SQRT_MULTIPLE_SCALE * SQRT_MULTIPLE_SCALE) / sqrtMultiplier;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
uint256 scaled = FullMath.mulDiv(uint256(sqrtPriceX96), sqrtMultiplier, SQRT_MULTIPLE_SCALE);
|
|
174
|
+
if (scaled > type(uint160).max) scaled = type(uint160).max;
|
|
175
|
+
|
|
176
|
+
int24 rawTick = TickMath.getTickAtSqrtPrice(uint160(scaled));
|
|
177
|
+
aligned = DopplerMath.alignTickToTickSpacing(isCurrency0, rawTick, key.tickSpacing);
|
|
178
|
+
|
|
179
|
+
int24 maxTick = TickMath.maxUsableTick(key.tickSpacing);
|
|
180
|
+
int24 minTick = -maxTick;
|
|
181
|
+
|
|
182
|
+
if (aligned > maxTick) {
|
|
183
|
+
aligned = maxTick;
|
|
184
|
+
} else if (aligned < minTick) {
|
|
185
|
+
aligned = minTick;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
int24 minAway = baseTick + (isCurrency0 ? key.tickSpacing : -key.tickSpacing);
|
|
189
|
+
if (isCurrency0) {
|
|
190
|
+
if (aligned < minAway) aligned = minAway;
|
|
191
|
+
} else {
|
|
192
|
+
if (aligned > minAway) aligned = minAway;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// @notice Computes square root of a 1e18-scaled value using Babylonian method
|
|
197
|
+
/// @param multiple Value to take sqrt of (e.g., 4e18 → 2e9)
|
|
198
|
+
/// @return result Square root with 1e9 scaling
|
|
199
|
+
/// @dev Uses iterative approximation: x_next = (x + multiple/x) / 2
|
|
200
|
+
function _sqrtMultiple(uint256 multiple) private pure returns (uint256 result) {
|
|
201
|
+
result = multiple;
|
|
202
|
+
uint256 x = (multiple + 1) >> 1;
|
|
203
|
+
while (x < result) {
|
|
204
|
+
result = x;
|
|
205
|
+
x = (multiple / x + x) >> 1;
|
|
206
|
+
}
|
|
207
|
+
require(result != 0, InvalidMultiple());
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
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 {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
13
|
+
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
14
|
+
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
15
|
+
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
16
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
17
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
18
|
+
|
|
19
|
+
import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
|
|
20
|
+
import {SwapLimitOrders, LimitOrderConfig, Orders} from "../libs/SwapLimitOrders.sol";
|
|
21
|
+
import {ISwapRouter} from "@zoralabs/shared-contracts/interfaces/uniswap/ISwapRouter.sol";
|
|
22
|
+
import {ISupportsLimitOrderFill} from "@zoralabs/coins/src/interfaces/ISupportsLimitOrderFill.sol";
|
|
23
|
+
import {IMsgSender} from "@zoralabs/coins/src/interfaces/IMsgSender.sol";
|
|
24
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
25
|
+
import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol";
|
|
26
|
+
import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
|
|
27
|
+
import {V3ToV4SwapLib} from "@zoralabs/coins/src/libs/V3ToV4SwapLib.sol";
|
|
28
|
+
import {SimpleAccessManaged} from "../access/SimpleAccessManaged.sol";
|
|
29
|
+
import {Permit2Payments} from "../libs/Permit2Payments.sol";
|
|
30
|
+
|
|
31
|
+
/// @title SwapWithLimitOrders
|
|
32
|
+
/// @notice Standalone router contract that executes swaps with automatic limit order placement and filling.
|
|
33
|
+
/// @dev This contract uses the poolManager unlock/callback pattern to execute swaps, place limit orders
|
|
34
|
+
/// based on the tick range crossed during the swap, and attempt to fill those orders in a single transaction.
|
|
35
|
+
/// Users call swapWithLimitOrders() directly, which triggers the unlock callback flow.
|
|
36
|
+
/// Uses Permit2 for token approvals, matching the universal-router pattern.
|
|
37
|
+
/// @author oveddan
|
|
38
|
+
contract SwapWithLimitOrders is IMsgSender, Permit2Payments {
|
|
39
|
+
using SafeERC20 for IERC20;
|
|
40
|
+
using BalanceDeltaLibrary for BalanceDelta;
|
|
41
|
+
using CurrencyLibrary for Currency;
|
|
42
|
+
using PoolIdLibrary for PoolKey;
|
|
43
|
+
using Path for bytes;
|
|
44
|
+
|
|
45
|
+
/// @notice The Uniswap V4 pool manager
|
|
46
|
+
IPoolManager public immutable poolManager;
|
|
47
|
+
|
|
48
|
+
/// @notice The limit order book contract
|
|
49
|
+
IZoraLimitOrderBook public immutable zoraLimitOrderBook;
|
|
50
|
+
|
|
51
|
+
/// @notice The Uniswap V3 swap router
|
|
52
|
+
ISwapRouter public immutable swapRouter;
|
|
53
|
+
|
|
54
|
+
/// @notice Canonical limit order configuration
|
|
55
|
+
LimitOrderConfig private _limitOrderConfig;
|
|
56
|
+
|
|
57
|
+
/// @notice Transient storage slot for tracking the current maker during swap execution
|
|
58
|
+
bytes32 private constant _MAKER_SLOT = keccak256("SwapWithLimitOrders.maker");
|
|
59
|
+
|
|
60
|
+
/// @notice Parameters for executing a swap with limit order placement
|
|
61
|
+
struct SwapWithLimitOrdersParams {
|
|
62
|
+
address recipient; // Who receives the swap output
|
|
63
|
+
LimitOrderConfig limitOrderConfig; // Limit order configuration
|
|
64
|
+
address inputCurrency; // Currency to use for swap (address(0) for ETH)
|
|
65
|
+
uint256 inputAmount; // Amount of input currency
|
|
66
|
+
bytes v3Route; // V3 route from input → backing currency (empty if not needed)
|
|
67
|
+
PoolKey[] v4Route; // V4 route including target pool as last element
|
|
68
|
+
uint256 minAmountOut; // Minimum amount of coins to receive from final swap
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// @notice Internal callback data passed to unlockCallback
|
|
72
|
+
struct CallbackData {
|
|
73
|
+
address recipient;
|
|
74
|
+
PoolKey[] v4Route; // Target pool is last element
|
|
75
|
+
uint256 currencyAmount; // Amount after V3 swap
|
|
76
|
+
address currencyReceived; // Currency received from V3 swap
|
|
77
|
+
uint256 minAmountOut;
|
|
78
|
+
LimitOrderConfig limitOrderConfig; // Limit order configuration for order creation
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// @notice Data returned from unlockCallback
|
|
82
|
+
struct UnlockResult {
|
|
83
|
+
uint256 coinAmount; // Amount of coins received
|
|
84
|
+
address coinAddress; // Address of the coin
|
|
85
|
+
bool isCoinCurrency0; // Whether coin is currency0 in target pool
|
|
86
|
+
int24 currentTick; // Tick after swaps
|
|
87
|
+
uint160 sqrtPriceX96; // Price after swaps
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// @notice Represents a limit order created with its configuration
|
|
91
|
+
struct CreatedOrder {
|
|
92
|
+
bytes32 orderId; // The order ID
|
|
93
|
+
uint256 multiple; // The price multiple used (e.g., 2e18 for 2x)
|
|
94
|
+
uint256 percentage; // The percentage of swap output allocated (basis points)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// @notice Emitted when a swap with limit order placement is executed
|
|
98
|
+
/// @param orders Array of created orders with their configuration. Only includes orders
|
|
99
|
+
/// that were actually created (skipped rungs due to rounding are omitted).
|
|
100
|
+
event SwapWithLimitOrdersExecuted(
|
|
101
|
+
address indexed sender,
|
|
102
|
+
address indexed recipient,
|
|
103
|
+
PoolKey poolKey,
|
|
104
|
+
BalanceDelta delta,
|
|
105
|
+
int24 tickBeforeSwap,
|
|
106
|
+
int24 tickAfterSwap,
|
|
107
|
+
CreatedOrder[] orders
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/// @notice Emitted when limit order config is updated
|
|
111
|
+
event LimitOrderConfigUpdated(uint256[] multiples, uint256[] percentages);
|
|
112
|
+
|
|
113
|
+
/// @notice Error thrown when caller is not the pool manager
|
|
114
|
+
error OnlyPoolManager();
|
|
115
|
+
|
|
116
|
+
/// @notice Error thrown when caller is not the authority
|
|
117
|
+
error OnlyAuthority();
|
|
118
|
+
|
|
119
|
+
/// @notice Error thrown when config does not match canonical config
|
|
120
|
+
error InvalidLimitOrderConfig();
|
|
121
|
+
|
|
122
|
+
/// @notice Error thrown when swap delta is zero
|
|
123
|
+
error ZeroSwapDelta();
|
|
124
|
+
|
|
125
|
+
/// @notice Error thrown when final swap output is below minimum
|
|
126
|
+
error InsufficientOutputAmount();
|
|
127
|
+
|
|
128
|
+
/// @notice Error thrown when v4Route is empty
|
|
129
|
+
error EmptyV4Route();
|
|
130
|
+
|
|
131
|
+
/// @notice Constructor
|
|
132
|
+
/// @param poolManager_ The Uniswap V4 pool manager
|
|
133
|
+
/// @param zoraLimitOrderBook_ The limit order book contract
|
|
134
|
+
/// @param swapRouter_ The Uniswap V3 swap router
|
|
135
|
+
/// @param permit2_ The Permit2 contract address (0x000000000022D473030F116dDEE9F6B43aC78BA3)
|
|
136
|
+
constructor(IPoolManager poolManager_, IZoraLimitOrderBook zoraLimitOrderBook_, ISwapRouter swapRouter_, address permit2_) Permit2Payments(permit2_) {
|
|
137
|
+
require(address(poolManager_) != address(0), "PoolManager cannot be zero");
|
|
138
|
+
require(address(zoraLimitOrderBook_) != address(0), "ZoraLimitOrderBook cannot be zero");
|
|
139
|
+
require(address(swapRouter_) != address(0), "SwapRouter cannot be zero");
|
|
140
|
+
require(permit2_ != address(0), "Permit2 cannot be zero");
|
|
141
|
+
poolManager = poolManager_;
|
|
142
|
+
zoraLimitOrderBook = zoraLimitOrderBook_;
|
|
143
|
+
swapRouter = swapRouter_;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// @inheritdoc IMsgSender
|
|
147
|
+
function msgSender() external view returns (address) {
|
|
148
|
+
TransientSlot.AddressSlot slot = TransientSlot.asAddress(_MAKER_SLOT);
|
|
149
|
+
return TransientSlot.tload(slot);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// @notice Sets the canonical limit order configuration
|
|
153
|
+
/// @dev Only callable by zoraLimitOrderBook.authority()
|
|
154
|
+
/// @param config The new limit order configuration
|
|
155
|
+
function setLimitOrderConfig(LimitOrderConfig memory config) external {
|
|
156
|
+
require(msg.sender == SimpleAccessManaged(address(zoraLimitOrderBook)).authority(), OnlyAuthority());
|
|
157
|
+
SwapLimitOrders.validate(config);
|
|
158
|
+
_limitOrderConfig = config;
|
|
159
|
+
emit LimitOrderConfigUpdated(config.multiples, config.percentages);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// @notice Returns the current limit order configuration
|
|
163
|
+
/// @return The current limit order configuration
|
|
164
|
+
function getLimitOrderConfig() external view returns (LimitOrderConfig memory) {
|
|
165
|
+
return _limitOrderConfig;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// @notice Executes a swap with automatic limit order placement and filling
|
|
169
|
+
/// @param params The swap and limit order parameters
|
|
170
|
+
/// @return delta The balance delta from the swap
|
|
171
|
+
function swapWithLimitOrders(SwapWithLimitOrdersParams calldata params) external payable returns (BalanceDelta delta) {
|
|
172
|
+
// Store recipient (maker) in transient storage for IMsgSender interface
|
|
173
|
+
TransientSlot.AddressSlot slot = TransientSlot.asAddress(_MAKER_SLOT);
|
|
174
|
+
TransientSlot.tstore(slot, params.recipient);
|
|
175
|
+
|
|
176
|
+
// Validate limit order parameters (signature, percentages, multiples, etc.)
|
|
177
|
+
SwapLimitOrders.validate(params.limitOrderConfig);
|
|
178
|
+
|
|
179
|
+
// Validate config matches canonical config
|
|
180
|
+
_validateConfigMatchesCurrent(params.limitOrderConfig);
|
|
181
|
+
|
|
182
|
+
// Require v4Route has at least one pool (the target)
|
|
183
|
+
require(params.v4Route.length > 0, EmptyV4Route());
|
|
184
|
+
|
|
185
|
+
// Validate routes
|
|
186
|
+
V3ToV4SwapLib.validateRoutes(params.v3Route, params.inputCurrency, params.v4Route);
|
|
187
|
+
|
|
188
|
+
// Validate and transfer input currency from msg.sender using Permit2
|
|
189
|
+
V3ToV4SwapLib.permit2TransferFrom(PERMIT2, params.inputCurrency, params.inputAmount, msg.sender, address(this), msg.value);
|
|
190
|
+
|
|
191
|
+
// Get target pool (last element in v4Route)
|
|
192
|
+
PoolKey memory targetPool = params.v4Route[params.v4Route.length - 1];
|
|
193
|
+
|
|
194
|
+
// Get tick before swap
|
|
195
|
+
(, int24 tickBeforeSwap, , ) = StateLibrary.getSlot0(poolManager, targetPool.toId());
|
|
196
|
+
|
|
197
|
+
// Execute V3 swap (inputCurrency -> backing currency)
|
|
198
|
+
(uint256 currencyAmount, address currencyReceived) = V3ToV4SwapLib.executeV3Swap(
|
|
199
|
+
swapRouter,
|
|
200
|
+
V3ToV4SwapLib.V3SwapParams({
|
|
201
|
+
v3Route: params.v3Route,
|
|
202
|
+
inputCurrency: params.inputCurrency,
|
|
203
|
+
inputAmount: params.inputAmount,
|
|
204
|
+
recipient: address(this)
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Prepare callback data for V4 swaps + limit order creation
|
|
209
|
+
CallbackData memory callbackData = CallbackData({
|
|
210
|
+
recipient: params.recipient,
|
|
211
|
+
v4Route: params.v4Route,
|
|
212
|
+
currencyAmount: currencyAmount,
|
|
213
|
+
currencyReceived: currencyReceived,
|
|
214
|
+
minAmountOut: params.minAmountOut,
|
|
215
|
+
limitOrderConfig: params.limitOrderConfig
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Execute V4 swaps + create orders via unlock callback
|
|
219
|
+
bytes memory result = poolManager.unlock(abi.encode(callbackData));
|
|
220
|
+
|
|
221
|
+
(CreatedOrder[] memory orders, bool isCoinCurrency0, int24 tickAfterSwap) = abi.decode(result, (CreatedOrder[], bool, int24));
|
|
222
|
+
|
|
223
|
+
// Check if hook supports limit order filling using ERC165
|
|
224
|
+
bool hookSupportsFill = IERC165(address(targetPool.hooks)).supportsInterface(type(ISupportsLimitOrderFill).interfaceId);
|
|
225
|
+
|
|
226
|
+
// Router-based filling for legacy hooks
|
|
227
|
+
if (!hookSupportsFill && orders.length > 0 && tickBeforeSwap != tickAfterSwap) {
|
|
228
|
+
_fillOrders(targetPool, !isCoinCurrency0, tickBeforeSwap, tickAfterSwap);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
emit SwapWithLimitOrdersExecuted(msg.sender, params.recipient, targetPool, BalanceDelta.wrap(0), tickBeforeSwap, tickAfterSwap, orders);
|
|
232
|
+
|
|
233
|
+
// Clear maker from transient storage
|
|
234
|
+
TransientSlot.tstore(slot, address(0));
|
|
235
|
+
|
|
236
|
+
return BalanceDelta.wrap(0);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// @notice Callback function called by the pool manager during unlock
|
|
240
|
+
/// @dev This function executes V4 swaps and settles coins to recipient
|
|
241
|
+
/// @param data Encoded CallbackData
|
|
242
|
+
/// @return Encoded UnlockResult containing coin amount and pool info
|
|
243
|
+
function unlockCallback(bytes calldata data) external returns (bytes memory) {
|
|
244
|
+
if (msg.sender != address(poolManager)) {
|
|
245
|
+
revert OnlyPoolManager();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
CallbackData memory callbackData = abi.decode(data, (CallbackData));
|
|
249
|
+
|
|
250
|
+
// Execute V4 multi-hop swap
|
|
251
|
+
V3ToV4SwapLib.V4SwapResult memory swapResult = _executeV4Swaps(callbackData);
|
|
252
|
+
|
|
253
|
+
// Get target pool and coin info
|
|
254
|
+
(PoolKey memory targetPool, bool isCoinCurrency0, address coinAddress) = _getTargetPoolInfo(callbackData.v4Route, swapResult.outputCurrency);
|
|
255
|
+
|
|
256
|
+
// Get current pool state after swap
|
|
257
|
+
(uint160 sqrtPriceX96, int24 currentTick) = _getPoolState(targetPool);
|
|
258
|
+
|
|
259
|
+
// Create limit orders
|
|
260
|
+
(CreatedOrder[] memory createdOrders, uint128 unallocated) = _createLimitOrders(
|
|
261
|
+
targetPool,
|
|
262
|
+
isCoinCurrency0,
|
|
263
|
+
coinAddress,
|
|
264
|
+
swapResult.outputAmount,
|
|
265
|
+
currentTick,
|
|
266
|
+
sqrtPriceX96,
|
|
267
|
+
callbackData.limitOrderConfig,
|
|
268
|
+
callbackData.recipient
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Settle currencies with pool manager
|
|
272
|
+
_settleCurrencies(callbackData.currencyReceived, callbackData.currencyAmount, coinAddress, unallocated, callbackData.recipient);
|
|
273
|
+
|
|
274
|
+
return abi.encode(createdOrders, isCoinCurrency0, currentTick);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// @notice Executes V4 multi-hop swaps and validates output
|
|
278
|
+
/// @param callbackData The callback data containing swap parameters
|
|
279
|
+
/// @return swapResult The result of the V4 swap containing output amount and currency
|
|
280
|
+
function _executeV4Swaps(CallbackData memory callbackData) internal returns (V3ToV4SwapLib.V4SwapResult memory swapResult) {
|
|
281
|
+
swapResult = V3ToV4SwapLib.executeV4MultiHopSwap(
|
|
282
|
+
poolManager,
|
|
283
|
+
V3ToV4SwapLib.V4SwapParams({
|
|
284
|
+
v4Route: callbackData.v4Route,
|
|
285
|
+
amountIn: callbackData.currencyAmount,
|
|
286
|
+
startingCurrency: Currency.wrap(callbackData.currencyReceived)
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Validate minimum output amount
|
|
291
|
+
require(swapResult.outputAmount >= callbackData.minAmountOut, InsufficientOutputAmount());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// @notice Gets target pool and coin information
|
|
295
|
+
/// @param v4Route The V4 route array
|
|
296
|
+
/// @param outputCurrency The output currency from swaps
|
|
297
|
+
/// @return targetPool The target pool (last pool in route)
|
|
298
|
+
/// @return isCoinCurrency0 Whether the coin is currency0 in the pool
|
|
299
|
+
/// @return coinAddress The address of the coin
|
|
300
|
+
function _getTargetPoolInfo(
|
|
301
|
+
PoolKey[] memory v4Route,
|
|
302
|
+
Currency outputCurrency
|
|
303
|
+
) internal pure returns (PoolKey memory targetPool, bool isCoinCurrency0, address coinAddress) {
|
|
304
|
+
uint256 targetPoolIndex = v4Route.length - 1;
|
|
305
|
+
targetPool = v4Route[targetPoolIndex];
|
|
306
|
+
coinAddress = Currency.unwrap(outputCurrency);
|
|
307
|
+
isCoinCurrency0 = Currency.unwrap(targetPool.currency0) == coinAddress;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// @notice Gets current tick and price from pool
|
|
311
|
+
/// @param targetPool The pool to query
|
|
312
|
+
/// @return sqrtPriceX96 The current sqrt price
|
|
313
|
+
/// @return tick The current tick
|
|
314
|
+
function _getPoolState(PoolKey memory targetPool) internal view returns (uint160 sqrtPriceX96, int24 tick) {
|
|
315
|
+
(sqrtPriceX96, tick, , ) = StateLibrary.getSlot0(poolManager, targetPool.toId());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// @notice Creates limit orders with metadata from swap output
|
|
319
|
+
/// @param targetPool The target pool for the orders
|
|
320
|
+
/// @param isCoinCurrency0 Whether the coin is currency0
|
|
321
|
+
/// @param coinAddress The address of the coin
|
|
322
|
+
/// @param coinAmount The amount of coins received from swap
|
|
323
|
+
/// @param currentTick The current tick after swap
|
|
324
|
+
/// @param sqrtPriceX96 The current sqrt price after swap
|
|
325
|
+
/// @param limitOrderConfig The limit order configuration
|
|
326
|
+
/// @param maker The maker address (order owner/recipient)
|
|
327
|
+
/// @return createdOrders Array of CreatedOrder structs with orderIds and config
|
|
328
|
+
/// @return unallocated The amount not allocated to orders (goes to maker)
|
|
329
|
+
function _createLimitOrders(
|
|
330
|
+
PoolKey memory targetPool,
|
|
331
|
+
bool isCoinCurrency0,
|
|
332
|
+
address coinAddress,
|
|
333
|
+
uint128 coinAmount,
|
|
334
|
+
int24 currentTick,
|
|
335
|
+
uint160 sqrtPriceX96,
|
|
336
|
+
LimitOrderConfig memory limitOrderConfig,
|
|
337
|
+
address maker
|
|
338
|
+
) internal returns (CreatedOrder[] memory createdOrders, uint128 unallocated) {
|
|
339
|
+
uint128 allocated;
|
|
340
|
+
Orders memory orders;
|
|
341
|
+
// Compute limit orders
|
|
342
|
+
(orders, allocated, unallocated) = SwapLimitOrders.computeOrders(targetPool, isCoinCurrency0, coinAmount, currentTick, sqrtPriceX96, limitOrderConfig);
|
|
343
|
+
|
|
344
|
+
// Create orders if there are any to create
|
|
345
|
+
if (orders.sizes.length > 0 && allocated > 0) {
|
|
346
|
+
// Take allocated coins from pool manager to this contract
|
|
347
|
+
poolManager.take(Currency.wrap(coinAddress), address(this), allocated);
|
|
348
|
+
|
|
349
|
+
// Set value for ETH transfers (0 for ERC20, allocated for ETH)
|
|
350
|
+
uint256 value = coinAddress != address(0) ? 0 : allocated;
|
|
351
|
+
|
|
352
|
+
// For ERC20, approve the order book to spend the coins
|
|
353
|
+
if (coinAddress != address(0)) {
|
|
354
|
+
IERC20(coinAddress).approve(address(zoraLimitOrderBook), allocated);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Create orders with prefunded path
|
|
358
|
+
bytes32[] memory orderIds = zoraLimitOrderBook.create{value: value}(targetPool, isCoinCurrency0, orders.sizes, orders.ticks, maker);
|
|
359
|
+
|
|
360
|
+
createdOrders = new CreatedOrder[](orderIds.length);
|
|
361
|
+
unchecked {
|
|
362
|
+
for (uint256 i; i < orderIds.length; ++i) {
|
|
363
|
+
createdOrders[i] = CreatedOrder({orderId: orderIds[i], multiple: orders.multiples[i], percentage: orders.percentages[i]});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
createdOrders = new CreatedOrder[](0);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// @notice Settles input currency and distributes output coins
|
|
372
|
+
/// @param inputCurrency The input currency address
|
|
373
|
+
/// @param inputAmount The input currency amount
|
|
374
|
+
/// @param coinAddress The coin address
|
|
375
|
+
/// @param unallocated The unallocated coin amount to send to maker
|
|
376
|
+
/// @param maker The maker address (buyer)
|
|
377
|
+
function _settleCurrencies(address inputCurrency, uint256 inputAmount, address coinAddress, uint128 unallocated, address maker) internal {
|
|
378
|
+
// Settle input currency with pool manager
|
|
379
|
+
_transferFundsToPoolManager(inputCurrency, inputAmount);
|
|
380
|
+
|
|
381
|
+
// Take unallocated coins to maker (buyer)
|
|
382
|
+
if (unallocated > 0) {
|
|
383
|
+
poolManager.take(Currency.wrap(coinAddress), maker, unallocated);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function _transferFundsToPoolManager(address token, uint256 amount) internal {
|
|
388
|
+
Currency currency = Currency.wrap(token);
|
|
389
|
+
// Settle input currency
|
|
390
|
+
// if erc20 currency, sync and transfer
|
|
391
|
+
if (!currency.isAddressZero()) {
|
|
392
|
+
poolManager.sync(currency);
|
|
393
|
+
|
|
394
|
+
// transfer with balance check
|
|
395
|
+
uint256 beforeBalance = currency.balanceOf(address(poolManager));
|
|
396
|
+
currency.transfer(address(poolManager), amount);
|
|
397
|
+
require(currency.balanceOf(address(poolManager)) == beforeBalance + amount, IZoraLimitOrderBook.InsufficientTransferFunds());
|
|
398
|
+
|
|
399
|
+
poolManager.settle();
|
|
400
|
+
} else {
|
|
401
|
+
poolManager.settle{value: amount}();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// @notice Fills limit orders within the tick range crossed by the swap
|
|
406
|
+
/// @param poolKey The pool key
|
|
407
|
+
/// @param isCurrency0 Whether to fill currency0 orders
|
|
408
|
+
/// @param tickBeforeSwap The tick before the swap
|
|
409
|
+
/// @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;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Call fill in locked mode - will trigger unlock/callback flow in ZoraLimitOrderBook
|
|
427
|
+
zoraLimitOrderBook.fill(poolKey, isCurrency0, startTick, endTick, 0, address(0));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// @notice Validates that the provided config matches the canonical config
|
|
431
|
+
/// @param config The config to validate
|
|
432
|
+
function _validateConfigMatchesCurrent(LimitOrderConfig memory config) internal view {
|
|
433
|
+
uint256 canonicalLength = _limitOrderConfig.multiples.length;
|
|
434
|
+
|
|
435
|
+
// If canonical config is uninitialized, skip validation
|
|
436
|
+
if (canonicalLength == 0) return;
|
|
437
|
+
|
|
438
|
+
// Check array lengths match
|
|
439
|
+
require(config.multiples.length == canonicalLength && config.percentages.length == canonicalLength, InvalidLimitOrderConfig());
|
|
440
|
+
|
|
441
|
+
// Validate all values in single loop
|
|
442
|
+
unchecked {
|
|
443
|
+
for (uint256 i; i < canonicalLength; ++i) {
|
|
444
|
+
require(
|
|
445
|
+
config.multiples[i] == _limitOrderConfig.multiples[i] && config.percentages[i] == _limitOrderConfig.percentages[i],
|
|
446
|
+
InvalidLimitOrderConfig()
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// @notice Allows contract to receive ETH
|
|
453
|
+
receive() external payable {}
|
|
454
|
+
}
|