@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,461 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
+ import {AccessManager} from "@openzeppelin/contracts/access/manager/AccessManager.sol";
6
+ import {SimpleAccessManaged} from "../src/access/SimpleAccessManaged.sol";
7
+ import {IZoraLimitOrderBook} from "../src/IZoraLimitOrderBook.sol";
8
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
9
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
10
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
12
+
13
+ contract LimitOrderAccessControlTest is BaseTest {
14
+ uint64 public constant CREATOR_ROLE = 1;
15
+ address public unauthorizedUser;
16
+ address public authorizedRouter;
17
+
18
+ function setUp() public override {
19
+ super.setUpNonForked();
20
+
21
+ // Set up test users
22
+ unauthorizedUser = makeAddr("unauthorizedUser");
23
+ authorizedRouter = makeAddr("authorizedRouter");
24
+ }
25
+
26
+ function _prepareOrder(
27
+ address caller,
28
+ PoolKey memory key
29
+ ) internal returns (bool isCurrency0, address orderCoin, uint256[] memory orderSizes, int24[] memory orderTicks) {
30
+ isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
31
+ orderCoin = _orderCoin(key, isCurrency0);
32
+
33
+ orderSizes = new uint256[](1);
34
+ orderSizes[0] = 1 ether;
35
+ orderTicks = new int24[](1);
36
+ int24 currentTick = _alignedTick(_currentTick(key), key.tickSpacing);
37
+ orderTicks[0] = isCurrency0 ? currentTick + key.tickSpacing * 4 : currentTick - key.tickSpacing * 4;
38
+
39
+ if (orderCoin == address(0)) {
40
+ vm.deal(caller, 2 ether);
41
+ } else {
42
+ deal(orderCoin, caller, 2 ether);
43
+ }
44
+
45
+ vm.startPrank(caller);
46
+ if (orderCoin != address(0)) {
47
+ IERC20(orderCoin).approve(address(limitOrderBook), 1 ether);
48
+ }
49
+ }
50
+
51
+ function _createOrder(
52
+ PoolKey memory key,
53
+ bool isCurrency0,
54
+ uint256[] memory orderSizes,
55
+ int24[] memory orderTicks,
56
+ address caller,
57
+ address orderCoin
58
+ ) internal {
59
+ limitOrderBook.create{value: orderCoin == address(0) ? 1 ether : 0}(key, isCurrency0, orderSizes, orderTicks, caller);
60
+ vm.stopPrank();
61
+ }
62
+
63
+ function _registerTestHook(address hookAddress) internal {
64
+ address[] memory hooks = new address[](1);
65
+ hooks[0] = hookAddress;
66
+ string[] memory tags = new string[](1);
67
+ tags[0] = "TEST_HOOK";
68
+ vm.prank(users.factoryOwner);
69
+ zoraHookRegistry.registerHooks(hooks, tags);
70
+ }
71
+
72
+ function test_create_worksWithPublicRole() public {
73
+ PoolKey memory key = creatorCoin.getPoolKey();
74
+
75
+ (bool isCurrency0, address orderCoin, uint256[] memory orderSizes, int24[] memory orderTicks) = _prepareOrder(unauthorizedUser, key);
76
+ _createOrder(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser, orderCoin);
77
+ }
78
+
79
+ function test_transitionToPermissioned() public {
80
+ PoolKey memory key = creatorCoin.getPoolKey();
81
+
82
+ // Initially anyone can create orders (PUBLIC_ROLE is set in BaseTest)
83
+ (bool isCurrency0, address orderCoin, uint256[] memory orderSizes, int24[] memory orderTicks) = _prepareOrder(unauthorizedUser, key);
84
+ _createOrder(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser, orderCoin);
85
+
86
+ // Create a specific role for authorized creators
87
+ accessManager.labelRole(CREATOR_ROLE, "CREATOR");
88
+
89
+ // Grant role to authorized router
90
+ accessManager.grantRole(CREATOR_ROLE, authorizedRouter, 0);
91
+
92
+ // Switch function to require CREATOR_ROLE
93
+ bytes4[] memory selectors = new bytes4[](1);
94
+ selectors[0] = IZoraLimitOrderBook.create.selector;
95
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, CREATOR_ROLE);
96
+
97
+ // Now unauthorized user should fail
98
+ (isCurrency0, orderCoin, orderSizes, orderTicks) = _prepareOrder(unauthorizedUser, key);
99
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
100
+ limitOrderBook.create{value: orderCoin == address(0) ? 1 ether : 0}(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser);
101
+ vm.stopPrank();
102
+
103
+ // But authorized router should succeed
104
+ (isCurrency0, orderCoin, orderSizes, orderTicks) = _prepareOrder(authorizedRouter, key);
105
+ _createOrder(key, isCurrency0, orderSizes, orderTicks, authorizedRouter, orderCoin);
106
+
107
+ // If we got here without reverting, the test passed
108
+ }
109
+
110
+ function test_unauthorizedUserCannotCreate() public {
111
+ PoolKey memory key = creatorCoin.getPoolKey();
112
+
113
+ // Set up permissioned mode
114
+ accessManager.labelRole(CREATOR_ROLE, "CREATOR");
115
+ accessManager.grantRole(CREATOR_ROLE, authorizedRouter, 0);
116
+
117
+ bytes4[] memory selectors = new bytes4[](1);
118
+ selectors[0] = IZoraLimitOrderBook.create.selector;
119
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, CREATOR_ROLE);
120
+
121
+ // Unauthorized user tries to create
122
+ (bool isCurrency0, address orderCoin, uint256[] memory orderSizes, int24[] memory orderTicks) = _prepareOrder(unauthorizedUser, key);
123
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
124
+ limitOrderBook.create{value: orderCoin == address(0) ? 1 ether : 0}(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser);
125
+ vm.stopPrank();
126
+ }
127
+
128
+ function test_grantAndRevokeRole() public {
129
+ PoolKey memory key = creatorCoin.getPoolKey();
130
+
131
+ // Set up permissioned mode
132
+ accessManager.labelRole(CREATOR_ROLE, "CREATOR");
133
+
134
+ bytes4[] memory selectors = new bytes4[](1);
135
+ selectors[0] = IZoraLimitOrderBook.create.selector;
136
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, CREATOR_ROLE);
137
+
138
+ // Initially unauthorized user cannot create
139
+ (bool isCurrency0, address orderCoin, uint256[] memory orderSizes, int24[] memory orderTicks) = _prepareOrder(unauthorizedUser, key);
140
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
141
+ limitOrderBook.create{value: orderCoin == address(0) ? 1 ether : 0}(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser);
142
+ vm.stopPrank();
143
+
144
+ // Grant role to user
145
+ accessManager.grantRole(CREATOR_ROLE, unauthorizedUser, 0);
146
+
147
+ // Now user can create
148
+ (isCurrency0, orderCoin, orderSizes, orderTicks) = _prepareOrder(unauthorizedUser, key);
149
+ _createOrder(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser, orderCoin);
150
+
151
+ // Revoke role
152
+ accessManager.revokeRole(CREATOR_ROLE, unauthorizedUser);
153
+
154
+ // Now user cannot create again
155
+ (isCurrency0, orderCoin, orderSizes, orderTicks) = _prepareOrder(unauthorizedUser, key);
156
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
157
+ limitOrderBook.create{value: orderCoin == address(0) ? 1 ether : 0}(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser);
158
+ vm.stopPrank();
159
+ }
160
+
161
+ function test_adminCanReconfigure() public {
162
+ PoolKey memory key = creatorCoin.getPoolKey();
163
+
164
+ // Admin sets up initial permissioned mode
165
+ accessManager.labelRole(CREATOR_ROLE, "CREATOR");
166
+ accessManager.grantRole(CREATOR_ROLE, authorizedRouter, 0);
167
+
168
+ bytes4[] memory selectors = new bytes4[](1);
169
+ selectors[0] = IZoraLimitOrderBook.create.selector;
170
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, CREATOR_ROLE);
171
+
172
+ // Unauthorized user cannot create
173
+ (bool isCurrency0, address orderCoin, uint256[] memory orderSizes, int24[] memory orderTicks) = _prepareOrder(unauthorizedUser, key);
174
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
175
+ limitOrderBook.create{value: orderCoin == address(0) ? 1 ether : 0}(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser);
176
+ vm.stopPrank();
177
+
178
+ // Admin decides to open it back up to public
179
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, accessManager.PUBLIC_ROLE());
180
+
181
+ // Now anyone can create again
182
+ (isCurrency0, orderCoin, orderSizes, orderTicks) = _prepareOrder(unauthorizedUser, key);
183
+ _createOrder(key, isCurrency0, orderSizes, orderTicks, unauthorizedUser, orderCoin);
184
+ }
185
+
186
+ function test_nonHookCannotFillWhileUnlocked() public {
187
+ PoolKey memory key = creatorCoin.getPoolKey();
188
+
189
+ // Create a mock contract that will try to call fill during unlock
190
+ UnlockedFillCaller caller = new UnlockedFillCaller(address(limitOrderBook), address(poolManager));
191
+
192
+ // Attempt to call fill while unlocked - should revert with UnlockedFillNotAllowed
193
+ vm.expectRevert(IZoraLimitOrderBook.UnlockedFillNotAllowed.selector);
194
+ caller.attemptUnlockedFill(key, false, -type(int24).max, type(int24).max, 1, address(0));
195
+ }
196
+
197
+ function test_fillRegisteredHookCanFillWhileUnlocked() public {
198
+ PoolKey memory key = creatorCoin.getPoolKey();
199
+
200
+ vm.recordLogs();
201
+ _executeSingleHopSwapWithLimitOrders(users.seller, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
202
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
203
+ assertGt(created.length, 0, "expected orders to be created");
204
+
205
+ // Move price past orders so they are fully crossed
206
+ _movePriceBeyondTicksWithAutoFillDisabled(created);
207
+
208
+ (int24 startTick, int24 endTick) = _tickWindow(created, key);
209
+ UnlockedFillCaller caller = new UnlockedFillCaller(address(limitOrderBook), address(poolManager));
210
+ _registerTestHook(address(caller));
211
+
212
+ vm.recordLogs();
213
+ caller.attemptUnlockedFill(key, created[0].isCurrency0, startTick, endTick, created.length, address(0));
214
+ FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
215
+
216
+ assertEq(fills.length, created.length, "fill count mismatch");
217
+ assertEq(_makerBalance(users.seller, created[0].coin), 0, "maker balance should be zero");
218
+ }
219
+
220
+ function test_fillUnregisteredHookCannotFillWhileUnlocked() public {
221
+ PoolKey memory key = creatorCoin.getPoolKey();
222
+ UnlockedFillCaller caller = new UnlockedFillCaller(address(limitOrderBook), address(poolManager));
223
+
224
+ vm.expectRevert(IZoraLimitOrderBook.UnlockedFillNotAllowed.selector);
225
+ caller.attemptUnlockedFill(key, true, -type(int24).max, type(int24).max, 5, address(0));
226
+ }
227
+
228
+ function test_fill_MaxFillCountDefaultsToStorage() public {
229
+ PoolKey memory key = creatorCoin.getPoolKey();
230
+
231
+ vm.recordLogs();
232
+ _executeSingleHopSwapWithLimitOrders(users.seller, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
233
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
234
+ assertGt(created.length, 2, "expected multiple orders");
235
+
236
+ // Move price past orders so they are fully crossed
237
+ _movePriceBeyondTicksWithAutoFillDisabled(created);
238
+
239
+ uint256 previousMax = limitOrderBook.getMaxFillCount();
240
+ vm.prank(users.factoryOwner);
241
+ limitOrderBook.setMaxFillCount(2);
242
+ (int24 startTick, int24 endTick) = _tickWindow(created, key);
243
+
244
+ vm.recordLogs();
245
+ limitOrderBook.fill(key, created[0].isCurrency0, startTick, endTick, 0, address(0));
246
+ FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
247
+ assertEq(fills.length, 2, "should use stored maxFillCount when input is zero");
248
+
249
+ vm.prank(users.factoryOwner);
250
+ limitOrderBook.setMaxFillCount(previousMax);
251
+ }
252
+
253
+ function test_fillBatchIgnoresEmptyOrderArrays() public {
254
+ PoolKey memory key = creatorCoin.getPoolKey();
255
+
256
+ vm.recordLogs();
257
+ _executeSingleHopSwapWithLimitOrders(users.seller, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
258
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
259
+ assertGt(created.length, 2, "expected >=2 orders");
260
+
261
+ // Move price past orders so they are fully crossed
262
+ _movePriceBeyondTicksWithAutoFillDisabled(created);
263
+
264
+ uint256 makerBalanceBefore = _makerBalance(users.seller, created[0].coin);
265
+
266
+ bytes32[] memory ids = new bytes32[](2);
267
+ ids[0] = created[0].orderId;
268
+ ids[1] = created[1].orderId;
269
+
270
+ IZoraLimitOrderBook.OrderBatch[] memory batches = new IZoraLimitOrderBook.OrderBatch[](3);
271
+ batches[0].key = key;
272
+ batches[0].isCurrency0 = created[0].isCurrency0;
273
+ batches[0].orderIds = new bytes32[](0);
274
+ batches[1].key = key;
275
+ batches[1].isCurrency0 = created[0].isCurrency0;
276
+ batches[1].orderIds = ids;
277
+ batches[2].key = key;
278
+ batches[2].isCurrency0 = created[0].isCurrency0;
279
+ batches[2].orderIds = new bytes32[](0);
280
+
281
+ vm.recordLogs();
282
+ limitOrderBook.fill(batches, address(0));
283
+ FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
284
+ assertEq(fills.length, ids.length, "only populated batch should fill");
285
+
286
+ uint256 makerBalanceAfter = _makerBalance(users.seller, created[0].coin);
287
+ uint256 expectedDelta = created[0].size + created[1].size;
288
+ assertApproxEqAbs(makerBalanceBefore - makerBalanceAfter, expectedDelta, 3, "unexpected maker balance delta");
289
+ }
290
+
291
+ function test_unlockCallbackRevertsForNonPoolManager() public {
292
+ vm.expectRevert(IZoraLimitOrderBook.NotPoolManager.selector);
293
+ limitOrderBook.unlockCallback(bytes(""));
294
+ }
295
+
296
+ function test_receiveRevertsForNonPoolManager() public {
297
+ vm.deal(address(this), 1 ether);
298
+ vm.expectRevert(IZoraLimitOrderBook.NotPoolManager.selector);
299
+ payable(address(limitOrderBook)).transfer(1 wei);
300
+ }
301
+
302
+ function test_setMaxFillCount_worksWithPublicRole() public {
303
+ // Initially max fill count should be 50 (set in BaseTest)
304
+ assertEq(limitOrderBook.getMaxFillCount(), 50);
305
+
306
+ // setMaxFillCount is already configured with PUBLIC_ROLE in BaseTest
307
+ // Any user should be able to set it
308
+ vm.prank(unauthorizedUser);
309
+ limitOrderBook.setMaxFillCount(20);
310
+
311
+ assertEq(limitOrderBook.getMaxFillCount(), 20);
312
+ }
313
+
314
+ function test_setMaxFillCount_unauthorizedUserCannotSet() public {
315
+ uint64 MAX_FILL_COUNT_ROLE = 2;
316
+
317
+ // Set up permissioned mode for setMaxFillCount
318
+ accessManager.labelRole(MAX_FILL_COUNT_ROLE, "MAX_FILL_COUNT_SETTER");
319
+ accessManager.grantRole(MAX_FILL_COUNT_ROLE, authorizedRouter, 0);
320
+
321
+ bytes4[] memory selectors = new bytes4[](1);
322
+ selectors[0] = IZoraLimitOrderBook.setMaxFillCount.selector;
323
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, MAX_FILL_COUNT_ROLE);
324
+
325
+ // Unauthorized user tries to set max fill count
326
+ vm.prank(unauthorizedUser);
327
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
328
+ limitOrderBook.setMaxFillCount(20);
329
+
330
+ // Verify value hasn't changed (still 50 from BaseTest)
331
+ assertEq(limitOrderBook.getMaxFillCount(), 50);
332
+ }
333
+
334
+ function test_setMaxFillCount_authorizedUserCanSet() public {
335
+ uint64 MAX_FILL_COUNT_ROLE = 2;
336
+
337
+ // Set up permissioned mode
338
+ accessManager.labelRole(MAX_FILL_COUNT_ROLE, "MAX_FILL_COUNT_SETTER");
339
+ accessManager.grantRole(MAX_FILL_COUNT_ROLE, authorizedRouter, 0);
340
+
341
+ bytes4[] memory selectors = new bytes4[](1);
342
+ selectors[0] = IZoraLimitOrderBook.setMaxFillCount.selector;
343
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, MAX_FILL_COUNT_ROLE);
344
+
345
+ // Authorized user sets max fill count
346
+ vm.prank(authorizedRouter);
347
+ limitOrderBook.setMaxFillCount(25);
348
+
349
+ assertEq(limitOrderBook.getMaxFillCount(), 25);
350
+ }
351
+
352
+ function test_setMaxFillCount_adminCanSet() public {
353
+ uint64 MAX_FILL_COUNT_ROLE = 2;
354
+
355
+ // Set up permissioned mode
356
+ accessManager.labelRole(MAX_FILL_COUNT_ROLE, "MAX_FILL_COUNT_SETTER");
357
+
358
+ bytes4[] memory selectors = new bytes4[](1);
359
+ selectors[0] = IZoraLimitOrderBook.setMaxFillCount.selector;
360
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, MAX_FILL_COUNT_ROLE);
361
+
362
+ // Grant the role to admin explicitly
363
+ accessManager.grantRole(MAX_FILL_COUNT_ROLE, address(this), 0);
364
+
365
+ // Admin (this contract) should be able to set it with the granted role
366
+ limitOrderBook.setMaxFillCount(30);
367
+
368
+ assertEq(limitOrderBook.getMaxFillCount(), 30);
369
+ }
370
+
371
+ function test_setMaxFillCount_grantAndRevokeRole() public {
372
+ uint64 MAX_FILL_COUNT_ROLE = 2;
373
+
374
+ // Set up permissioned mode
375
+ accessManager.labelRole(MAX_FILL_COUNT_ROLE, "MAX_FILL_COUNT_SETTER");
376
+
377
+ bytes4[] memory selectors = new bytes4[](1);
378
+ selectors[0] = IZoraLimitOrderBook.setMaxFillCount.selector;
379
+ accessManager.setTargetFunctionRole(address(limitOrderBook), selectors, MAX_FILL_COUNT_ROLE);
380
+
381
+ // Initially unauthorized user cannot set
382
+ vm.prank(unauthorizedUser);
383
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
384
+ limitOrderBook.setMaxFillCount(15);
385
+
386
+ // Grant role to user
387
+ accessManager.grantRole(MAX_FILL_COUNT_ROLE, unauthorizedUser, 0);
388
+
389
+ // Now user can set
390
+ vm.prank(unauthorizedUser);
391
+ limitOrderBook.setMaxFillCount(15);
392
+ assertEq(limitOrderBook.getMaxFillCount(), 15);
393
+
394
+ // Revoke role
395
+ accessManager.revokeRole(MAX_FILL_COUNT_ROLE, unauthorizedUser);
396
+
397
+ // Now user cannot set again
398
+ vm.prank(unauthorizedUser);
399
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
400
+ limitOrderBook.setMaxFillCount(20);
401
+
402
+ // Verify value hasn't changed from last successful set
403
+ assertEq(limitOrderBook.getMaxFillCount(), 15);
404
+ }
405
+
406
+ function test_setAuthority_revertsForUnauthorizedCaller() public {
407
+ address newAuthority = address(new AccessManager(address(this)));
408
+
409
+ vm.prank(unauthorizedUser);
410
+ vm.expectRevert(SimpleAccessManaged.AccessManagedUnauthorized.selector);
411
+ limitOrderBook.setAuthority(newAuthority);
412
+ }
413
+
414
+ function test_setAuthority_revertsForNonContractAddress() public {
415
+ // Deploy a simple test contract with test contract as authority
416
+ AuthorityTester tester = new AuthorityTester(address(this));
417
+
418
+ address eoaAddress = makeAddr("eoa");
419
+
420
+ // Try to set EOA as authority - should revert with AccessManagedInvalidAuthority
421
+ vm.expectRevert(abi.encodeWithSelector(SimpleAccessManaged.AccessManagedInvalidAuthority.selector, eoaAddress));
422
+ tester.setAuthority(eoaAddress);
423
+ }
424
+ }
425
+
426
+ contract UnlockedFillCaller {
427
+ IZoraLimitOrderBook public immutable limitOrderBook;
428
+ IPoolManager public immutable poolManager;
429
+
430
+ PoolKey private pendingKey;
431
+ bool private pendingIsCurrency0;
432
+ int24 private pendingStartTick;
433
+ int24 private pendingEndTick;
434
+ uint256 private pendingMaxFillCount;
435
+ address private pendingFillReferral;
436
+
437
+ constructor(address _limitOrderBook, address _poolManager) {
438
+ limitOrderBook = IZoraLimitOrderBook(_limitOrderBook);
439
+ poolManager = IPoolManager(_poolManager);
440
+ }
441
+
442
+ function attemptUnlockedFill(PoolKey memory key, bool isCurrency0, int24 startTick, int24 endTick, uint256 maxFillCount, address fillReferral) external {
443
+ pendingKey = key;
444
+ pendingIsCurrency0 = isCurrency0;
445
+ pendingStartTick = startTick;
446
+ pendingEndTick = endTick;
447
+ pendingMaxFillCount = maxFillCount;
448
+ pendingFillReferral = fillReferral;
449
+
450
+ poolManager.unlock(abi.encode(0));
451
+ }
452
+
453
+ function unlockCallback(bytes calldata) external returns (bytes memory) {
454
+ limitOrderBook.fill(pendingKey, pendingIsCurrency0, pendingStartTick, pendingEndTick, pendingMaxFillCount, pendingFillReferral);
455
+ return bytes("");
456
+ }
457
+ }
458
+
459
+ contract AuthorityTester is SimpleAccessManaged {
460
+ constructor(address initialAuthority) SimpleAccessManaged(initialAuthority) {}
461
+ }
@@ -0,0 +1,194 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
6
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
7
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
8
+
9
+ contract LimitOrderBitmapTest is BaseTest {
10
+ function test_bitmap_MultipleTicksInitialized() public {
11
+ PoolKey memory key = creatorCoin.getPoolKey();
12
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
13
+ address orderCoin = _orderCoin(key, isCurrency0);
14
+
15
+ // Create orders at 5 different ticks
16
+ (uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 5, 25e18);
17
+
18
+ uint256 totalSize;
19
+ for (uint256 i; i < orderSizes.length; ++i) {
20
+ totalSize += orderSizes[i];
21
+ }
22
+ _fundAndApprove(users.seller, orderCoin, totalSize);
23
+
24
+ vm.recordLogs();
25
+ vm.prank(users.seller);
26
+ limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
27
+
28
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
29
+ bytes32 poolKeyHash = created[0].poolKeyHash;
30
+
31
+ // Verify all ticks are initialized in bitmap
32
+ for (uint256 i; i < orderTicks.length; ++i) {
33
+ assertTrue(_isTickInitialized(poolKeyHash, orderCoin, orderTicks[i], key.tickSpacing), "tick should be initialized");
34
+ }
35
+ }
36
+
37
+ function test_bitmap_ClearedWhenTickEmpty() public {
38
+ PoolKey memory key = creatorCoin.getPoolKey();
39
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
40
+ address orderCoin = _orderCoin(key, isCurrency0);
41
+
42
+ // Create single order
43
+ (uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
44
+
45
+ _fundAndApprove(users.seller, orderCoin, orderSizes[0]);
46
+
47
+ vm.recordLogs();
48
+ vm.prank(users.seller);
49
+ limitOrderBook.create{value: orderCoin == address(0) ? orderSizes[0] : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
50
+
51
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
52
+ bytes32 poolKeyHash = created[0].poolKeyHash;
53
+
54
+ // Verify tick is initialized
55
+ assertTrue(_isTickInitialized(poolKeyHash, orderCoin, orderTicks[0], key.tickSpacing), "tick should be initialized after create");
56
+
57
+ // Withdraw order
58
+ bytes32[] memory orderIds = new bytes32[](1);
59
+ orderIds[0] = created[0].orderId;
60
+ vm.prank(users.seller);
61
+ limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
62
+
63
+ // Verify tick is cleared in bitmap
64
+ assertFalse(_isTickInitialized(poolKeyHash, orderCoin, orderTicks[0], key.tickSpacing), "tick should be cleared after withdraw");
65
+ }
66
+
67
+ function test_bitmap_RemainsSetWithPartialOrders() public {
68
+ PoolKey memory key = creatorCoin.getPoolKey();
69
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
70
+ address orderCoin = _orderCoin(key, isCurrency0);
71
+
72
+ // Create 2 orders at same tick
73
+ int24 tick = _getValidTick(key, isCurrency0);
74
+ uint256[] memory orderSizes = new uint256[](2);
75
+ orderSizes[0] = 25e18;
76
+ orderSizes[1] = 25e18;
77
+ int24[] memory orderTicks = new int24[](2);
78
+ orderTicks[0] = tick;
79
+ orderTicks[1] = tick;
80
+
81
+ uint256 totalSize = orderSizes[0] + orderSizes[1];
82
+ _fundAndApprove(users.seller, orderCoin, totalSize);
83
+
84
+ vm.recordLogs();
85
+ vm.prank(users.seller);
86
+ limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
87
+
88
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
89
+ bytes32 poolKeyHash = created[0].poolKeyHash;
90
+
91
+ // Withdraw first order only
92
+ bytes32[] memory orderIds = new bytes32[](1);
93
+ orderIds[0] = created[0].orderId;
94
+ vm.prank(users.seller);
95
+ limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
96
+
97
+ // Bitmap should still be set because second order remains
98
+ assertTrue(_isTickInitialized(poolKeyHash, orderCoin, tick, key.tickSpacing), "tick should remain initialized with remaining order");
99
+ }
100
+
101
+ function test_bitmap_wordBoundaries() public {
102
+ PoolKey memory key = creatorCoin.getPoolKey();
103
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
104
+ address orderCoin = _orderCoin(key, isCurrency0);
105
+ int24 spacing = key.tickSpacing;
106
+
107
+ // Test ticks at word boundaries (words change every 256 ticks)
108
+ // Word boundary occurs at tick = n * 256 * spacing
109
+ int24[] memory boundaryTicks = new int24[](4);
110
+ boundaryTicks[0] = 0; // word 0, bit 0
111
+ boundaryTicks[1] = 255 * spacing; // word 0, bit 255 (last bit in word 0)
112
+ boundaryTicks[2] = 256 * spacing; // word 1, bit 0 (first bit in word 1)
113
+ boundaryTicks[3] = -256 * spacing; // word -1, bit 0
114
+
115
+ uint256[] memory orderSizes = new uint256[](4);
116
+ for (uint256 i = 0; i < 4; i++) {
117
+ orderSizes[i] = 10e18;
118
+ }
119
+
120
+ uint256 totalSize = 40e18;
121
+ _fundAndApprove(users.seller, orderCoin, totalSize);
122
+
123
+ vm.recordLogs();
124
+ vm.prank(users.seller);
125
+ limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, boundaryTicks, users.seller);
126
+
127
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
128
+ bytes32 poolKeyHash = created[0].poolKeyHash;
129
+
130
+ // Verify all boundary ticks are initialized
131
+ for (uint256 i = 0; i < boundaryTicks.length; i++) {
132
+ assertTrue(_isTickInitialized(poolKeyHash, orderCoin, boundaryTicks[i], spacing), "boundary tick should be initialized");
133
+ }
134
+ }
135
+
136
+ function test_bitmap_extremeTicks() public {
137
+ PoolKey memory key = creatorCoin.getPoolKey();
138
+ bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
139
+ address orderCoin = _orderCoin(key, isCurrency0);
140
+ int24 spacing = key.tickSpacing;
141
+
142
+ // Use ticks near the current tick but test extreme positions within range
143
+ int24 currentTick = _currentTick(key);
144
+
145
+ // Create ticks at far distances from current, but within reasonable range
146
+ // For currency0 (sells), go far above current tick
147
+ // For currency1 (sells), go far below current tick
148
+ int24[] memory extremeTicks = new int24[](2);
149
+ if (isCurrency0) {
150
+ // Far above current tick
151
+ extremeTicks[0] = _alignedTick(currentTick + (500 * spacing), spacing);
152
+ extremeTicks[1] = _alignedTick(currentTick + (1000 * spacing), spacing);
153
+ } else {
154
+ // Far below current tick
155
+ extremeTicks[0] = _alignedTick(currentTick - (500 * spacing), spacing);
156
+ extremeTicks[1] = _alignedTick(currentTick - (1000 * spacing), spacing);
157
+ }
158
+
159
+ uint256[] memory orderSizes = new uint256[](2);
160
+ orderSizes[0] = 10e18;
161
+ orderSizes[1] = 10e18;
162
+
163
+ uint256 totalSize = 20e18;
164
+ _fundAndApprove(users.seller, orderCoin, totalSize);
165
+
166
+ vm.recordLogs();
167
+ vm.prank(users.seller);
168
+ limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, extremeTicks, users.seller);
169
+
170
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
171
+ bytes32 poolKeyHash = created[0].poolKeyHash;
172
+
173
+ // Verify extreme ticks are initialized
174
+ assertTrue(_isTickInitialized(poolKeyHash, orderCoin, extremeTicks[0], spacing), "far tick 1 should be initialized");
175
+ assertTrue(_isTickInitialized(poolKeyHash, orderCoin, extremeTicks[1], spacing), "far tick 2 should be initialized");
176
+ }
177
+
178
+ function _fundAndApprove(address user, address token, uint256 amount) internal {
179
+ if (token == address(0)) {
180
+ vm.deal(user, amount);
181
+ } else {
182
+ deal(token, user, amount);
183
+ vm.prank(user);
184
+ IERC20(token).approve(address(limitOrderBook), amount);
185
+ }
186
+ }
187
+
188
+ function _getValidTick(PoolKey memory key, bool isCurrency0) internal view returns (int24) {
189
+ int24 currentTick = _currentTick(key);
190
+ int24 offset = isCurrency0 ? key.tickSpacing * 2 : -key.tickSpacing * 2;
191
+ int24 targetTick = currentTick + offset;
192
+ return _alignedTick(targetTick, key.tickSpacing);
193
+ }
194
+ }