@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.
Files changed (208) hide show
  1. package/.turbo/turbo-build$colon$js.log +85 -0
  2. package/AUDIT_NOTES.md +33 -0
  3. package/AUDIT_RFP.md +408 -0
  4. package/CHANGELOG.md +25 -0
  5. package/GAS_COMPARISON_RESULTS.md +194 -0
  6. package/LICENSE +21 -0
  7. package/README.md +650 -0
  8. package/SPEC.md +291 -0
  9. package/abis/BalanceDeltaLibrary.json +15 -0
  10. package/abis/BeforeSwapDeltaLibrary.json +15 -0
  11. package/abis/CurrencyLibrary.json +25 -0
  12. package/abis/CustomRevert.json +28 -0
  13. package/abis/IAllowanceTransfer.json +486 -0
  14. package/abis/IAuthority.json +31 -0
  15. package/abis/ICoin.json +1074 -0
  16. package/abis/IDeployedCoinVersionLookup.json +21 -0
  17. package/abis/IDopplerErrors.json +44 -0
  18. package/abis/IEIP712.json +15 -0
  19. package/abis/IERC1363.json +373 -0
  20. package/abis/IERC165.json +21 -0
  21. package/abis/IERC20.json +185 -0
  22. package/abis/IERC20Minimal.json +172 -0
  23. package/abis/IERC6909Claims.json +288 -0
  24. package/abis/IERC7572.json +21 -0
  25. package/abis/IExtsload.json +64 -0
  26. package/abis/IExttload.json +40 -0
  27. package/abis/IHasCoinType.json +15 -0
  28. package/abis/IHasPoolKey.json +42 -0
  29. package/abis/IHasRewardsRecipients.json +54 -0
  30. package/abis/IHasSwapPath.json +60 -0
  31. package/abis/IHasTotalSupplyForPositions.json +15 -0
  32. package/abis/IHooks.json +789 -0
  33. package/abis/IMsgSender.json +15 -0
  34. package/abis/IPoolManager.json +1286 -0
  35. package/abis/IProtocolFees.json +174 -0
  36. package/abis/ISupportsLimitOrderFill.json +15 -0
  37. package/abis/ISwapPathRouter.json +92 -0
  38. package/abis/ISwapRouter.json +219 -0
  39. package/abis/IUniswapV3SwapCallback.json +25 -0
  40. package/abis/IUpgradeableDestinationV4Hook.json +84 -0
  41. package/abis/IUpgradeableDestinationV4HookWithUpdateableFee.json +95 -0
  42. package/abis/IUpgradeableV4Hook.json +112 -0
  43. package/abis/IZoraHookRegistry.json +188 -0
  44. package/abis/IZoraLimitOrderBook.json +623 -0
  45. package/abis/IZoraLimitOrderBookCoinsInterface.json +67 -0
  46. package/abis/IZoraV4CoinHook.json +610 -0
  47. package/abis/Permit2Payments.json +7 -0
  48. package/abis/Position.json +7 -0
  49. package/abis/SafeCast.json +7 -0
  50. package/abis/SafeCast160.json +7 -0
  51. package/abis/SafeERC20.json +34 -0
  52. package/abis/SimpleAccessManaged.json +57 -0
  53. package/abis/SimpleAccessManager.json +351 -0
  54. package/abis/SqrtPriceMath.json +22 -0
  55. package/abis/StateLibrary.json +80 -0
  56. package/abis/SwapLimitOrders.json +22 -0
  57. package/abis/SwapWithLimitOrders.json +457 -0
  58. package/abis/TickBitmap.json +18 -0
  59. package/abis/TickMath.json +24 -0
  60. package/abis/V3ToV4SwapLib.json +28 -0
  61. package/abis/ZoraLimitOrderBook.json +771 -0
  62. package/cache/solidity-files-cache.json +1 -0
  63. package/dist/index.cjs +760 -0
  64. package/dist/index.cjs.map +1 -0
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +731 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/wagmiGenerated.d.ts +1012 -0
  70. package/dist/wagmiGenerated.d.ts.map +1 -0
  71. package/foundry.toml +29 -0
  72. package/gas_comparison.py +49 -0
  73. package/out/BalanceDelta.sol/BalanceDeltaLibrary.json +1 -0
  74. package/out/BeforeSwapDelta.sol/BeforeSwapDeltaLibrary.json +1 -0
  75. package/out/BitMath.sol/BitMath.json +1 -0
  76. package/out/BytesLib.sol/BytesLib.json +1 -0
  77. package/out/CoinCommon.sol/CoinCommon.json +1 -0
  78. package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -0
  79. package/out/CoinConstants.sol/CoinConstants.json +1 -0
  80. package/out/Context.sol/Context.json +1 -0
  81. package/out/Currency.sol/CurrencyLibrary.json +1 -0
  82. package/out/CurrencyReserves.sol/CurrencyReserves.json +1 -0
  83. package/out/CustomRevert.sol/CustomRevert.json +1 -0
  84. package/out/DopplerMath.sol/DopplerMath.json +1 -0
  85. package/out/FixedPoint128.sol/FixedPoint128.json +1 -0
  86. package/out/FixedPoint96.sol/FixedPoint96.json +1 -0
  87. package/out/FullMath.sol/FullMath.json +1 -0
  88. package/out/IAllowanceTransfer.sol/IAllowanceTransfer.json +1 -0
  89. package/out/IAuthority.sol/IAuthority.json +1 -0
  90. package/out/ICoin.sol/ICoin.json +1 -0
  91. package/out/ICoin.sol/IHasCoinType.json +1 -0
  92. package/out/ICoin.sol/IHasPoolKey.json +1 -0
  93. package/out/ICoin.sol/IHasSwapPath.json +1 -0
  94. package/out/ICoin.sol/IHasTotalSupplyForPositions.json +1 -0
  95. package/out/IDeployedCoinVersionLookup.sol/IDeployedCoinVersionLookup.json +1 -0
  96. package/out/IDopplerErrors.sol/IDopplerErrors.json +1 -0
  97. package/out/IEIP712.sol/IEIP712.json +1 -0
  98. package/out/IERC1363.sol/IERC1363.json +1 -0
  99. package/out/IERC165.sol/IERC165.json +1 -0
  100. package/out/IERC20.sol/IERC20.json +1 -0
  101. package/out/IERC20Minimal.sol/IERC20Minimal.json +1 -0
  102. package/out/IERC6909Claims.sol/IERC6909Claims.json +1 -0
  103. package/out/IERC7572.sol/IERC7572.json +1 -0
  104. package/out/IExtsload.sol/IExtsload.json +1 -0
  105. package/out/IExttload.sol/IExttload.json +1 -0
  106. package/out/IHasRewardsRecipients.sol/IHasRewardsRecipients.json +1 -0
  107. package/out/IHooks.sol/IHooks.json +1 -0
  108. package/out/IMsgSender.sol/IMsgSender.json +1 -0
  109. package/out/IPoolManager.sol/IPoolManager.json +1 -0
  110. package/out/IProtocolFees.sol/IProtocolFees.json +1 -0
  111. package/out/ISupportsLimitOrderFill.sol/ISupportsLimitOrderFill.json +1 -0
  112. package/out/ISwapPathRouter.sol/ISwapPathRouter.json +1 -0
  113. package/out/ISwapRouter.sol/ISwapRouter.json +1 -0
  114. package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -0
  115. package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4Hook.json +1 -0
  116. package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4HookWithUpdateableFee.json +1 -0
  117. package/out/IUpgradeableV4Hook.sol/IUpgradeableV4Hook.json +1 -0
  118. package/out/IZoraHookRegistry.sol/IZoraHookRegistry.json +1 -0
  119. package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -0
  120. package/out/IZoraLimitOrderBookCoinsInterface.sol/IZoraLimitOrderBookCoinsInterface.json +1 -0
  121. package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.json +1 -0
  122. package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -0
  123. package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -0
  124. package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -0
  125. package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -0
  126. package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -0
  127. package/out/LimitOrderQueues.sol/LimitOrderQueues.json +1 -0
  128. package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -0
  129. package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -0
  130. package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -0
  131. package/out/LiquidityAmounts.sol/LiquidityAmounts.json +1 -0
  132. package/out/LiquidityMath.sol/LiquidityMath.json +1 -0
  133. package/out/Lock.sol/Lock.json +1 -0
  134. package/out/NonzeroDeltaCount.sol/NonzeroDeltaCount.json +1 -0
  135. package/out/Path.sol/Path.json +1 -0
  136. package/out/PathKey.sol/PathKeyLibrary.json +1 -0
  137. package/out/Permit2Payments.sol/Permit2Payments.json +1 -0
  138. package/out/PoolId.sol/PoolIdLibrary.json +1 -0
  139. package/out/Position.sol/Position.json +1 -0
  140. package/out/SafeCast.sol/SafeCast.json +1 -0
  141. package/out/SafeCast160.sol/SafeCast160.json +1 -0
  142. package/out/SafeERC20.sol/SafeERC20.json +1 -0
  143. package/out/SimpleAccessManaged.sol/SimpleAccessManaged.json +1 -0
  144. package/out/SimpleAccessManager.sol/SimpleAccessManager.json +1 -0
  145. package/out/SqrtPriceMath.sol/SqrtPriceMath.json +1 -0
  146. package/out/StateLibrary.sol/StateLibrary.json +1 -0
  147. package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -0
  148. package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -0
  149. package/out/TickBitmap.sol/TickBitmap.json +1 -0
  150. package/out/TickMath.sol/TickMath.json +1 -0
  151. package/out/TransientSlot.sol/TransientSlot.json +1 -0
  152. package/out/TransientStateLibrary.sol/TransientStateLibrary.json +1 -0
  153. package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -0
  154. package/out/UnsafeMath.sol/UnsafeMath.json +1 -0
  155. package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -0
  156. package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -0
  157. package/out/build-info/69718f10d1dc37f0.json +1 -0
  158. package/out/uniswap/BitMath.sol/BitMath.json +1 -0
  159. package/out/uniswap/CustomRevert.sol/CustomRevert.json +1 -0
  160. package/out/uniswap/FullMath.sol/FullMath.json +1 -0
  161. package/out/uniswap/SafeCast.sol/SafeCast.json +1 -0
  162. package/out/uniswap/TickMath.sol/TickMath.json +1 -0
  163. package/package/index.ts +1 -0
  164. package/package/wagmiGenerated.ts +738 -0
  165. package/package.json +57 -0
  166. package/remappings.txt +11 -0
  167. package/src/IZoraLimitOrderBook.sol +195 -0
  168. package/src/ZoraLimitOrderBook.sol +220 -0
  169. package/src/access/SimpleAccessManaged.sol +76 -0
  170. package/src/access/SimpleAccessManager.sol +268 -0
  171. package/src/libs/LimitOrderBitmap.sol +84 -0
  172. package/src/libs/LimitOrderCommon.sol +91 -0
  173. package/src/libs/LimitOrderCreate.sol +277 -0
  174. package/src/libs/LimitOrderFill.sol +362 -0
  175. package/src/libs/LimitOrderLiquidity.sol +222 -0
  176. package/src/libs/LimitOrderQueues.sol +101 -0
  177. package/src/libs/LimitOrderStorage.sol +34 -0
  178. package/src/libs/LimitOrderTypes.sol +41 -0
  179. package/src/libs/LimitOrderWithdraw.sol +100 -0
  180. package/src/libs/Permit2Payments.sol +41 -0
  181. package/src/libs/SwapLimitOrders.sol +209 -0
  182. package/src/router/SwapWithLimitOrders.sol +454 -0
  183. package/test/LimitOrderAccessControl.t.sol +461 -0
  184. package/test/LimitOrderBitmap.t.sol +194 -0
  185. package/test/LimitOrderCreate.t.sol +348 -0
  186. package/test/LimitOrderFill.t.sol +1005 -0
  187. package/test/LimitOrderLibraries.t.sol +354 -0
  188. package/test/LimitOrderLiquidityPayouts.t.sol +333 -0
  189. package/test/LimitOrderV4Pools.t.sol +157 -0
  190. package/test/LimitOrderWithdraw.t.sol +653 -0
  191. package/test/SimpleAccessManager.t.sol +420 -0
  192. package/test/SwapWithLimitOrders.t.sol +107 -0
  193. package/test/SwapWithLimitOrdersRouter.t.sol +1073 -0
  194. package/test/gas/LimitOrderFillGas.t.sol +1008 -0
  195. package/test/gas/LimitOrderSwapGas.t.sol +403 -0
  196. package/test/gas/logs/gas_benchmarks_fill_20251201.log +30 -0
  197. package/test/gas/logs/gas_benchmarks_swap_20251201.log +27 -0
  198. package/test/unit/LimitOrderBitmapUnit.t.sol +276 -0
  199. package/test/unit/LimitOrderCreateUnit.t.sol +358 -0
  200. package/test/unit/SwapLimitOrdersUnit.t.sol +672 -0
  201. package/test/unit/SwapLimitOrdersValidation.t.sol +423 -0
  202. package/test/unit/SwapWithLimitOrdersUnit.t.sol +321 -0
  203. package/test/utils/BaseTest.sol +793 -0
  204. package/test/utils/TestableZoraLimitOrderBook.sol +54 -0
  205. package/tsconfig.build.json +10 -0
  206. package/tsconfig.json +9 -0
  207. package/tsup.config.ts +11 -0
  208. package/wagmi.config.ts +18 -0
