@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,653 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
|
+
|
|
6
|
+
import {IZoraLimitOrderBook} from "../src/IZoraLimitOrderBook.sol";
|
|
7
|
+
import {LimitOrderTypes} from "../src/libs/LimitOrderTypes.sol";
|
|
8
|
+
|
|
9
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
10
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
11
|
+
|
|
12
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
13
|
+
import {Vm} from "forge-std/Vm.sol";
|
|
14
|
+
|
|
15
|
+
contract LimitOrderWithdrawTest is BaseTest {
|
|
16
|
+
function test_withdrawOrdersCancelsAll() public {
|
|
17
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
18
|
+
|
|
19
|
+
vm.recordLogs();
|
|
20
|
+
_executeSingleHopSwapWithLimitOrders(users.seller, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
21
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
22
|
+
assertGt(created.length, 0, "expected orders to be created");
|
|
23
|
+
_assertOpenOrderState(users.seller, created[0].coin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
24
|
+
|
|
25
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
26
|
+
address orderCoin = created[0].coin;
|
|
27
|
+
uint256 tokenBalanceBefore = _balanceOf(orderCoin, users.seller);
|
|
28
|
+
uint256 epochBefore = _poolEpoch(created[0].poolKeyHash);
|
|
29
|
+
uint256 totalOrderSize = _sumOrderSizes(created);
|
|
30
|
+
|
|
31
|
+
vm.recordLogs();
|
|
32
|
+
vm.prank(users.seller);
|
|
33
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
34
|
+
UpdatedOrderLog[] memory updates = _decodeUpdatedLogs(vm.getRecordedLogs());
|
|
35
|
+
|
|
36
|
+
uint256 cancelled;
|
|
37
|
+
for (uint256 i; i < updates.length; ++i) {
|
|
38
|
+
if (updates[i].maker != users.seller) continue;
|
|
39
|
+
if (!updates[i].isCancelled) continue;
|
|
40
|
+
++cancelled;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
assertEq(cancelled, orderIds.length, "all orders should cancel");
|
|
44
|
+
|
|
45
|
+
uint256 tokenBalanceAfter = _balanceOf(orderCoin, users.seller);
|
|
46
|
+
assertApproxEqAbs(tokenBalanceAfter, tokenBalanceBefore + totalOrderSize, 5, "token refund mismatch");
|
|
47
|
+
assertEq(_poolEpoch(created[0].poolKeyHash), epochBefore, "withdraw should not change epoch");
|
|
48
|
+
|
|
49
|
+
for (uint256 i; i < created.length; ++i) {
|
|
50
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[i].poolKeyHash, orderCoin, created[i].tick);
|
|
51
|
+
assertEq(tickQueue.length, 0, "tick queue length");
|
|
52
|
+
assertEq(tickQueue.balance, 0, "tick queue balance");
|
|
53
|
+
assertFalse(_isTickInitialized(created[i].poolKeyHash, orderCoin, created[i].tick, key.tickSpacing), "tick bitmap still set");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function test_withdrawOrdersRevertsForMixedCoins() public {
|
|
58
|
+
PoolKey memory creatorKey = creatorCoin.getPoolKey();
|
|
59
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
60
|
+
|
|
61
|
+
vm.recordLogs();
|
|
62
|
+
_executeSingleHopSwapWithLimitOrders(users.seller, creatorKey, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
63
|
+
CreatedOrderLog[] memory creatorOrders = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
64
|
+
assertGt(creatorOrders.length, 0, "expected creator orders");
|
|
65
|
+
|
|
66
|
+
// Content pool requires multi-hop routing: ZORA → SharedToken → ContentCoin
|
|
67
|
+
vm.recordLogs();
|
|
68
|
+
PoolKey[] memory contentRoute = new PoolKey[](2);
|
|
69
|
+
contentRoute[0] = creatorKey; // First hop: ZORA → SharedToken
|
|
70
|
+
contentRoute[1] = contentKey; // Second hop: SharedToken → ContentCoin
|
|
71
|
+
_executeMultiHopSwapWithLimitOrders(users.seller, contentRoute, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
72
|
+
CreatedOrderLog[] memory contentOrders = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
73
|
+
assertGt(contentOrders.length, 0, "expected content orders");
|
|
74
|
+
|
|
75
|
+
bytes32[] memory mixed = new bytes32[](creatorOrders.length + contentOrders.length);
|
|
76
|
+
for (uint256 i; i < creatorOrders.length; ++i) {
|
|
77
|
+
mixed[i] = creatorOrders[i].orderId;
|
|
78
|
+
}
|
|
79
|
+
for (uint256 i; i < contentOrders.length; ++i) {
|
|
80
|
+
mixed[creatorOrders.length + i] = contentOrders[i].orderId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// First content order will trigger the mismatch
|
|
84
|
+
bytes32 mismatchOrderId = contentOrders[0].orderId;
|
|
85
|
+
address expectedCoin = creatorOrders[0].coin;
|
|
86
|
+
address actualCoin = contentOrders[0].coin;
|
|
87
|
+
|
|
88
|
+
vm.expectRevert(abi.encodeWithSelector(IZoraLimitOrderBook.CoinMismatch.selector, mismatchOrderId, expectedCoin, actualCoin));
|
|
89
|
+
vm.prank(users.seller);
|
|
90
|
+
limitOrderBook.withdraw(mixed, expectedCoin, 0, users.seller);
|
|
91
|
+
|
|
92
|
+
_assertOpenOrderState(users.seller, creatorOrders[0].coin, creatorOrders[0].poolKeyHash, creatorOrders, creatorKey.tickSpacing);
|
|
93
|
+
_assertOpenOrderState(users.seller, contentOrders[0].coin, contentOrders[0].poolKeyHash, contentOrders, contentKey.tickSpacing);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function test_withdrawOrdersRevertsForRecipientZero() public {
|
|
97
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
98
|
+
|
|
99
|
+
vm.recordLogs();
|
|
100
|
+
_executeSingleHopSwapWithLimitOrders(users.seller, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
101
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
102
|
+
assertGt(created.length, 0, "expected orders to be created");
|
|
103
|
+
|
|
104
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
105
|
+
address orderCoin = created[0].coin;
|
|
106
|
+
|
|
107
|
+
vm.expectRevert(IZoraLimitOrderBook.AddressZero.selector);
|
|
108
|
+
vm.prank(users.seller);
|
|
109
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, address(0));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function test_withdrawOrdersRevertsForNonMaker() public {
|
|
113
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
114
|
+
|
|
115
|
+
vm.recordLogs();
|
|
116
|
+
_executeSingleHopSwapWithLimitOrders(users.seller, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
117
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
118
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
119
|
+
address orderCoin = created[0].coin;
|
|
120
|
+
|
|
121
|
+
vm.expectRevert(IZoraLimitOrderBook.OrderNotMaker.selector);
|
|
122
|
+
vm.prank(users.buyer);
|
|
123
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.buyer);
|
|
124
|
+
|
|
125
|
+
_assertOpenOrderState(users.seller, created[0].coin, created[0].poolKeyHash, created, key.tickSpacing);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function test_withdrawOrdersRevertsOnInvalidOrder() public {
|
|
129
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
130
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
131
|
+
|
|
132
|
+
uint256[] memory orderSizes = new uint256[](1);
|
|
133
|
+
orderSizes[0] = 75e18;
|
|
134
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 1);
|
|
135
|
+
(CreatedOrderLog[] memory created, address orderCoin) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
136
|
+
|
|
137
|
+
bytes32[] memory mixed = new bytes32[](2);
|
|
138
|
+
mixed[0] = created[0].orderId;
|
|
139
|
+
mixed[1] = bytes32(uint256(123));
|
|
140
|
+
|
|
141
|
+
vm.expectRevert(IZoraLimitOrderBook.InvalidOrder.selector);
|
|
142
|
+
vm.prank(users.seller);
|
|
143
|
+
limitOrderBook.withdraw(mixed, orderCoin, 0, users.seller);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function test_withdrawOrdersRevertsOnClosedOrder() public {
|
|
147
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
148
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
149
|
+
|
|
150
|
+
uint256[] memory orderSizes = new uint256[](1);
|
|
151
|
+
orderSizes[0] = 90e18;
|
|
152
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 1);
|
|
153
|
+
(CreatedOrderLog[] memory created, address orderCoin) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
154
|
+
|
|
155
|
+
limitOrderBook.forceOrderStatus(created[0].orderId, LimitOrderTypes.OrderStatus.INACTIVE);
|
|
156
|
+
|
|
157
|
+
bytes32[] memory ids = new bytes32[](1);
|
|
158
|
+
ids[0] = created[0].orderId;
|
|
159
|
+
|
|
160
|
+
vm.expectRevert(IZoraLimitOrderBook.OrderClosed.selector);
|
|
161
|
+
vm.prank(users.seller);
|
|
162
|
+
limitOrderBook.withdraw(ids, orderCoin, 0, users.seller);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function test_cancelOrderFullCancellationMarksInactive() public {
|
|
166
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
167
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
168
|
+
|
|
169
|
+
uint256[] memory orderSizes = new uint256[](1);
|
|
170
|
+
orderSizes[0] = 500e18;
|
|
171
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 1);
|
|
172
|
+
(CreatedOrderLog[] memory created, address orderCoin) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
173
|
+
|
|
174
|
+
bytes32[] memory ids = new bytes32[](1);
|
|
175
|
+
ids[0] = created[0].orderId;
|
|
176
|
+
|
|
177
|
+
vm.prank(users.seller);
|
|
178
|
+
limitOrderBook.withdraw(ids, orderCoin, 0, users.seller);
|
|
179
|
+
|
|
180
|
+
LimitOrderTypes.LimitOrder memory orderState = limitOrderBook.exposedOrder(created[0].orderId);
|
|
181
|
+
assertEq(uint8(orderState.status), uint8(LimitOrderTypes.OrderStatus.INACTIVE), "order should be marked inactive");
|
|
182
|
+
|
|
183
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, created[0].tick);
|
|
184
|
+
assertEq(tickQueue.length, 0, "tick queue length");
|
|
185
|
+
assertEq(tickQueue.balance, 0, "tick queue balance");
|
|
186
|
+
assertFalse(_isTickInitialized(created[0].poolKeyHash, orderCoin, created[0].tick, key.tickSpacing), "tick bitmap should clear");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function test_withdrawWithMinAmountOutStopsEarly() public {
|
|
190
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
191
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
192
|
+
|
|
193
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
194
|
+
orderSizes[0] = 100e18;
|
|
195
|
+
orderSizes[1] = 200e18;
|
|
196
|
+
orderSizes[2] = 300e18;
|
|
197
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, orderSizes.length, 1);
|
|
198
|
+
(CreatedOrderLog[] memory created, address orderCoin) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
199
|
+
|
|
200
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
201
|
+
uint256 tokenBalanceBefore = _balanceOf(orderCoin, users.seller);
|
|
202
|
+
|
|
203
|
+
// Use actual created order sizes (which may differ due to liquidity rounding)
|
|
204
|
+
uint256 actualFirstTwo = created[0].size + created[1].size;
|
|
205
|
+
|
|
206
|
+
vm.recordLogs();
|
|
207
|
+
vm.prank(users.seller);
|
|
208
|
+
limitOrderBook.withdraw(orderIds, orderCoin, actualFirstTwo, users.seller);
|
|
209
|
+
UpdatedOrderLog[] memory updates = _decodeUpdatedLogs(vm.getRecordedLogs());
|
|
210
|
+
|
|
211
|
+
// Should have cancelled exactly 2 orders
|
|
212
|
+
uint256 cancelled;
|
|
213
|
+
for (uint256 i; i < updates.length; ++i) {
|
|
214
|
+
if (updates[i].maker != users.seller) continue;
|
|
215
|
+
if (updates[i].isCancelled) ++cancelled;
|
|
216
|
+
}
|
|
217
|
+
assertEq(cancelled, 2, "should cancel first two orders");
|
|
218
|
+
|
|
219
|
+
// Third order should still be open
|
|
220
|
+
LimitOrderTypes.LimitOrder memory thirdOrder = limitOrderBook.exposedOrder(created[2].orderId);
|
|
221
|
+
assertEq(uint8(thirdOrder.status), uint8(LimitOrderTypes.OrderStatus.OPEN), "third order should still be open");
|
|
222
|
+
|
|
223
|
+
uint256 tokenBalanceAfter = _balanceOf(orderCoin, users.seller);
|
|
224
|
+
assertApproxEqAbs(tokenBalanceAfter, tokenBalanceBefore + actualFirstTwo, 5, "token refund should match first two orders");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function test_withdrawWithMinAmountOutRevertsIfNotReached() public {
|
|
228
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
229
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
230
|
+
|
|
231
|
+
uint256[] memory orderSizes = new uint256[](2);
|
|
232
|
+
orderSizes[0] = 100e18;
|
|
233
|
+
orderSizes[1] = 200e18;
|
|
234
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, orderSizes.length, 1);
|
|
235
|
+
(CreatedOrderLog[] memory created, address orderCoin) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
236
|
+
|
|
237
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
238
|
+
|
|
239
|
+
// Use actual created sizes and request slightly more
|
|
240
|
+
uint256 actualTotal = created[0].size + created[1].size;
|
|
241
|
+
uint256 minAmountOut = actualTotal + 1;
|
|
242
|
+
|
|
243
|
+
vm.expectRevert(abi.encodeWithSelector(IZoraLimitOrderBook.MinAmountNotReached.selector, actualTotal, minAmountOut));
|
|
244
|
+
vm.prank(users.seller);
|
|
245
|
+
limitOrderBook.withdraw(orderIds, orderCoin, minAmountOut, users.seller);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function test_withdrawWithZeroMinAmountOutCancelsAll() public {
|
|
249
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
250
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
251
|
+
|
|
252
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
253
|
+
orderSizes[0] = 100e18;
|
|
254
|
+
orderSizes[1] = 200e18;
|
|
255
|
+
orderSizes[2] = 300e18;
|
|
256
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, orderSizes.length, 1);
|
|
257
|
+
(CreatedOrderLog[] memory created, address orderCoin) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
258
|
+
|
|
259
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
260
|
+
uint256 tokenBalanceBefore = _balanceOf(orderCoin, users.seller);
|
|
261
|
+
uint256 totalSize = orderSizes[0] + orderSizes[1] + orderSizes[2];
|
|
262
|
+
|
|
263
|
+
vm.recordLogs();
|
|
264
|
+
vm.prank(users.seller);
|
|
265
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
266
|
+
UpdatedOrderLog[] memory updates = _decodeUpdatedLogs(vm.getRecordedLogs());
|
|
267
|
+
|
|
268
|
+
// All orders should be cancelled
|
|
269
|
+
uint256 cancelled;
|
|
270
|
+
for (uint256 i; i < updates.length; ++i) {
|
|
271
|
+
if (updates[i].maker != users.seller) continue;
|
|
272
|
+
if (updates[i].isCancelled) ++cancelled;
|
|
273
|
+
}
|
|
274
|
+
assertEq(cancelled, 3, "all orders should cancel");
|
|
275
|
+
|
|
276
|
+
uint256 tokenBalanceAfter = _balanceOf(orderCoin, users.seller);
|
|
277
|
+
assertApproxEqAbs(tokenBalanceAfter, tokenBalanceBefore + totalSize, 5, "token refund should match total");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function test_withdrawRevertsOnEmptyOrderIds() public {
|
|
281
|
+
bytes32[] memory orderIds = new bytes32[](0);
|
|
282
|
+
|
|
283
|
+
vm.expectRevert(IZoraLimitOrderBook.InvalidOrder.selector);
|
|
284
|
+
vm.prank(users.seller);
|
|
285
|
+
limitOrderBook.withdraw(orderIds, address(0), 0, users.seller);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// @notice Tests that withdrawing filled orders reverts appropriately
|
|
289
|
+
function test_withdraw_filledOrdersReverts() public {
|
|
290
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
291
|
+
|
|
292
|
+
vm.recordLogs();
|
|
293
|
+
_executeSingleHopSwapWithLimitOrders(users.seller, key, DEFAULT_LIMIT_ORDER_AMOUNT, _defaultMultiples(), _defaultPercentages());
|
|
294
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
295
|
+
address orderCoin = created[0].coin;
|
|
296
|
+
|
|
297
|
+
// Manually mark first order as filled
|
|
298
|
+
limitOrderBook.forceOrderStatus(created[0].orderId, LimitOrderTypes.OrderStatus.FILLED);
|
|
299
|
+
|
|
300
|
+
// Try to withdraw filled order - should revert
|
|
301
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
302
|
+
orderIds[0] = created[0].orderId;
|
|
303
|
+
|
|
304
|
+
vm.prank(users.seller);
|
|
305
|
+
vm.expectRevert(IZoraLimitOrderBook.OrderClosed.selector);
|
|
306
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// @notice Tests that bitmap is cleaned when last order at tick is withdrawn
|
|
310
|
+
function test_withdraw_lastOrderAtTick_cleansBitmap() public {
|
|
311
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
312
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
313
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
314
|
+
|
|
315
|
+
// Create single order
|
|
316
|
+
(uint256[] memory sizes, int24[] memory ticks) = _buildDeterministicOrders(key, isCurrency0, 1, 50e18);
|
|
317
|
+
_fundAndApprove(users.seller, orderCoin, sizes[0]);
|
|
318
|
+
|
|
319
|
+
vm.recordLogs();
|
|
320
|
+
vm.prank(users.seller);
|
|
321
|
+
limitOrderBook.create{value: orderCoin == address(0) ? sizes[0] : 0}(key, isCurrency0, sizes, ticks, users.seller);
|
|
322
|
+
|
|
323
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
324
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
325
|
+
int24 tick = ticks[0];
|
|
326
|
+
|
|
327
|
+
// Verify bitmap set
|
|
328
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, tick, key.tickSpacing), "tick should be initialized");
|
|
329
|
+
|
|
330
|
+
// Withdraw order
|
|
331
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
332
|
+
orderIds[0] = created[0].orderId;
|
|
333
|
+
vm.prank(users.seller);
|
|
334
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
335
|
+
|
|
336
|
+
// Bitmap should be cleared
|
|
337
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, tick, key.tickSpacing), "tick should be cleared");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function test_withdraw_filledOrder_makerBalanceUnchanged() public {
|
|
341
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
342
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
343
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
344
|
+
|
|
345
|
+
// Create 1 order
|
|
346
|
+
uint256[] memory orderSizes = new uint256[](1);
|
|
347
|
+
orderSizes[0] = 100 ether;
|
|
348
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 1);
|
|
349
|
+
(CreatedOrderLog[] memory created, ) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
350
|
+
|
|
351
|
+
uint256 balanceAfterCreate = _makerBalance(users.seller, orderCoin);
|
|
352
|
+
assertGt(balanceAfterCreate, 0, "balance should be positive after create");
|
|
353
|
+
|
|
354
|
+
// Mark order as filled
|
|
355
|
+
limitOrderBook.forceOrderStatus(created[0].orderId, LimitOrderTypes.OrderStatus.FILLED);
|
|
356
|
+
|
|
357
|
+
// Try to withdraw filled order - should revert without touching balance
|
|
358
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
359
|
+
orderIds[0] = created[0].orderId;
|
|
360
|
+
|
|
361
|
+
vm.prank(users.seller);
|
|
362
|
+
vm.expectRevert(IZoraLimitOrderBook.OrderClosed.selector);
|
|
363
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
364
|
+
|
|
365
|
+
// Balance should be unchanged (transaction reverted)
|
|
366
|
+
assertEq(_makerBalance(users.seller, orderCoin), balanceAfterCreate, "balance should be unchanged after revert");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function test_withdraw_mixedFilledAndOpen_revertsOnFirstFilled() public {
|
|
370
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
371
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
372
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
373
|
+
|
|
374
|
+
// Create 3 orders
|
|
375
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
376
|
+
orderSizes[0] = 100 ether;
|
|
377
|
+
orderSizes[1] = 200 ether;
|
|
378
|
+
orderSizes[2] = 300 ether;
|
|
379
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 3, 1);
|
|
380
|
+
(CreatedOrderLog[] memory created, ) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
381
|
+
|
|
382
|
+
uint256 balanceAfterCreate = _makerBalance(users.seller, orderCoin);
|
|
383
|
+
|
|
384
|
+
// Mark second order as filled
|
|
385
|
+
limitOrderBook.forceOrderStatus(created[1].orderId, LimitOrderTypes.OrderStatus.FILLED);
|
|
386
|
+
|
|
387
|
+
// Try to withdraw all 3 orders - should revert when hitting the filled one
|
|
388
|
+
bytes32[] memory orderIds = new bytes32[](3);
|
|
389
|
+
orderIds[0] = created[0].orderId;
|
|
390
|
+
orderIds[1] = created[1].orderId;
|
|
391
|
+
orderIds[2] = created[2].orderId;
|
|
392
|
+
|
|
393
|
+
vm.prank(users.seller);
|
|
394
|
+
vm.expectRevert(IZoraLimitOrderBook.OrderClosed.selector);
|
|
395
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
396
|
+
|
|
397
|
+
// Balance should be unchanged (entire transaction reverted, including first order)
|
|
398
|
+
assertEq(_makerBalance(users.seller, orderCoin), balanceAfterCreate, "balance unchanged after revert");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function test_withdraw_withMinAmountOut_filledOrder_revertsBeforeReachingThreshold() public {
|
|
402
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
403
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
404
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
405
|
+
|
|
406
|
+
// Create 2 orders
|
|
407
|
+
uint256[] memory orderSizes = new uint256[](2);
|
|
408
|
+
orderSizes[0] = 100 ether;
|
|
409
|
+
orderSizes[1] = 200 ether;
|
|
410
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 2, 1);
|
|
411
|
+
(CreatedOrderLog[] memory created, ) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
412
|
+
|
|
413
|
+
uint256 balanceAfterCreate = _makerBalance(users.seller, orderCoin);
|
|
414
|
+
|
|
415
|
+
// Mark first order as filled
|
|
416
|
+
limitOrderBook.forceOrderStatus(created[0].orderId, LimitOrderTypes.OrderStatus.FILLED);
|
|
417
|
+
|
|
418
|
+
// Try to withdraw both with minAmountOut - should revert on first filled order
|
|
419
|
+
bytes32[] memory orderIds = new bytes32[](2);
|
|
420
|
+
orderIds[0] = created[0].orderId;
|
|
421
|
+
orderIds[1] = created[1].orderId;
|
|
422
|
+
|
|
423
|
+
uint256 minAmountOut = created[1].size; // Even though we want the second order
|
|
424
|
+
|
|
425
|
+
vm.prank(users.seller);
|
|
426
|
+
vm.expectRevert(IZoraLimitOrderBook.OrderClosed.selector);
|
|
427
|
+
limitOrderBook.withdraw(orderIds, orderCoin, minAmountOut, users.seller);
|
|
428
|
+
|
|
429
|
+
// Balance unchanged
|
|
430
|
+
assertEq(_makerBalance(users.seller, orderCoin), balanceAfterCreate, "balance unchanged after revert");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function test_makerBalanceUpdated_emittedOnWithdraw() public {
|
|
434
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
435
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
436
|
+
|
|
437
|
+
uint256[] memory orderSizes = new uint256[](2);
|
|
438
|
+
orderSizes[0] = 100 ether;
|
|
439
|
+
orderSizes[1] = 200 ether;
|
|
440
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, orderSizes.length, 1);
|
|
441
|
+
(CreatedOrderLog[] memory created, address orderCoin) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
442
|
+
|
|
443
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
444
|
+
|
|
445
|
+
vm.recordLogs();
|
|
446
|
+
vm.prank(users.seller);
|
|
447
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
448
|
+
|
|
449
|
+
// Find MakerBalanceUpdated events
|
|
450
|
+
Vm.Log[] memory logs = vm.getRecordedLogs();
|
|
451
|
+
uint256 eventCount;
|
|
452
|
+
uint256 finalBalance;
|
|
453
|
+
for (uint256 i; i < logs.length; ++i) {
|
|
454
|
+
if (logs[i].topics[0] == IZoraLimitOrderBook.MakerBalanceUpdated.selector) {
|
|
455
|
+
address eventMaker = address(uint160(uint256(logs[i].topics[1])));
|
|
456
|
+
address eventCoin = address(uint160(uint256(logs[i].topics[2])));
|
|
457
|
+
if (eventMaker == users.seller && eventCoin == orderCoin) {
|
|
458
|
+
finalBalance = abi.decode(logs[i].data, (uint256));
|
|
459
|
+
++eventCount;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Should have 2 events (one per order cancelled)
|
|
465
|
+
assertEq(eventCount, 2, "should emit 2 MakerBalanceUpdated events");
|
|
466
|
+
assertEq(finalBalance, 0, "final balance should be zero");
|
|
467
|
+
assertEq(_makerBalance(users.seller, orderCoin), 0, "maker balance should be zero");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function test_withdraw_ethBackedOrders() public {
|
|
471
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
472
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
473
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
474
|
+
|
|
475
|
+
// Skip if this pool doesn't use ETH
|
|
476
|
+
if (orderCoin != address(0)) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Create ETH-backed orders
|
|
481
|
+
uint256[] memory orderSizes = new uint256[](2);
|
|
482
|
+
orderSizes[0] = 1 ether;
|
|
483
|
+
orderSizes[1] = 2 ether;
|
|
484
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, orderSizes.length, 1);
|
|
485
|
+
(CreatedOrderLog[] memory created, ) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
486
|
+
|
|
487
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
488
|
+
uint256 ethBalanceBefore = users.seller.balance;
|
|
489
|
+
uint256 totalSize = created[0].size + created[1].size;
|
|
490
|
+
|
|
491
|
+
// Withdraw ETH-backed orders
|
|
492
|
+
vm.prank(users.seller);
|
|
493
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
494
|
+
|
|
495
|
+
// Verify ETH was refunded
|
|
496
|
+
uint256 ethBalanceAfter = users.seller.balance;
|
|
497
|
+
assertApproxEqAbs(ethBalanceAfter, ethBalanceBefore + totalSize, 5, "ETH refund mismatch");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function test_withdraw_ethBackedOrders_withMinAmountOut() public {
|
|
501
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
502
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
503
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
504
|
+
|
|
505
|
+
// Skip if this pool doesn't use ETH
|
|
506
|
+
if (orderCoin != address(0)) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Create 3 ETH-backed orders
|
|
511
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
512
|
+
orderSizes[0] = 1 ether;
|
|
513
|
+
orderSizes[1] = 2 ether;
|
|
514
|
+
orderSizes[2] = 3 ether;
|
|
515
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, orderSizes.length, 1);
|
|
516
|
+
(CreatedOrderLog[] memory created, ) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
517
|
+
|
|
518
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
519
|
+
uint256 ethBalanceBefore = users.seller.balance;
|
|
520
|
+
uint256 firstTwoSize = created[0].size + created[1].size;
|
|
521
|
+
|
|
522
|
+
// Withdraw with minAmountOut - should stop after first two orders
|
|
523
|
+
vm.prank(users.seller);
|
|
524
|
+
limitOrderBook.withdraw(orderIds, orderCoin, firstTwoSize, users.seller);
|
|
525
|
+
|
|
526
|
+
// Verify first two orders' ETH was refunded
|
|
527
|
+
uint256 ethBalanceAfter = users.seller.balance;
|
|
528
|
+
assertApproxEqAbs(ethBalanceAfter, ethBalanceBefore + firstTwoSize, 5, "ETH refund mismatch");
|
|
529
|
+
|
|
530
|
+
// Third order should still be open
|
|
531
|
+
LimitOrderTypes.LimitOrder memory thirdOrder = limitOrderBook.exposedOrder(created[2].orderId);
|
|
532
|
+
assertEq(uint8(thirdOrder.status), uint8(LimitOrderTypes.OrderStatus.OPEN), "third order should still be open");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function _balanceOf(address token, address account) private view returns (uint256) {
|
|
536
|
+
if (token == address(0)) {
|
|
537
|
+
return account.balance;
|
|
538
|
+
}
|
|
539
|
+
return IERC20(token).balanceOf(account);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function _createOrders(
|
|
543
|
+
address maker,
|
|
544
|
+
PoolKey memory key,
|
|
545
|
+
bool isCurrency0,
|
|
546
|
+
uint256[] memory orderSizes,
|
|
547
|
+
int24[] memory orderTicks
|
|
548
|
+
) private returns (CreatedOrderLog[] memory created, address orderCoin) {
|
|
549
|
+
require(orderSizes.length == orderTicks.length, "order configuration mismatch");
|
|
550
|
+
|
|
551
|
+
orderCoin = _orderCoin(key, isCurrency0);
|
|
552
|
+
uint256 totalSize;
|
|
553
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
554
|
+
totalSize += orderSizes[i];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
_fundAndApprove(maker, orderCoin, totalSize);
|
|
558
|
+
|
|
559
|
+
vm.recordLogs();
|
|
560
|
+
vm.prank(maker);
|
|
561
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, maker);
|
|
562
|
+
created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function _fundAndApprove(address maker, address coin, uint256 amount) private {
|
|
566
|
+
if (coin == address(0)) {
|
|
567
|
+
vm.deal(maker, amount);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
deal(coin, maker, amount);
|
|
572
|
+
vm.startPrank(maker);
|
|
573
|
+
IERC20(coin).approve(address(limitOrderBook), amount);
|
|
574
|
+
vm.stopPrank();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/// @notice Tests that reentrancy during withdrawal is prevented by CEI pattern
|
|
578
|
+
function test_withdraw_reentrancyPrevented() public {
|
|
579
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
580
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
581
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
582
|
+
|
|
583
|
+
// Create 2 orders for the seller
|
|
584
|
+
uint256[] memory orderSizes = new uint256[](2);
|
|
585
|
+
orderSizes[0] = 100 ether;
|
|
586
|
+
orderSizes[1] = 100 ether;
|
|
587
|
+
(, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 2, 1);
|
|
588
|
+
(CreatedOrderLog[] memory created, ) = _createOrders(users.seller, key, isCurrency0, orderSizes, orderTicks);
|
|
589
|
+
|
|
590
|
+
// Deploy malicious recipient that will try to re-enter
|
|
591
|
+
ReentrancyAttacker attacker = new ReentrancyAttacker(limitOrderBook, created[1].orderId, orderCoin);
|
|
592
|
+
|
|
593
|
+
// Withdraw first order to the attacker contract
|
|
594
|
+
// The attacker will try to withdraw the second order during the callback
|
|
595
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
596
|
+
orderIds[0] = created[0].orderId;
|
|
597
|
+
|
|
598
|
+
vm.prank(users.seller);
|
|
599
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, address(attacker));
|
|
600
|
+
|
|
601
|
+
// Verify first order was withdrawn successfully
|
|
602
|
+
LimitOrderTypes.LimitOrder memory firstOrder = limitOrderBook.exposedOrder(created[0].orderId);
|
|
603
|
+
assertEq(uint8(firstOrder.status), uint8(LimitOrderTypes.OrderStatus.INACTIVE), "first order should be inactive");
|
|
604
|
+
|
|
605
|
+
// Verify attacker's reentrancy attempt failed (second order still open)
|
|
606
|
+
// Note: The attack would fail with OrderClosed if order was already marked inactive,
|
|
607
|
+
// or it would succeed if state wasn't updated before external call
|
|
608
|
+
assertFalse(attacker.attackSucceeded(), "reentrancy attack should have failed");
|
|
609
|
+
|
|
610
|
+
// Second order should still be open (attacker couldn't steal it)
|
|
611
|
+
LimitOrderTypes.LimitOrder memory secondOrder = limitOrderBook.exposedOrder(created[1].orderId);
|
|
612
|
+
assertEq(uint8(secondOrder.status), uint8(LimitOrderTypes.OrderStatus.OPEN), "second order should still be open");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/// @notice Malicious contract that attempts reentrancy during token receipt
|
|
617
|
+
contract ReentrancyAttacker {
|
|
618
|
+
IZoraLimitOrderBook public limitOrderBook;
|
|
619
|
+
bytes32 public targetOrderId;
|
|
620
|
+
address public coin;
|
|
621
|
+
bool public attackSucceeded;
|
|
622
|
+
bool public attacked;
|
|
623
|
+
|
|
624
|
+
constructor(IZoraLimitOrderBook _limitOrderBook, bytes32 _targetOrderId, address _coin) {
|
|
625
|
+
limitOrderBook = _limitOrderBook;
|
|
626
|
+
targetOrderId = _targetOrderId;
|
|
627
|
+
coin = _coin;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/// @notice Called when receiving ERC20 tokens - attempts reentrancy
|
|
631
|
+
fallback() external payable {
|
|
632
|
+
_tryAttack();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
receive() external payable {
|
|
636
|
+
_tryAttack();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function _tryAttack() internal {
|
|
640
|
+
if (!attacked) {
|
|
641
|
+
attacked = true;
|
|
642
|
+
// Try to withdraw another order during the callback
|
|
643
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
644
|
+
orderIds[0] = targetOrderId;
|
|
645
|
+
|
|
646
|
+
try limitOrderBook.withdraw(orderIds, coin, 0, address(this)) {
|
|
647
|
+
attackSucceeded = true;
|
|
648
|
+
} catch {
|
|
649
|
+
attackSucceeded = false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|