@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,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
|
+
}
|