@@ -0,0 +1,348 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
+
6
+ import {IZoraLimitOrderBook} from "../src/IZoraLimitOrderBook.sol";
7
+ import {CoinCommon} from "@zoralabs/coins/src/libs/CoinCommon.sol";
8
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
9
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
10
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
+ import {Vm} from "forge-std/Vm.sol";
12
+
13
+ contract LimitOrderCreateTest is BaseTest {
14
+ function test_create_prefundedViaAutosellCreatesOrders() public {
15
+ PoolKey memory key = creatorCoin.getPoolKey();
16
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
17
+ address orderCoin = _orderCoin(key, isCurrency0);
18
+
19
+ vm.recordLogs();
20
+ _executeSingleHopSwapWithLimitOrders(users.buyer, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
21
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
22
+
23
+ assertGt(created.length, 0, "expected limit orders orders");
24
+ for (uint256 i; i < created.length; ++i) {
25
+ assertEq(created[i].maker, users.buyer, "maker mismatch");
26
+ assertEq(created[i].coin, orderCoin, "coin mismatch");
27
+ }
28
+
29
+ uint256 realized = _sumOrderSizes(created);
30
+ assertEq(_makerBalance(users.buyer, orderCoin), realized, "maker balance mismatch");
31
+ _assertOpenOrderState(users.buyer, orderCoin, created[0].poolKeyHash, created, key.tickSpacing);
32
+ }
33
+
34
+ function test_create_pullsErc20FundsForExternalMaker() public {
35
+ PoolKey memory key = creatorCoin.getPoolKey();
36
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
37
+ address orderCoin = _orderCoin(key, isCurrency0);
38
+
39
+ uint256[] memory orderSizes = new uint256[](2);
40
+ orderSizes[0] = 40e18;
41
+ orderSizes[1] = 25e18;
42
+ uint256 totalSize = orderSizes[0] + orderSizes[1];
43
+
44
+ int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
45
+ int24[] memory orderTicks = new int24[](2);
46
+ orderTicks[0] = isCurrency0 ? baseTick + key.tickSpacing : baseTick - key.tickSpacing;
47
+ orderTicks[1] = isCurrency0 ? orderTicks[0] + key.tickSpacing : orderTicks[0] - key.tickSpacing;
48
+
49
+ if (orderCoin == address(0)) {
50
+ vm.deal(users.seller, totalSize);
51
+ } else {
52
+ deal(orderCoin, users.seller, totalSize);
53
+ vm.startPrank(users.seller);
54
+ IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
55
+ vm.stopPrank();
56
+ }
57
+
58
+ vm.recordLogs();
59
+ vm.prank(users.seller);
60
+ bytes32[] memory orderIds = limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(
61
+ key,
62
+ isCurrency0,
63
+ orderSizes,
64
+ orderTicks,
65
+ users.seller
66
+ );
67
+
68
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
69
+ assertEq(created.length, orderIds.length, "mismatch between emitted and returned orders");
70
+
71
+ uint256 realized = _sumOrderSizes(created);
72
+ assertApproxEqAbs(realized, totalSize, 1, "realized size drift");
73
+ assertEq(_makerBalance(users.seller, orderCoin), realized, "maker balance mismatch");
74
+ _assertOpenOrderState(users.seller, orderCoin, CoinCommon.hashPoolKey(key), created, key.tickSpacing);
75
+ }
76
+
77
+ function test_create_refundsResidualAndIncrementsNonce() public {
78
+ PoolKey memory key = creatorCoin.getPoolKey();
79
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
80
+ address orderCoin = _orderCoin(key, isCurrency0);
81
+
82
+ uint256[] memory orderSizes = new uint256[](3);
83
+ orderSizes[0] = 123456789123456789;
84
+ orderSizes[1] = 987654321987654321;
85
+ orderSizes[2] = 222222222222222222;
86
+
87
+ int24[] memory orderTicks = new int24[](3);
88
+ int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
89
+ for (uint256 i; i < orderTicks.length; ++i) {
90
+ orderTicks[i] = _alignedTickForOrder(isCurrency0, baseTick, key.tickSpacing, i);
91
+ }
92
+
93
+ uint256 totalSize = orderSizes[0] + orderSizes[1] + orderSizes[2];
94
+ deal(orderCoin, users.seller, totalSize);
95
+
96
+ vm.startPrank(users.seller);
97
+ IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
98
+ vm.stopPrank();
99
+
100
+ uint256 nonceBefore = _makerNonce(users.seller);
101
+ uint256 tokenBalanceBefore = IERC20(orderCoin).balanceOf(users.seller);
102
+
103
+ vm.recordLogs();
104
+ vm.prank(users.seller);
105
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, users.seller);
106
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
107
+ assertEq(created.length, orderSizes.length, "unexpected created order count");
108
+
109
+ uint256 realized = _sumOrderSizes(created);
110
+ uint256 residual = totalSize - realized;
111
+ assertGt(residual, 0, "expected residual refund");
112
+
113
+ assertEq(_makerBalance(users.seller, orderCoin), realized, "maker balance mismatch");
114
+ assertEq(IERC20(orderCoin).balanceOf(users.seller), tokenBalanceBefore - totalSize + residual, "maker residual mismatch");
115
+ assertEq(_makerNonce(users.seller), nonceBefore + orderSizes.length, "maker nonce mismatch");
116
+ }
117
+
118
+ function test_create_revertsWhenRealizedZero() public {
119
+ PoolKey memory key = creatorCoin.getPoolKey();
120
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
121
+ address orderCoin = _orderCoin(key, isCurrency0);
122
+
123
+ uint256[] memory orderSizes = new uint256[](1);
124
+ orderSizes[0] = 1;
125
+
126
+ int24[] memory orderTicks = new int24[](1);
127
+ orderTicks[0] = _alignedTick(_currentTick(key), key.tickSpacing);
128
+
129
+ deal(orderCoin, users.seller, orderSizes[0]);
130
+ vm.startPrank(users.seller);
131
+ IERC20(orderCoin).approve(address(limitOrderBook), orderSizes[0]);
132
+ vm.stopPrank();
133
+
134
+ vm.expectRevert(IZoraLimitOrderBook.ZeroRealizedOrder.selector);
135
+ vm.prank(users.seller);
136
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, users.seller);
137
+ }
138
+
139
+ function test_create_revertsOnZeroOrderSize() public {
140
+ PoolKey memory key = creatorCoin.getPoolKey();
141
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
142
+
143
+ uint256[] memory orderSizes = new uint256[](1);
144
+ orderSizes[0] = 0;
145
+
146
+ int24[] memory orderTicks = new int24[](1);
147
+ orderTicks[0] = _alignedTick(_currentTick(key), key.tickSpacing);
148
+
149
+ vm.expectRevert(IZoraLimitOrderBook.ZeroOrderSize.selector);
150
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, users.seller);
151
+ }
152
+
153
+ function test_create_revertsOnArrayLengthMismatch() public {
154
+ PoolKey memory key = creatorCoin.getPoolKey();
155
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
156
+
157
+ uint256[] memory orderSizes = new uint256[](2);
158
+ orderSizes[0] = 1 ether;
159
+ orderSizes[1] = 1 ether;
160
+
161
+ int24[] memory orderTicks = new int24[](1);
162
+ orderTicks[0] = _alignedTick(_currentTick(key), key.tickSpacing);
163
+
164
+ vm.expectRevert(IZoraLimitOrderBook.ArrayLengthMismatch.selector);
165
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, users.seller);
166
+ }
167
+
168
+ function test_create_revertsOnZeroMaker() public {
169
+ PoolKey memory key = creatorCoin.getPoolKey();
170
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
171
+
172
+ uint256[] memory orderSizes = new uint256[](1);
173
+ orderSizes[0] = 1 ether;
174
+
175
+ int24[] memory orderTicks = new int24[](1);
176
+ orderTicks[0] = _alignedTick(_currentTick(key), key.tickSpacing);
177
+
178
+ vm.expectRevert(IZoraLimitOrderBook.ZeroMaker.selector);
179
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, address(0));
180
+ }
181
+
182
+ function test_create_revertsOnZeroOrderSizeInBatch() public {
183
+ PoolKey memory key = creatorCoin.getPoolKey();
184
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
185
+
186
+ uint256[] memory orderSizes = new uint256[](2);
187
+ orderSizes[0] = 1 ether;
188
+ orderSizes[1] = 0; // Zero size - invalid
189
+
190
+ int24[] memory orderTicks = new int24[](2);
191
+ orderTicks[0] = _alignedTick(_currentTick(key), key.tickSpacing);
192
+ orderTicks[1] = orderTicks[0] + key.tickSpacing;
193
+
194
+ vm.expectRevert(IZoraLimitOrderBook.ZeroOrderSize.selector);
195
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, users.seller);
196
+ }
197
+
198
+ function test_create_revertsOnExcessNativeValue() public {
199
+ PoolKey memory key = creatorCoin.getPoolKey();
200
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
201
+ address orderCoin = _orderCoin(key, isCurrency0);
202
+
203
+ // Only test if order coin is native currency
204
+ if (orderCoin != address(0)) {
205
+ return; // Skip if not native
206
+ }
207
+
208
+ uint256[] memory orderSizes = new uint256[](1);
209
+ orderSizes[0] = 1 ether;
210
+
211
+ int24[] memory orderTicks = new int24[](1);
212
+ int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
213
+ orderTicks[0] = isCurrency0 ? baseTick + key.tickSpacing : baseTick - key.tickSpacing;
214
+
215
+ vm.deal(users.seller, 2 ether);
216
+
217
+ vm.prank(users.seller);
218
+ vm.expectRevert(IZoraLimitOrderBook.NativeValueMismatch.selector);
219
+ limitOrderBook.create{value: 2 ether}(key, isCurrency0, orderSizes, orderTicks, users.seller); // Too much value
220
+ }
221
+
222
+ function test_create_revertsOnInsufficientNativeValue() public {
223
+ PoolKey memory key = creatorCoin.getPoolKey();
224
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
225
+ address orderCoin = _orderCoin(key, isCurrency0);
226
+
227
+ // Only test if order coin is native currency
228
+ if (orderCoin != address(0)) {
229
+ return; // Skip if not native
230
+ }
231
+
232
+ uint256[] memory orderSizes = new uint256[](1);
233
+ orderSizes[0] = 1 ether;
234
+
235
+ int24[] memory orderTicks = new int24[](1);
236
+ int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
237
+ orderTicks[0] = isCurrency0 ? baseTick + key.tickSpacing : baseTick - key.tickSpacing;
238
+
239
+ vm.deal(users.seller, 0.5 ether);
240
+
241
+ vm.prank(users.seller);
242
+ vm.expectRevert(IZoraLimitOrderBook.NativeValueMismatch.selector);
243
+ limitOrderBook.create{value: 0.5 ether}(key, isCurrency0, orderSizes, orderTicks, users.seller); // Too little value
244
+ }
245
+
246
+ function test_create_revertsOnInsufficientERC20Approval() public {
247
+ PoolKey memory key = creatorCoin.getPoolKey();
248
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
249
+ address orderCoin = _orderCoin(key, isCurrency0);
250
+
251
+ // Only test if order coin is ERC20
252
+ if (orderCoin == address(0)) {
253
+ return; // Skip if native
254
+ }
255
+
256
+ uint256[] memory orderSizes = new uint256[](1);
257
+ orderSizes[0] = 1 ether;
258
+
259
+ int24[] memory orderTicks = new int24[](1);
260
+ int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
261
+ orderTicks[0] = isCurrency0 ? baseTick + key.tickSpacing : baseTick - key.tickSpacing;
262
+
263
+ deal(orderCoin, users.seller, 1 ether);
264
+ // Don't approve - should fail with ERC20 error
265
+
266
+ vm.prank(users.seller);
267
+ vm.expectRevert(); // ERC20 will throw standard error
268
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, users.seller);
269
+ }
270
+
271
+ function test_create_revertsOnInsufficientERC20Balance() public {
272
+ PoolKey memory key = creatorCoin.getPoolKey();
273
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
274
+ address orderCoin = _orderCoin(key, isCurrency0);
275
+
276
+ // Only test if order coin is ERC20
277
+ if (orderCoin == address(0)) {
278
+ return; // Skip if native
279
+ }
280
+
281
+ uint256[] memory orderSizes = new uint256[](1);
282
+ orderSizes[0] = 1 ether;
283
+
284
+ int24[] memory orderTicks = new int24[](1);
285
+ int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
286
+ orderTicks[0] = isCurrency0 ? baseTick + key.tickSpacing : baseTick - key.tickSpacing;
287
+
288
+ // Give only half the needed balance
289
+ deal(orderCoin, users.seller, 0.5 ether);
290
+ vm.startPrank(users.seller);
291
+ IERC20(orderCoin).approve(address(limitOrderBook), 1 ether);
292
+
293
+ vm.expectRevert(); // ERC20 will throw standard error
294
+ limitOrderBook.create(key, isCurrency0, orderSizes, orderTicks, users.seller);
295
+ vm.stopPrank();
296
+ }
297
+
298
+ function test_makerBalanceUpdated_emittedOnCreate() public {
299
+ PoolKey memory key = creatorCoin.getPoolKey();
300
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
301
+ address orderCoin = _orderCoin(key, isCurrency0);
302
+
303
+ uint256[] memory orderSizes = new uint256[](3);
304
+ orderSizes[0] = 100 ether;
305
+ orderSizes[1] = 200 ether;
306
+ orderSizes[2] = 150 ether;
307
+ (, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, orderSizes.length, 1);
308
+
309
+ uint256 totalSize;
310
+ for (uint256 i; i < orderSizes.length; ++i) {
311
+ totalSize += orderSizes[i];
312
+ }
313
+
314
+ if (orderCoin == address(0)) {
315
+ vm.deal(users.seller, totalSize);
316
+ } else {
317
+ deal(orderCoin, users.seller, totalSize);
318
+ vm.prank(users.seller);
319
+ IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
320
+ }
321
+
322
+ vm.recordLogs();
323
+ vm.prank(users.seller);
324
+ limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
325
+
326
+ Vm.Log[] memory logs = vm.getRecordedLogs();
327
+
328
+ // Find MakerBalanceUpdated events
329
+ uint256 eventCount;
330
+ uint256 finalBalance;
331
+
332
+ for (uint256 i; i < logs.length; ++i) {
333
+ if (logs[i].topics[0] == IZoraLimitOrderBook.MakerBalanceUpdated.selector) {
334
+ address eventMaker = address(uint160(uint256(logs[i].topics[1])));
335
+ address eventCoin = address(uint160(uint256(logs[i].topics[2])));
336
+ if (eventMaker == users.seller && eventCoin == orderCoin) {
337
+ finalBalance = abi.decode(logs[i].data, (uint256));
338
+ ++eventCount;
339
+ }
340
+ }
341
+ }
342
+
343
+ // Should have 3 events (one per order created)
344
+ assertEq(eventCount, 3, "should emit 3 MakerBalanceUpdated events");
345
+ assertEq(finalBalance, _makerBalance(users.seller, orderCoin), "final balance from event should match actual");
346
+ assertGt(finalBalance, 0, "final balance should be positive after creation");
347
+ }
348
+ }