@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,1005 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
|
+
import {IZoraLimitOrderBook} from "../src/IZoraLimitOrderBook.sol";
|
|
6
|
+
import {LimitOrderCommon} from "../src/libs/LimitOrderCommon.sol";
|
|
7
|
+
import {CoinCommon} from "@zoralabs/coins/src/libs/CoinCommon.sol";
|
|
8
|
+
import {ICoin} from "@zoralabs/coins/src/interfaces/ICoin.sol";
|
|
9
|
+
import {LimitOrderTypes} from "../src/libs/LimitOrderTypes.sol";
|
|
10
|
+
|
|
11
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
12
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
13
|
+
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
|
14
|
+
|
|
15
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
16
|
+
|
|
17
|
+
contract LimitOrderFillTest is BaseTest {
|
|
18
|
+
function test_debugCreateMakerBalance() public {
|
|
19
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
20
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
21
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(key, isCurrency0);
|
|
22
|
+
|
|
23
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
|
|
24
|
+
uint256 totalSize = orderSizes[0];
|
|
25
|
+
|
|
26
|
+
if (orderCoin == address(0)) {
|
|
27
|
+
vm.deal(users.seller, totalSize);
|
|
28
|
+
} else {
|
|
29
|
+
deal(orderCoin, users.seller, totalSize);
|
|
30
|
+
vm.prank(users.seller);
|
|
31
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
vm.prank(users.seller);
|
|
35
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
36
|
+
|
|
37
|
+
// Contract view
|
|
38
|
+
uint256 onchainBalance = limitOrderBook.balanceOf(users.seller, orderCoin);
|
|
39
|
+
assertEq(onchainBalance, totalSize, "maker balance from contract");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function test_fillWithNoOrdersIsNoop() public {
|
|
43
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
44
|
+
|
|
45
|
+
vm.recordLogs();
|
|
46
|
+
limitOrderBook.fill(key, true, -type(int24).max, type(int24).max, 5, address(0));
|
|
47
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
48
|
+
assertEq(fills.length, 0, "unexpected fills");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function test_fillRangeConsumesOrders() public {
|
|
52
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
53
|
+
|
|
54
|
+
vm.recordLogs();
|
|
55
|
+
_executeSingleHopSwapWithLimitOrders(users.buyer, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
56
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
57
|
+
assertGt(created.length, 0, "expected orders to be created");
|
|
58
|
+
_assertOpenOrderState(users.buyer, created[0].coin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
59
|
+
|
|
60
|
+
// Move price past orders so they are fully crossed
|
|
61
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
62
|
+
|
|
63
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
64
|
+
bool isCurrency0 = created[0].isCurrency0;
|
|
65
|
+
address orderCoin = created[0].coin;
|
|
66
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
67
|
+
uint256 epochBefore = _poolEpoch(poolKeyHash);
|
|
68
|
+
|
|
69
|
+
vm.recordLogs();
|
|
70
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, created.length, address(0));
|
|
71
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
72
|
+
|
|
73
|
+
assertEq(fills.length, created.length, "fill count mismatch");
|
|
74
|
+
for (uint256 i; i < fills.length; ++i) {
|
|
75
|
+
assertEq(fills[i].maker, users.buyer, "maker mismatch");
|
|
76
|
+
assertEq(fills[i].coinIn, orderCoin, "coin mismatch");
|
|
77
|
+
assertEq(fills[i].fillReferral, address(0), "unexpected referral");
|
|
78
|
+
assertEq(fills[i].fillReferralAmount, 0, "unexpected referral amount");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
assertEq(_makerBalance(users.buyer, orderCoin), 0, "maker balance should be zero");
|
|
82
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
83
|
+
|
|
84
|
+
for (uint256 i; i < created.length; ++i) {
|
|
85
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, created[i].tick);
|
|
86
|
+
assertEq(tickQueue.length, 0, "tick queue length");
|
|
87
|
+
assertEq(tickQueue.balance, 0, "tick queue balance");
|
|
88
|
+
assertEq(tickQueue.head, bytes32(0), "tick queue head not cleared");
|
|
89
|
+
assertEq(tickQueue.tail, bytes32(0), "tick queue tail not cleared");
|
|
90
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, created[i].tick, key.tickSpacing), "tick bitmap still set");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function test_fillRangeConsumesOrdersWithAutoFillDisabledDuringSetup() public {
|
|
95
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
96
|
+
|
|
97
|
+
vm.recordLogs();
|
|
98
|
+
_executeSingleHopSwapWithLimitOrders(users.buyer, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
99
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
100
|
+
assertGt(created.length, 0, "expected orders to be created");
|
|
101
|
+
_assertOpenOrderState(users.buyer, created[0].coin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
102
|
+
|
|
103
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
104
|
+
|
|
105
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
106
|
+
bool isCurrency0 = created[0].isCurrency0;
|
|
107
|
+
address orderCoin = created[0].coin;
|
|
108
|
+
|
|
109
|
+
vm.recordLogs();
|
|
110
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, created.length, address(0));
|
|
111
|
+
for (uint256 i; i < created.length; ++i) {
|
|
112
|
+
LimitOrderTypes.LimitOrder memory orderState = limitOrderBook.exposedOrder(created[i].orderId);
|
|
113
|
+
assertEq(uint256(orderState.status), uint256(LimitOrderTypes.OrderStatus.FILLED), "order remained open");
|
|
114
|
+
}
|
|
115
|
+
assertEq(_makerBalance(users.buyer, orderCoin), 0, "maker balance should be zero");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function test_fillSentinelBoundsConsumesOrders() 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, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 2, 25e18);
|
|
124
|
+
uint256 totalSize;
|
|
125
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
126
|
+
totalSize += orderSizes[i];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (orderCoin == address(0)) {
|
|
130
|
+
vm.deal(users.seller, totalSize);
|
|
131
|
+
} else {
|
|
132
|
+
deal(orderCoin, users.seller, totalSize);
|
|
133
|
+
vm.startPrank(users.seller);
|
|
134
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
135
|
+
vm.stopPrank();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
vm.recordLogs();
|
|
139
|
+
vm.prank(users.seller);
|
|
140
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
141
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
142
|
+
assertEq(created.length, orderSizes.length, "unexpected created order count");
|
|
143
|
+
_assertOpenOrderState(users.seller, orderCoin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
144
|
+
|
|
145
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
146
|
+
|
|
147
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
148
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
149
|
+
uint256 epochBefore = _poolEpoch(poolKeyHash);
|
|
150
|
+
|
|
151
|
+
vm.recordLogs();
|
|
152
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 0, address(0));
|
|
153
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
154
|
+
|
|
155
|
+
assertEq(fills.length, created.length, "fill count mismatch");
|
|
156
|
+
assertEq(_makerBalance(users.seller, orderCoin), 0, "maker balance should be zero");
|
|
157
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
158
|
+
|
|
159
|
+
for (uint256 i; i < created.length; ++i) {
|
|
160
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, created[i].tick);
|
|
161
|
+
assertEq(tickQueue.length, 0, "tick queue length");
|
|
162
|
+
assertEq(tickQueue.balance, 0, "tick queue balance");
|
|
163
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, created[i].tick, key.tickSpacing), "tick bitmap still set");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function test_fillSentinelBoundsAtMaxTickDoesNotRevert() public {
|
|
168
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
169
|
+
_setPoolTick(key, TickMath.MAX_TICK);
|
|
170
|
+
|
|
171
|
+
vm.recordLogs();
|
|
172
|
+
limitOrderBook.fill(key, true, -type(int24).max, type(int24).max, 1, address(0));
|
|
173
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
174
|
+
assertEq(fills.length, 0, "no fills expected at sentinel boundary");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function test_fillSentinelBoundsAtMinTickDoesNotRevert() public {
|
|
178
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
179
|
+
_setPoolTick(key, TickMath.MIN_TICK);
|
|
180
|
+
|
|
181
|
+
vm.recordLogs();
|
|
182
|
+
limitOrderBook.fill(key, false, -type(int24).max, type(int24).max, 1, address(0));
|
|
183
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
184
|
+
assertEq(fills.length, 0, "no fills expected at sentinel boundary");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function test_currency1StartSentinelAnchorsCurrentTick() public {
|
|
188
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
189
|
+
bool isCurrency0 = false;
|
|
190
|
+
|
|
191
|
+
int24 anchorTick = _alignedTick(_currentTick(key), key.tickSpacing);
|
|
192
|
+
_setPoolTick(key, anchorTick);
|
|
193
|
+
|
|
194
|
+
(int24 resolvedStart, int24 resolvedEnd) = limitOrderBook.exposedResolveTickRange(key, isCurrency0, -type(int24).max, anchorTick - key.tickSpacing);
|
|
195
|
+
|
|
196
|
+
assertEq(resolvedStart, anchorTick, "start sentinel should anchor current tick");
|
|
197
|
+
assertEq(resolvedEnd, anchorTick - key.tickSpacing, "explicit end should be preserved");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function test_fillRangeUsesDefaultWhenMaxZero() public {
|
|
201
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
202
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
203
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
204
|
+
|
|
205
|
+
vm.prank(users.factoryOwner);
|
|
206
|
+
limitOrderBook.setMaxFillCount(2);
|
|
207
|
+
|
|
208
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 3, 20e18);
|
|
209
|
+
uint256 totalSize;
|
|
210
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
211
|
+
totalSize += orderSizes[i];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (orderCoin == address(0)) {
|
|
215
|
+
vm.deal(users.seller, totalSize);
|
|
216
|
+
} else {
|
|
217
|
+
deal(orderCoin, users.seller, totalSize);
|
|
218
|
+
vm.startPrank(users.seller);
|
|
219
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
220
|
+
vm.stopPrank();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
vm.recordLogs();
|
|
224
|
+
vm.prank(users.seller);
|
|
225
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
226
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
227
|
+
assertEq(created.length, orderSizes.length, "unexpected created order count");
|
|
228
|
+
_assertOpenOrderState(users.seller, orderCoin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
229
|
+
|
|
230
|
+
// Move price past orders to make them fillable, but disable auto-fill so manual fill can consume them
|
|
231
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
232
|
+
|
|
233
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
234
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
235
|
+
uint256 epochBefore = _poolEpoch(poolKeyHash);
|
|
236
|
+
|
|
237
|
+
vm.recordLogs();
|
|
238
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 0, address(0));
|
|
239
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
240
|
+
|
|
241
|
+
assertEq(fills.length, 2, "should fill router default count");
|
|
242
|
+
|
|
243
|
+
uint256 expectedRemaining = totalSize;
|
|
244
|
+
for (uint256 i; i < fills.length; ++i) {
|
|
245
|
+
expectedRemaining -= uint256(fills[i].amountIn);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Allow 1 wei rounding error due to liquidity/amount conversions in V4
|
|
249
|
+
assertApproxEqAbs(_makerBalance(users.seller, orderCoin), expectedRemaining, 1, "maker balance");
|
|
250
|
+
|
|
251
|
+
uint256 remainingTicks;
|
|
252
|
+
for (uint256 i; i < created.length; ++i) {
|
|
253
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, created[i].tick);
|
|
254
|
+
if (tickQueue.length == 0) {
|
|
255
|
+
assertEq(tickQueue.balance, 0, "cleared tick balance");
|
|
256
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, created[i].tick, key.tickSpacing), "tick bitmap still set");
|
|
257
|
+
} else {
|
|
258
|
+
++remainingTicks;
|
|
259
|
+
assertEq(tickQueue.length, 1, "remaining tick length");
|
|
260
|
+
// Allow 1 wei rounding error due to liquidity/amount conversions in V4
|
|
261
|
+
assertApproxEqAbs(uint256(tickQueue.balance), expectedRemaining, 1, "remaining tick balance");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
assertEq(remainingTicks, 1, "expected single remaining tick");
|
|
265
|
+
|
|
266
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function test_fillRangeRespectsExplicitMaxFillCount() public {
|
|
270
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
271
|
+
|
|
272
|
+
vm.recordLogs();
|
|
273
|
+
_executeSingleHopSwapWithLimitOrders(users.buyer, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
274
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
275
|
+
assertGt(created.length, 1, "expected multiple orders");
|
|
276
|
+
_assertOpenOrderState(users.buyer, created[0].coin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
277
|
+
|
|
278
|
+
// Move price past orders so they are fully crossed
|
|
279
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
280
|
+
|
|
281
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
282
|
+
bool isCurrency0 = created[0].isCurrency0;
|
|
283
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
284
|
+
address orderCoin = created[0].coin;
|
|
285
|
+
uint256 epochBefore = _poolEpoch(poolKeyHash);
|
|
286
|
+
uint256 maxFillCount = 1;
|
|
287
|
+
uint256 totalSize = _sumOrderSizes(created);
|
|
288
|
+
|
|
289
|
+
vm.recordLogs();
|
|
290
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, maxFillCount, address(0));
|
|
291
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
292
|
+
|
|
293
|
+
assertEq(fills.length, maxFillCount, "fill count mismatch");
|
|
294
|
+
|
|
295
|
+
uint256 expectedRemaining = totalSize;
|
|
296
|
+
for (uint256 i; i < fills.length; ++i) {
|
|
297
|
+
expectedRemaining -= uint256(fills[i].amountIn);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
assertEq(_makerBalance(users.buyer, orderCoin), expectedRemaining, "maker balance");
|
|
301
|
+
|
|
302
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function test_manualFillWithDisabledHookAutoFill() public {
|
|
306
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
307
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
308
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
309
|
+
|
|
310
|
+
// 1. Create a single manual order out-of-the-money
|
|
311
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 50e18);
|
|
312
|
+
uint256 totalSize = orderSizes[0];
|
|
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
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
326
|
+
assertEq(created.length, 1, "expected single order");
|
|
327
|
+
|
|
328
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
329
|
+
|
|
330
|
+
// 2. Do a small swap that DOESN'T cross the order to increment epoch
|
|
331
|
+
// (Order is far out of the money, so small swap won't reach it)
|
|
332
|
+
address swapper = makeAddr("price-mover");
|
|
333
|
+
uint128 smallSwap = uint128(DEFAULT_LIMIT_ORDER_AMOUNT / 100);
|
|
334
|
+
deal(address(zoraToken), swapper, uint256(smallSwap));
|
|
335
|
+
_swapSomeCurrencyForCoin(ICoin(address(creatorCoin)), address(zoraToken), smallSwap, swapper);
|
|
336
|
+
|
|
337
|
+
// 3. Now DISABLE hook auto-fills
|
|
338
|
+
uint256 originalMaxFillCount = limitOrderBook.getMaxFillCount();
|
|
339
|
+
vm.prank(users.factoryOwner);
|
|
340
|
+
limitOrderBook.setMaxFillCount(0);
|
|
341
|
+
|
|
342
|
+
// 4. Move price past the order WITHOUT triggering hook fills
|
|
343
|
+
uint128 swapAmount = uint128(DEFAULT_LIMIT_ORDER_AMOUNT * 10);
|
|
344
|
+
deal(address(zoraToken), swapper, uint256(swapAmount));
|
|
345
|
+
_swapSomeCurrencyForCoin(ICoin(address(creatorCoin)), address(zoraToken), swapAmount, swapper);
|
|
346
|
+
|
|
347
|
+
// 5. Restore original maxFillCount
|
|
348
|
+
vm.prank(users.factoryOwner);
|
|
349
|
+
limitOrderBook.setMaxFillCount(originalMaxFillCount);
|
|
350
|
+
|
|
351
|
+
// 6. Verify order exists and check epoch
|
|
352
|
+
uint256 currentEpoch = _poolEpoch(poolKeyHash);
|
|
353
|
+
QueueSnapshot memory queueBefore = _tickQueueSnapshot(poolKeyHash, orderCoin, created[0].tick);
|
|
354
|
+
assertGt(queueBefore.length, 0, "order should exist in tick queue");
|
|
355
|
+
|
|
356
|
+
// Note: we created at epoch 0, small swap incremented to 1, big swap tried to fill but maxFillCount=0
|
|
357
|
+
// So current epoch should be > 0, allowing our fill
|
|
358
|
+
|
|
359
|
+
// 7. Now manually fill the order with explicit tick window
|
|
360
|
+
uint256 epochBefore = currentEpoch;
|
|
361
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
362
|
+
|
|
363
|
+
vm.recordLogs();
|
|
364
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 1, address(0));
|
|
365
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
366
|
+
|
|
367
|
+
// 8. Verify fill succeeded
|
|
368
|
+
assertEq(fills.length, 1, "should fill single order");
|
|
369
|
+
assertEq(fills[0].maker, users.seller, "maker mismatch");
|
|
370
|
+
assertGt(fills[0].amountOut, 0, "should have output amount");
|
|
371
|
+
|
|
372
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
373
|
+
|
|
374
|
+
// Verify order consumed
|
|
375
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, created[0].tick);
|
|
376
|
+
assertEq(tickQueue.length, 0, "tick queue should be empty");
|
|
377
|
+
assertEq(tickQueue.balance, 0, "tick balance should be zero");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function test_fillRangePaysReferral() public {
|
|
381
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
382
|
+
address referral = makeAddr("referral");
|
|
383
|
+
|
|
384
|
+
// 1. Create orders via swap - orders placed as limit orders behind current price
|
|
385
|
+
vm.recordLogs();
|
|
386
|
+
_executeSingleHopSwapWithLimitOrders(users.buyer, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
387
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
388
|
+
assertGt(created.length, 0, "expected orders to be created");
|
|
389
|
+
_assertOpenOrderState(users.buyer, created[0].coin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
390
|
+
|
|
391
|
+
// Move price past orders so they are fully crossed
|
|
392
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
393
|
+
|
|
394
|
+
bool isCurrency0 = created[0].isCurrency0;
|
|
395
|
+
address orderCoin = created[0].coin;
|
|
396
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
397
|
+
|
|
398
|
+
// 2. Fill one order with referral address
|
|
399
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
400
|
+
address payoutCoin = isCurrency0 ? Currency.unwrap(key.currency1) : Currency.unwrap(key.currency0);
|
|
401
|
+
uint256 referralBalanceBefore = _balanceOf(payoutCoin, referral);
|
|
402
|
+
uint256 epochBefore = _poolEpoch(poolKeyHash);
|
|
403
|
+
|
|
404
|
+
vm.recordLogs();
|
|
405
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 1, referral);
|
|
406
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
407
|
+
|
|
408
|
+
assertEq(fills.length, 1, "should fill single order");
|
|
409
|
+
assertEq(fills[0].maker, users.buyer, "maker mismatch");
|
|
410
|
+
assertEq(fills[0].coinIn, orderCoin, "coin mismatch");
|
|
411
|
+
assertEq(fills[0].fillReferral, referral, "referral address is correctly tracked");
|
|
412
|
+
|
|
413
|
+
// Verify referral balance change matches the event amount (validates accounting correctness)
|
|
414
|
+
uint256 referralBalanceAfter = _balanceOf(payoutCoin, referral);
|
|
415
|
+
uint256 referralDelta = referralBalanceAfter - referralBalanceBefore;
|
|
416
|
+
assertEq(referralDelta, fills[0].fillReferralAmount, "referral balance delta must match event");
|
|
417
|
+
|
|
418
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function test_fillRevertsOnInvalidWindowCurrency0() public {
|
|
422
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
423
|
+
int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
|
|
424
|
+
int24 startTick = baseTick + key.tickSpacing;
|
|
425
|
+
int24 endTick = baseTick - key.tickSpacing;
|
|
426
|
+
|
|
427
|
+
vm.expectRevert(abi.encodeWithSelector(IZoraLimitOrderBook.InvalidFillWindow.selector, startTick, endTick, true));
|
|
428
|
+
limitOrderBook.fill(key, true, startTick, endTick, 1, address(0));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function test_fillRevertsOnInvalidWindowCurrency1() public {
|
|
432
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
433
|
+
int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
|
|
434
|
+
int24 startTick = baseTick - key.tickSpacing;
|
|
435
|
+
int24 endTick = baseTick + key.tickSpacing;
|
|
436
|
+
|
|
437
|
+
vm.expectRevert(abi.encodeWithSelector(IZoraLimitOrderBook.InvalidFillWindow.selector, startTick, endTick, false));
|
|
438
|
+
limitOrderBook.fill(key, false, startTick, endTick, 1, address(0));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function test_fillRangeViaHookConsumesOrders() public {
|
|
442
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
443
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
444
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
445
|
+
|
|
446
|
+
// Create orders manually at specific ticks OUT of the money
|
|
447
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 3, 30e18);
|
|
448
|
+
uint256 totalSize;
|
|
449
|
+
for (uint256 i = 0; i < orderSizes.length; ++i) {
|
|
450
|
+
totalSize += orderSizes[i];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
_fundMaker(orderCoin, users.seller, totalSize);
|
|
454
|
+
|
|
455
|
+
vm.recordLogs();
|
|
456
|
+
vm.prank(users.seller);
|
|
457
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
458
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
459
|
+
assertEq(created.length, orderSizes.length, "expected all orders to be created");
|
|
460
|
+
_assertOpenOrderState(users.seller, orderCoin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
461
|
+
|
|
462
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
463
|
+
uint256 epochBefore = _poolEpoch(poolKeyHash);
|
|
464
|
+
|
|
465
|
+
// _movePriceBeyondTicks triggers the hook's afterSwap which automatically fills the orders
|
|
466
|
+
vm.recordLogs();
|
|
467
|
+
_movePriceBeyondTicks(created);
|
|
468
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
469
|
+
|
|
470
|
+
assertEq(fills.length, created.length, "hook fill count mismatch");
|
|
471
|
+
assertEq(_makerBalance(users.seller, orderCoin), 0, "maker balance should clear");
|
|
472
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function test_fillRangeSkipsStaleOrdersButRespectsMaxFillCount() public {
|
|
476
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
477
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
478
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
479
|
+
|
|
480
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 3, 25e18);
|
|
481
|
+
uint256 totalSize;
|
|
482
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
483
|
+
totalSize += orderSizes[i];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (orderCoin == address(0)) {
|
|
487
|
+
vm.deal(users.seller, totalSize);
|
|
488
|
+
} else {
|
|
489
|
+
deal(orderCoin, users.seller, totalSize);
|
|
490
|
+
vm.startPrank(users.seller);
|
|
491
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
492
|
+
vm.stopPrank();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
vm.recordLogs();
|
|
496
|
+
vm.prank(users.seller);
|
|
497
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
498
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
499
|
+
assertEq(created.length, orderSizes.length, "unexpected created order count");
|
|
500
|
+
_assertOpenOrderState(users.seller, orderCoin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
501
|
+
|
|
502
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
503
|
+
uint256 epochBefore = _poolEpoch(poolKeyHash);
|
|
504
|
+
|
|
505
|
+
uint256 staleIndex;
|
|
506
|
+
int24 minTick = created[0].tick;
|
|
507
|
+
for (uint256 i = 1; i < created.length; ++i) {
|
|
508
|
+
if (created[i].tick < minTick) {
|
|
509
|
+
minTick = created[i].tick;
|
|
510
|
+
staleIndex = i;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
_setOrderCreatedEpoch(created[staleIndex].orderId, uint32(epochBefore + 1));
|
|
514
|
+
|
|
515
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
516
|
+
|
|
517
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
518
|
+
uint256 maxFillCount = 2;
|
|
519
|
+
|
|
520
|
+
vm.recordLogs();
|
|
521
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, maxFillCount, address(0));
|
|
522
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
523
|
+
|
|
524
|
+
assertEq(fills.length, maxFillCount, "fill count mismatch");
|
|
525
|
+
for (uint256 i; i < fills.length; ++i) {
|
|
526
|
+
assertTrue(fills[i].orderId != created[staleIndex].orderId, "stale order should remain");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
uint256 expectedRemaining = totalSize;
|
|
530
|
+
for (uint256 i; i < fills.length; ++i) {
|
|
531
|
+
expectedRemaining -= uint256(fills[i].amountIn);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
assertEq(_makerBalance(users.seller, orderCoin), expectedRemaining, "maker balance");
|
|
535
|
+
|
|
536
|
+
for (uint256 i; i < created.length; ++i) {
|
|
537
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, created[i].tick);
|
|
538
|
+
if (i == staleIndex) {
|
|
539
|
+
assertEq(tickQueue.length, 1, "stale tick length");
|
|
540
|
+
assertEq(uint256(tickQueue.balance), expectedRemaining, "stale tick balance");
|
|
541
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, created[i].tick, key.tickSpacing), "stale tick bitmap cleared");
|
|
542
|
+
} else {
|
|
543
|
+
assertEq(tickQueue.length, 0, "cleared tick length");
|
|
544
|
+
assertEq(tickQueue.balance, 0, "cleared tick balance");
|
|
545
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, created[i].tick, key.tickSpacing), "tick bitmap still set");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
_assertEpochIncrement(poolKeyHash, epochBefore);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function test_fillBatchConsumesOrders() public {
|
|
553
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
554
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
555
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(key, isCurrency0);
|
|
556
|
+
|
|
557
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 2, 25e18);
|
|
558
|
+
uint256 totalSize;
|
|
559
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
560
|
+
totalSize += orderSizes[i];
|
|
561
|
+
}
|
|
562
|
+
if (orderCoin == address(0)) {
|
|
563
|
+
vm.deal(users.seller, totalSize);
|
|
564
|
+
} else {
|
|
565
|
+
deal(orderCoin, users.seller, totalSize);
|
|
566
|
+
vm.prank(users.seller);
|
|
567
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
vm.recordLogs();
|
|
571
|
+
vm.prank(users.seller);
|
|
572
|
+
bytes32[] memory orderIds = limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(
|
|
573
|
+
key,
|
|
574
|
+
isCurrency0,
|
|
575
|
+
orderSizes,
|
|
576
|
+
orderTicks,
|
|
577
|
+
users.seller
|
|
578
|
+
);
|
|
579
|
+
assertTrue(orderIds[0] != orderIds[1], "duplicate order ids");
|
|
580
|
+
LimitOrderTypes.LimitOrder memory order0Before = limitOrderBook.exposedOrder(orderIds[0]);
|
|
581
|
+
LimitOrderTypes.LimitOrder memory order1Before = limitOrderBook.exposedOrder(orderIds[1]);
|
|
582
|
+
assertEq(uint256(order0Before.status), uint256(LimitOrderTypes.OrderStatus.OPEN), "order0 pre status");
|
|
583
|
+
assertEq(uint256(order1Before.status), uint256(LimitOrderTypes.OrderStatus.OPEN), "order1 pre status");
|
|
584
|
+
|
|
585
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
586
|
+
assertEq(created.length, orderIds.length, "unexpected created order count");
|
|
587
|
+
bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
|
|
588
|
+
for (uint256 i; i < created.length; ++i) {
|
|
589
|
+
assertEq(created[i].poolKeyHash, poolKeyHash, "pool hash mismatch");
|
|
590
|
+
}
|
|
591
|
+
_assertOpenOrderState(users.seller, orderCoin, poolKeyHash, created, key.tickSpacing);
|
|
592
|
+
|
|
593
|
+
_movePriceBeyondTicks(created);
|
|
594
|
+
|
|
595
|
+
IZoraLimitOrderBook.OrderBatch[] memory batches = new IZoraLimitOrderBook.OrderBatch[](1);
|
|
596
|
+
batches[0] = IZoraLimitOrderBook.OrderBatch({key: key, isCurrency0: isCurrency0, orderIds: orderIds});
|
|
597
|
+
|
|
598
|
+
limitOrderBook.fill(batches, address(0));
|
|
599
|
+
LimitOrderTypes.LimitOrder memory order0After = limitOrderBook.exposedOrder(orderIds[0]);
|
|
600
|
+
LimitOrderTypes.LimitOrder memory order1After = limitOrderBook.exposedOrder(orderIds[1]);
|
|
601
|
+
assertEq(uint256(order0After.status), uint256(LimitOrderTypes.OrderStatus.FILLED), "order0 post status");
|
|
602
|
+
assertEq(uint256(order1After.status), uint256(LimitOrderTypes.OrderStatus.FILLED), "order1 post status");
|
|
603
|
+
assertEq(_makerBalance(users.seller, orderCoin), 0, "maker balance should be zero");
|
|
604
|
+
|
|
605
|
+
for (uint256 i; i < created.length; ++i) {
|
|
606
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[i].poolKeyHash, orderCoin, created[i].tick);
|
|
607
|
+
assertEq(tickQueue.length, 0, "tick queue length");
|
|
608
|
+
assertEq(tickQueue.balance, 0, "tick queue balance");
|
|
609
|
+
assertFalse(_isTickInitialized(created[i].poolKeyHash, orderCoin, created[i].tick, key.tickSpacing), "tick bitmap still set");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function test_fillBatchConsumesOrdersWithAutoFillDisabledDuringSetup() public {
|
|
614
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
615
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
616
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(key, isCurrency0);
|
|
617
|
+
|
|
618
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 2, 25e18);
|
|
619
|
+
uint256 totalSize;
|
|
620
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
621
|
+
totalSize += orderSizes[i];
|
|
622
|
+
}
|
|
623
|
+
if (orderCoin == address(0)) {
|
|
624
|
+
vm.deal(users.seller, totalSize);
|
|
625
|
+
} else {
|
|
626
|
+
deal(orderCoin, users.seller, totalSize);
|
|
627
|
+
vm.prank(users.seller);
|
|
628
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
vm.recordLogs();
|
|
632
|
+
vm.prank(users.seller);
|
|
633
|
+
bytes32[] memory orderIds = limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(
|
|
634
|
+
key,
|
|
635
|
+
isCurrency0,
|
|
636
|
+
orderSizes,
|
|
637
|
+
orderTicks,
|
|
638
|
+
users.seller
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
642
|
+
assertEq(created.length, orderIds.length, "unexpected created order count");
|
|
643
|
+
|
|
644
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
645
|
+
|
|
646
|
+
IZoraLimitOrderBook.OrderBatch[] memory batches = new IZoraLimitOrderBook.OrderBatch[](1);
|
|
647
|
+
batches[0] = IZoraLimitOrderBook.OrderBatch({key: key, isCurrency0: isCurrency0, orderIds: orderIds});
|
|
648
|
+
|
|
649
|
+
limitOrderBook.fill(batches, address(0));
|
|
650
|
+
LimitOrderTypes.LimitOrder memory order0After = limitOrderBook.exposedOrder(orderIds[0]);
|
|
651
|
+
LimitOrderTypes.LimitOrder memory order1After = limitOrderBook.exposedOrder(orderIds[1]);
|
|
652
|
+
assertEq(uint256(order0After.status), uint256(LimitOrderTypes.OrderStatus.FILLED), "order0 post status");
|
|
653
|
+
assertEq(uint256(order1After.status), uint256(LimitOrderTypes.OrderStatus.FILLED), "order1 post status");
|
|
654
|
+
assertEq(_makerBalance(users.seller, orderCoin), 0, "maker balance should be zero");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function _balanceOf(address token, address account) private view returns (uint256) {
|
|
658
|
+
if (token == address(0)) {
|
|
659
|
+
return account.balance;
|
|
660
|
+
}
|
|
661
|
+
return IERC20(token).balanceOf(account);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function _fundMaker(address asset, address maker, uint256 amount) private {
|
|
665
|
+
if (asset == address(0)) {
|
|
666
|
+
vm.deal(maker, amount);
|
|
667
|
+
} else {
|
|
668
|
+
deal(asset, maker, amount);
|
|
669
|
+
vm.prank(maker);
|
|
670
|
+
IERC20(asset).approve(address(limitOrderBook), amount);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function test_fillWithNoResidual() public {
|
|
675
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
676
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
677
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
678
|
+
|
|
679
|
+
// Create orders with sizes that divide evenly into liquidity (no residual)
|
|
680
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 100e18);
|
|
681
|
+
|
|
682
|
+
uint256 totalSize = orderSizes[0];
|
|
683
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
684
|
+
|
|
685
|
+
vm.recordLogs();
|
|
686
|
+
vm.prank(users.seller);
|
|
687
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
688
|
+
|
|
689
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
690
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
691
|
+
|
|
692
|
+
// Fill order - should handle zero residual gracefully
|
|
693
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
694
|
+
vm.recordLogs();
|
|
695
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, created.length, address(0));
|
|
696
|
+
|
|
697
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
698
|
+
assertEq(fills.length, 1, "should fill one order");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function test_fillWithReferral() public {
|
|
702
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
703
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
704
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
705
|
+
|
|
706
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 50e18);
|
|
707
|
+
|
|
708
|
+
uint256 totalSize = orderSizes[0];
|
|
709
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
710
|
+
|
|
711
|
+
vm.recordLogs();
|
|
712
|
+
vm.prank(users.seller);
|
|
713
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
714
|
+
|
|
715
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
716
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
717
|
+
|
|
718
|
+
// Fill with referral address
|
|
719
|
+
address referral = makeAddr("referral");
|
|
720
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
721
|
+
|
|
722
|
+
vm.recordLogs();
|
|
723
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, created.length, referral);
|
|
724
|
+
|
|
725
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
726
|
+
assertEq(fills.length, 1, "should fill one order");
|
|
727
|
+
assertEq(fills[0].fillReferral, referral, "referral should be set");
|
|
728
|
+
assertGt(fills[0].fillReferralAmount, 0, "referral should receive fee");
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function test_fillWithoutReferral() public {
|
|
732
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
733
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
734
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
735
|
+
|
|
736
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 50e18);
|
|
737
|
+
|
|
738
|
+
uint256 totalSize = orderSizes[0];
|
|
739
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
740
|
+
|
|
741
|
+
vm.recordLogs();
|
|
742
|
+
vm.prank(users.seller);
|
|
743
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
744
|
+
|
|
745
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
746
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
747
|
+
|
|
748
|
+
// Fill without referral (address(0))
|
|
749
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
750
|
+
|
|
751
|
+
vm.recordLogs();
|
|
752
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, created.length, address(0));
|
|
753
|
+
|
|
754
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
755
|
+
assertEq(fills.length, 1, "should fill one order");
|
|
756
|
+
assertEq(fills[0].fillReferral, address(0), "referral should be zero");
|
|
757
|
+
assertEq(fills[0].fillReferralAmount, 0, "referral should receive no fee");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function _fundAndApprove(address user, address token, uint256 amount) internal {
|
|
761
|
+
if (token == address(0)) {
|
|
762
|
+
vm.deal(user, amount);
|
|
763
|
+
} else {
|
|
764
|
+
deal(token, user, amount);
|
|
765
|
+
vm.prank(user);
|
|
766
|
+
IERC20(token).approve(address(limitOrderBook), amount);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/// @notice Tests fill() with maxFillCount=0 (line 86-88)
|
|
771
|
+
/// @dev This tests the branch: if (maxFillCount == 0) maxFillCount = getMaxFillCount();
|
|
772
|
+
function test_fill_maxFillCountZero_usesDefault() public {
|
|
773
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
774
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
775
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
776
|
+
|
|
777
|
+
// Set max fill count to 2 so we can verify default is used
|
|
778
|
+
vm.prank(users.factoryOwner);
|
|
779
|
+
limitOrderBook.setMaxFillCount(2);
|
|
780
|
+
|
|
781
|
+
// Create 5 orders that can be filled
|
|
782
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 5, 20e18);
|
|
783
|
+
uint256 totalSize;
|
|
784
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
785
|
+
totalSize += orderSizes[i];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (orderCoin == address(0)) {
|
|
789
|
+
vm.deal(users.seller, totalSize);
|
|
790
|
+
} else {
|
|
791
|
+
deal(orderCoin, users.seller, totalSize);
|
|
792
|
+
vm.startPrank(users.seller);
|
|
793
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
794
|
+
vm.stopPrank();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
vm.recordLogs();
|
|
798
|
+
vm.prank(users.seller);
|
|
799
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
800
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
801
|
+
assertEq(created.length, orderSizes.length, "unexpected created order count");
|
|
802
|
+
|
|
803
|
+
// Move price past orders to make them fillable, but disable auto-fill so manual fill can consume them
|
|
804
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
805
|
+
|
|
806
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
807
|
+
|
|
808
|
+
// Call fill with maxFillCount = 0 (should use default of 2)
|
|
809
|
+
vm.recordLogs();
|
|
810
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 0, address(0));
|
|
811
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
812
|
+
|
|
813
|
+
// Should fill 2 orders (the default max count)
|
|
814
|
+
assertEq(fills.length, 2, "should fill default max count of 2 orders");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/// @notice Tests batch fill with empty orderIds array (line 134)
|
|
818
|
+
/// @dev This tests the branch: if (batch.orderIds.length != 0)
|
|
819
|
+
function test_batchFill_emptyOrderIds_skips() public {
|
|
820
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
821
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
822
|
+
|
|
823
|
+
// Create batch with empty orderIds
|
|
824
|
+
IZoraLimitOrderBook.OrderBatch[] memory batches = new IZoraLimitOrderBook.OrderBatch[](2);
|
|
825
|
+
batches[0] = IZoraLimitOrderBook.OrderBatch({
|
|
826
|
+
key: key,
|
|
827
|
+
isCurrency0: isCurrency0,
|
|
828
|
+
orderIds: new bytes32[](0) // Empty array - should be skipped
|
|
829
|
+
});
|
|
830
|
+
batches[1] = IZoraLimitOrderBook.OrderBatch({
|
|
831
|
+
key: key,
|
|
832
|
+
isCurrency0: isCurrency0,
|
|
833
|
+
orderIds: new bytes32[](0) // Empty array - should be skipped
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
vm.recordLogs();
|
|
837
|
+
limitOrderBook.fill(batches, address(0));
|
|
838
|
+
|
|
839
|
+
// Should complete without reverting (skips empty batches)
|
|
840
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
841
|
+
assertEq(fills.length, 0, "should not fill anything from empty batches");
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/// @notice Tests batch fill with mixed empty and non-empty batches
|
|
845
|
+
function test_batchFill_mixedEmptyAndNonEmpty_processesNonEmpty() public {
|
|
846
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
847
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
848
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
849
|
+
|
|
850
|
+
// Create one order
|
|
851
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
|
|
852
|
+
uint256 totalSize = orderSizes[0];
|
|
853
|
+
|
|
854
|
+
if (orderCoin == address(0)) {
|
|
855
|
+
vm.deal(users.seller, totalSize);
|
|
856
|
+
} else {
|
|
857
|
+
deal(orderCoin, users.seller, totalSize);
|
|
858
|
+
vm.startPrank(users.seller);
|
|
859
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
860
|
+
vm.stopPrank();
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
vm.recordLogs();
|
|
864
|
+
vm.prank(users.seller);
|
|
865
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
866
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
867
|
+
assertEq(created.length, 1, "should create 1 order");
|
|
868
|
+
|
|
869
|
+
// Move price past order to make it fillable, with auto-fill disabled
|
|
870
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
871
|
+
|
|
872
|
+
// Create batches: first empty, second with order
|
|
873
|
+
IZoraLimitOrderBook.OrderBatch[] memory batches = new IZoraLimitOrderBook.OrderBatch[](2);
|
|
874
|
+
batches[0] = IZoraLimitOrderBook.OrderBatch({
|
|
875
|
+
key: key,
|
|
876
|
+
isCurrency0: isCurrency0,
|
|
877
|
+
orderIds: new bytes32[](0) // Empty - should skip
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
881
|
+
orderIds[0] = created[0].orderId;
|
|
882
|
+
batches[1] = IZoraLimitOrderBook.OrderBatch({
|
|
883
|
+
key: key,
|
|
884
|
+
isCurrency0: isCurrency0,
|
|
885
|
+
orderIds: orderIds // Non-empty - should process
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
vm.recordLogs();
|
|
889
|
+
limitOrderBook.fill(batches, address(0));
|
|
890
|
+
|
|
891
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
892
|
+
assertEq(fills.length, 1, "should fill the one order from non-empty batch");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/// @notice Verifies that once the pool tick crosses past the order boundary, the order
|
|
896
|
+
/// gets filled and coinOut is the counter asset (not same as coinIn).
|
|
897
|
+
function test_fillSucceedsOnceCrossed_rangeWalk() public {
|
|
898
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
899
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
900
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
901
|
+
|
|
902
|
+
// Create a single order out-of-the-money
|
|
903
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 50e18);
|
|
904
|
+
uint256 totalSize = orderSizes[0];
|
|
905
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
906
|
+
|
|
907
|
+
vm.recordLogs();
|
|
908
|
+
vm.prank(users.seller);
|
|
909
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
910
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
911
|
+
|
|
912
|
+
bytes32 orderId = created[0].orderId;
|
|
913
|
+
|
|
914
|
+
// Move price past the order to make it crossed (using real swap, auto-fill disabled)
|
|
915
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
916
|
+
|
|
917
|
+
// Now fill manually - should work since order is crossed
|
|
918
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
919
|
+
vm.recordLogs();
|
|
920
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 10, address(0));
|
|
921
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
922
|
+
|
|
923
|
+
// Should fill
|
|
924
|
+
assertEq(fills.length, 1, "should fill after crossing");
|
|
925
|
+
|
|
926
|
+
// Verify coinIn != coinOut (counter asset received)
|
|
927
|
+
assertTrue(fills[0].coinIn != fills[0].coinOut, "coinIn should differ from coinOut");
|
|
928
|
+
assertEq(fills[0].coinIn, orderCoin, "coinIn should be the order coin");
|
|
929
|
+
assertGt(fills[0].amountOut, 0, "should have non-zero amountOut");
|
|
930
|
+
|
|
931
|
+
// Verify order is now FILLED
|
|
932
|
+
LimitOrderTypes.LimitOrder memory orderAfter = limitOrderBook.exposedOrder(orderId);
|
|
933
|
+
assertEq(uint256(orderAfter.status), uint256(LimitOrderTypes.OrderStatus.FILLED), "order should be FILLED");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/// @notice Same as above but using the batch fill path.
|
|
937
|
+
function test_fillSucceedsOnceCrossed_batchFill() public {
|
|
938
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
939
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
940
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
941
|
+
|
|
942
|
+
// Create a single order out-of-the-money
|
|
943
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 50e18);
|
|
944
|
+
uint256 totalSize = orderSizes[0];
|
|
945
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
946
|
+
|
|
947
|
+
vm.recordLogs();
|
|
948
|
+
vm.prank(users.seller);
|
|
949
|
+
bytes32[] memory orderIds = limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(
|
|
950
|
+
key,
|
|
951
|
+
isCurrency0,
|
|
952
|
+
orderSizes,
|
|
953
|
+
orderTicks,
|
|
954
|
+
users.seller
|
|
955
|
+
);
|
|
956
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
957
|
+
|
|
958
|
+
// Move price past the order to make it crossed
|
|
959
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
960
|
+
|
|
961
|
+
// Fill via batch - should work since order is crossed
|
|
962
|
+
IZoraLimitOrderBook.OrderBatch[] memory batches = new IZoraLimitOrderBook.OrderBatch[](1);
|
|
963
|
+
batches[0] = IZoraLimitOrderBook.OrderBatch({key: key, isCurrency0: isCurrency0, orderIds: orderIds});
|
|
964
|
+
|
|
965
|
+
vm.recordLogs();
|
|
966
|
+
limitOrderBook.fill(batches, address(0));
|
|
967
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
968
|
+
|
|
969
|
+
// Should fill
|
|
970
|
+
assertEq(fills.length, 1, "should fill after crossing via batch");
|
|
971
|
+
assertTrue(fills[0].coinIn != fills[0].coinOut, "coinIn should differ from coinOut");
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/// @notice Tests that after crossing, fill correctly pays out the counter asset.
|
|
975
|
+
/// This is a simpler version that just verifies correct payout after real price movement.
|
|
976
|
+
function test_fillAfterCrossing_correctPayout() public {
|
|
977
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
978
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
979
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
980
|
+
address counterCoin = isCurrency0 ? Currency.unwrap(key.currency1) : Currency.unwrap(key.currency0);
|
|
981
|
+
|
|
982
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 50e18);
|
|
983
|
+
uint256 totalSize = orderSizes[0];
|
|
984
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
985
|
+
|
|
986
|
+
vm.recordLogs();
|
|
987
|
+
vm.prank(users.seller);
|
|
988
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
989
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
990
|
+
|
|
991
|
+
// Move price past the order
|
|
992
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
993
|
+
|
|
994
|
+
// Fill
|
|
995
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
996
|
+
vm.recordLogs();
|
|
997
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 10, address(0));
|
|
998
|
+
FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
|
|
999
|
+
|
|
1000
|
+
assertEq(fills.length, 1, "should fill");
|
|
1001
|
+
assertEq(fills[0].coinIn, orderCoin, "coinIn should be order coin");
|
|
1002
|
+
assertEq(fills[0].coinOut, counterCoin, "coinOut should be counter coin");
|
|
1003
|
+
assertGt(fills[0].amountOut, 0, "should receive counter asset");
|
|
1004
|
+
}
|
|
1005
|
+
}
|