@zoralabs/limit-orders 0.2.0 → 0.2.2
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 +50 -49
- package/CHANGELOG.md +73 -0
- package/abis/ISetLimitOrderConfig.json +27 -0
- package/abis/IWETH.json +118 -0
- package/abis/IZoraLimitOrderBook.json +5 -0
- package/abis/LimitOrderLiquidity.json +7 -0
- package/abis/LimitOrderViews.json +62 -0
- package/abis/{SimpleAccessManaged.json → Ownable.json} +29 -10
- package/abis/Ownable2Step.json +115 -0
- package/abis/PermittedCallers.json +181 -0
- package/abis/SwapWithLimitOrders.json +134 -14
- package/abis/ZoraLimitOrderBook.json +187 -35
- package/cache/solidity-files-cache.json +1 -1
- package/dist/index.cjs +219 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +219 -34
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +254 -41
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/out/BalanceDelta.sol/BalanceDeltaLibrary.json +1 -1
- package/out/BeforeSwapDelta.sol/BeforeSwapDeltaLibrary.json +1 -1
- package/out/BitMath.sol/BitMath.json +1 -1
- package/out/BytesLib.sol/BytesLib.json +1 -1
- package/out/CoinCommon.sol/CoinCommon.json +1 -1
- package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
- package/out/CoinConstants.sol/CoinConstants.json +1 -1
- package/out/Context.sol/Context.json +1 -1
- package/out/Currency.sol/CurrencyLibrary.json +1 -1
- package/out/CurrencyReserves.sol/CurrencyReserves.json +1 -1
- package/out/CustomRevert.sol/CustomRevert.json +1 -1
- package/out/DopplerMath.sol/DopplerMath.json +1 -1
- package/out/FixedPoint128.sol/FixedPoint128.json +1 -1
- package/out/FixedPoint96.sol/FixedPoint96.json +1 -1
- package/out/FullMath.sol/FullMath.json +1 -1
- package/out/IAllowanceTransfer.sol/IAllowanceTransfer.json +1 -1
- package/out/ICoin.sol/ICoin.json +1 -1
- package/out/ICoin.sol/IHasCoinType.json +1 -1
- package/out/ICoin.sol/IHasPoolKey.json +1 -1
- package/out/ICoin.sol/IHasSwapPath.json +1 -1
- package/out/ICoin.sol/IHasTotalSupplyForPositions.json +1 -1
- package/out/IDeployedCoinVersionLookup.sol/IDeployedCoinVersionLookup.json +1 -1
- package/out/IDopplerErrors.sol/IDopplerErrors.json +1 -1
- package/out/IEIP712.sol/IEIP712.json +1 -1
- package/out/IERC1363.sol/IERC1363.json +1 -1
- package/out/IERC165.sol/IERC165.json +1 -1
- package/out/IERC20.sol/IERC20.json +1 -1
- package/out/IERC20Minimal.sol/IERC20Minimal.json +1 -1
- package/out/IERC6909Claims.sol/IERC6909Claims.json +1 -1
- package/out/IERC7572.sol/IERC7572.json +1 -1
- package/out/IExtsload.sol/IExtsload.json +1 -1
- package/out/IExttload.sol/IExttload.json +1 -1
- package/out/IHasRewardsRecipients.sol/IHasRewardsRecipients.json +1 -1
- package/out/IHooks.sol/IHooks.json +1 -1
- package/out/IMsgSender.sol/IMsgSender.json +1 -1
- package/out/IPoolManager.sol/IPoolManager.json +1 -1
- package/out/IProtocolFees.sol/IProtocolFees.json +1 -1
- package/out/ISetLimitOrderConfig.sol/ISetLimitOrderConfig.json +1 -0
- package/out/ISupportsLimitOrderFill.sol/ISupportsLimitOrderFill.json +1 -1
- package/out/ISwapPathRouter.sol/ISwapPathRouter.json +1 -1
- package/out/ISwapRouter.sol/ISwapRouter.json +1 -1
- package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -1
- package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4Hook.json +1 -1
- package/out/IUpgradeableV4Hook.sol/IUpgradeableDestinationV4HookWithUpdateableFee.json +1 -1
- package/out/IUpgradeableV4Hook.sol/IUpgradeableV4Hook.json +1 -1
- package/out/IWETH.sol/IWETH.json +1 -0
- package/out/IZoraHookRegistry.sol/IZoraHookRegistry.json +1 -1
- package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -1
- package/out/IZoraLimitOrderBookCoinsInterface.sol/IZoraLimitOrderBookCoinsInterface.json +1 -1
- package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.json +1 -1
- package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -1
- package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -1
- package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -1
- package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -1
- package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -1
- package/out/LimitOrderQueues.sol/LimitOrderQueues.json +1 -1
- package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -1
- package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -1
- package/out/LimitOrderViews.sol/LimitOrderViews.json +1 -0
- package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
- package/out/LiquidityAmounts.sol/LiquidityAmounts.json +1 -1
- package/out/LiquidityMath.sol/LiquidityMath.json +1 -1
- package/out/Lock.sol/Lock.json +1 -1
- package/out/NonzeroDeltaCount.sol/NonzeroDeltaCount.json +1 -1
- package/out/Ownable.sol/Ownable.json +1 -0
- package/out/Ownable2Step.sol/Ownable2Step.json +1 -0
- package/out/Path.sol/Path.json +1 -1
- package/out/PathKey.sol/PathKeyLibrary.json +1 -1
- package/out/Permit2Payments.sol/Permit2Payments.json +1 -1
- package/out/PermittedCallers.sol/PermittedCallers.json +1 -0
- package/out/PoolId.sol/PoolIdLibrary.json +1 -1
- package/out/Position.sol/Position.json +1 -1
- package/out/SafeCast.sol/SafeCast.json +1 -1
- package/out/SafeCast160.sol/SafeCast160.json +1 -1
- package/out/SafeERC20.sol/SafeERC20.json +1 -1
- package/out/SqrtPriceMath.sol/SqrtPriceMath.json +1 -1
- package/out/StateLibrary.sol/StateLibrary.json +1 -1
- package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
- package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
- package/out/TickBitmap.sol/TickBitmap.json +1 -1
- package/out/TickMath.sol/TickMath.json +1 -1
- package/out/TransientSlot.sol/TransientSlot.json +1 -1
- package/out/TransientStateLibrary.sol/TransientStateLibrary.json +1 -1
- package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -1
- package/out/UnsafeMath.sol/UnsafeMath.json +1 -1
- package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -1
- package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
- package/out/build-info/37e0124d88d60569.json +1 -0
- package/out/uniswap/BitMath.sol/BitMath.json +1 -1
- package/out/uniswap/CustomRevert.sol/CustomRevert.json +1 -1
- package/out/uniswap/FullMath.sol/FullMath.json +1 -1
- package/out/uniswap/SafeCast.sol/SafeCast.json +1 -1
- package/out/uniswap/TickMath.sol/TickMath.json +1 -1
- package/package/wagmiGenerated.ts +218 -33
- package/package.json +1 -1
- package/src/IZoraLimitOrderBook.sol +5 -5
- package/src/ZoraLimitOrderBook.sol +24 -41
- package/src/access/PermittedCallers.sol +41 -0
- package/src/libs/LimitOrderBitmap.sol +0 -51
- package/src/libs/LimitOrderCommon.sol +48 -30
- package/src/libs/LimitOrderCreate.sol +5 -18
- package/src/libs/LimitOrderFill.sol +32 -161
- package/src/libs/LimitOrderLiquidity.sol +92 -71
- package/src/libs/LimitOrderViews.sol +168 -0
- package/src/libs/LimitOrderWithdraw.sol +13 -4
- package/src/libs/SwapLimitOrders.sol +14 -7
- package/src/router/ISetLimitOrderConfig.sol +12 -0
- package/src/router/SwapWithLimitOrders.sol +46 -33
- package/test/LimitOrderAccessControl.t.sol +173 -156
- package/test/LimitOrderBitmap.t.sol +13 -7
- package/test/LimitOrderFill.t.sol +42 -4
- package/test/LimitOrderLibraries.t.sol +18 -10
- package/test/LimitOrderLiquidityPayouts.t.sol +280 -3
- package/test/LimitOrderWithdraw.t.sol +28 -1
- package/test/SwapWithLimitOrders.t.sol +3 -5
- package/test/SwapWithLimitOrdersRouter.t.sol +108 -13
- package/test/gas/LimitOrderFillGas.t.sol +0 -7
- package/test/gas/LimitOrderSwapGas.t.sol +0 -6
- package/test/unit/LimitOrderBitmapUnit.t.sol +0 -134
- package/test/unit/LimitOrderCreateUnit.t.sol +32 -0
- package/test/unit/SwapLimitOrdersUnit.t.sol +231 -33
- package/test/unit/SwapLimitOrdersValidation.t.sol +28 -42
- package/test/unit/SwapWithLimitOrdersUnit.t.sol +21 -88
- package/test/utils/BaseTest.sol +34 -22
- package/test/utils/MockWETH.sol +39 -0
- package/test/utils/TestableZoraLimitOrderBook.sol +5 -7
- package/abis/IAuthority.json +0 -31
- package/abis/SimpleAccessManager.json +0 -351
- package/out/IAuthority.sol/IAuthority.json +0 -1
- package/out/SimpleAccessManaged.sol/SimpleAccessManaged.json +0 -1
- package/out/SimpleAccessManager.sol/SimpleAccessManager.json +0 -1
- package/out/build-info/69718f10d1dc37f0.json +0 -1
- package/src/access/SimpleAccessManaged.sol +0 -76
- package/src/access/SimpleAccessManager.sol +0 -268
- package/test/SimpleAccessManager.t.sol +0 -420
|
@@ -67,7 +67,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
67
67
|
assertEq(created.length, 3, "should create three orders");
|
|
68
68
|
|
|
69
69
|
// Verify tick queue linked list structure
|
|
70
|
-
|
|
70
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
71
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
71
72
|
assertEq(tickQueue.length, 3, "queue length should be 3");
|
|
72
73
|
assertEq(tickQueue.head, created[0].orderId, "head should be first order");
|
|
73
74
|
assertEq(tickQueue.tail, created[2].orderId, "tail should be last order");
|
|
@@ -108,7 +109,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
108
109
|
vm.prank(users.seller);
|
|
109
110
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
113
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
112
114
|
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
113
115
|
assertEq(tickQueue.head, created[1].orderId, "head should be second order");
|
|
114
116
|
assertEq(tickQueue.tail, created[2].orderId, "tail unchanged");
|
|
@@ -149,7 +151,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
149
151
|
vm.prank(users.seller);
|
|
150
152
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
155
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
153
156
|
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
154
157
|
assertEq(tickQueue.head, created[0].orderId, "head unchanged");
|
|
155
158
|
assertEq(tickQueue.tail, created[1].orderId, "tail should be second order");
|
|
@@ -190,7 +193,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
190
193
|
vm.prank(users.seller);
|
|
191
194
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
192
195
|
|
|
193
|
-
|
|
196
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
197
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, fillableTick);
|
|
194
198
|
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
195
199
|
assertEq(tickQueue.head, created[0].orderId, "head unchanged");
|
|
196
200
|
assertEq(tickQueue.tail, created[2].orderId, "tail unchanged");
|
|
@@ -237,7 +241,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
237
241
|
|
|
238
242
|
// Verify bitmap is set
|
|
239
243
|
bytes32 poolKeyHash = keccak256(abi.encode(key));
|
|
240
|
-
|
|
244
|
+
int24 fillableTick = _fillableTick(isCurrency0, orderTicks[0], key.tickSpacing);
|
|
245
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be initialized");
|
|
241
246
|
}
|
|
242
247
|
|
|
243
248
|
function test_bitmapSetIfFirstWhenNonEmpty() public {
|
|
@@ -262,9 +267,10 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
262
267
|
|
|
263
268
|
// Verify bitmap is still set (second enqueue didn't break it)
|
|
264
269
|
bytes32 poolKeyHash = keccak256(abi.encode(key));
|
|
265
|
-
|
|
270
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
271
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be initialized");
|
|
266
272
|
|
|
267
|
-
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin,
|
|
273
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, fillableTick);
|
|
268
274
|
assertEq(tickQueue.length, 2, "should have 2 orders at same tick");
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -290,7 +296,8 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
290
296
|
vm.prank(users.seller);
|
|
291
297
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
292
298
|
|
|
293
|
-
|
|
299
|
+
int24 fillableTick = _fillableTick(isCurrency0, orderTicks[0], key.tickSpacing);
|
|
300
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should be cleared");
|
|
294
301
|
}
|
|
295
302
|
|
|
296
303
|
function test_bitmapClearIfEmptyWhenStillHasOrders() public {
|
|
@@ -323,9 +330,10 @@ contract LimitOrderLibrariesTest is BaseTest {
|
|
|
323
330
|
vm.prank(users.seller);
|
|
324
331
|
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
325
332
|
|
|
326
|
-
|
|
333
|
+
int24 fillableTick = _fillableTick(isCurrency0, tick, key.tickSpacing);
|
|
334
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, fillableTick, key.tickSpacing), "tick should still be initialized");
|
|
327
335
|
|
|
328
|
-
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin,
|
|
336
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, fillableTick);
|
|
329
337
|
assertEq(tickQueue.length, 1, "should have 1 order remaining");
|
|
330
338
|
}
|
|
331
339
|
|
|
@@ -19,6 +19,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
19
19
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
20
20
|
|
|
21
21
|
import {SimpleERC20} from "@zoralabs/coins/test/mocks/SimpleERC20.sol";
|
|
22
|
+
import {MockWETH} from "./utils/MockWETH.sol";
|
|
22
23
|
|
|
23
24
|
contract MockPoolManager {
|
|
24
25
|
using CurrencyLibrary for Currency;
|
|
@@ -29,8 +30,12 @@ contract MockPoolManager {
|
|
|
29
30
|
|
|
30
31
|
uint256 public swapCalls;
|
|
31
32
|
uint256 public takeCalls;
|
|
33
|
+
uint256 public syncCalls;
|
|
34
|
+
uint256 public settleCalls;
|
|
32
35
|
Currency public lastTakeCurrency;
|
|
33
36
|
uint256 public lastTakeAmount;
|
|
37
|
+
Currency public lastSyncCurrency;
|
|
38
|
+
uint256 public lastSettleValue;
|
|
34
39
|
|
|
35
40
|
function setModifyLiquidityResponse(int128 amount0, int128 amount1, int128 fee0, int128 fee1) external {
|
|
36
41
|
liquidityDelta = toBalanceDelta(amount0, amount1);
|
|
@@ -63,9 +68,14 @@ contract MockPoolManager {
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
function sync(Currency) external {
|
|
71
|
+
function sync(Currency currency) external {
|
|
72
|
+
++syncCalls;
|
|
73
|
+
lastSyncCurrency = currency;
|
|
74
|
+
}
|
|
67
75
|
|
|
68
76
|
function settle() external payable returns (uint256) {
|
|
77
|
+
++settleCalls;
|
|
78
|
+
lastSettleValue = msg.value;
|
|
69
79
|
return msg.value;
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -74,6 +84,11 @@ contract MockPoolManager {
|
|
|
74
84
|
|
|
75
85
|
contract LimitOrderLiquidityHarness {
|
|
76
86
|
LimitOrderTypes.LimitOrder internal order;
|
|
87
|
+
address internal weth;
|
|
88
|
+
|
|
89
|
+
function setWeth(address weth_) external {
|
|
90
|
+
weth = weth_;
|
|
91
|
+
}
|
|
77
92
|
|
|
78
93
|
function configureOrder(address maker, bool isCurrency0, int24 tickLower, int24 tickUpper, uint128 liquidity) external {
|
|
79
94
|
order.maker = maker;
|
|
@@ -99,7 +114,7 @@ contract LimitOrderLiquidityHarness {
|
|
|
99
114
|
address coinIn,
|
|
100
115
|
IDeployedCoinVersionLookup versionLookup
|
|
101
116
|
) external returns (Currency coinOut, uint128 makerAmount, uint128 referralAmount) {
|
|
102
|
-
return LimitOrderLiquidity.burnAndPayout(IPoolManager(address(poolManager)), key, order, orderId, feeRecipient, coinIn, versionLookup);
|
|
117
|
+
return LimitOrderLiquidity.burnAndPayout(IPoolManager(address(poolManager)), key, order, orderId, feeRecipient, coinIn, versionLookup, weth);
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
function burnAndRefund(MockPoolManager poolManager, PoolKey memory key, bytes32 orderId, address recipient) external returns (uint128 amountOut) {
|
|
@@ -112,13 +127,16 @@ contract LimitOrderLiquidityHarness {
|
|
|
112
127
|
order.liquidity,
|
|
113
128
|
orderId,
|
|
114
129
|
recipient,
|
|
115
|
-
order.isCurrency0
|
|
130
|
+
order.isCurrency0,
|
|
131
|
+
weth
|
|
116
132
|
);
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
function refundResidual(PoolKey memory key, bool isCurrency0, address maker, uint128 amount) external {
|
|
120
136
|
LimitOrderLiquidity.refundResidual(key, isCurrency0, maker, amount);
|
|
121
137
|
}
|
|
138
|
+
|
|
139
|
+
receive() external payable {}
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
contract MockCoinVersionLookup is IDeployedCoinVersionLookup {
|
|
@@ -167,6 +185,7 @@ contract LimitOrderLiquidityPayoutsTest is Test {
|
|
|
167
185
|
MockCoinVersionLookup internal versionLookup;
|
|
168
186
|
SimpleERC20 internal currency0Token;
|
|
169
187
|
SimpleERC20 internal currency1Token;
|
|
188
|
+
MockWETH internal weth;
|
|
170
189
|
PoolKey internal poolKey;
|
|
171
190
|
address internal maker;
|
|
172
191
|
|
|
@@ -179,6 +198,8 @@ contract LimitOrderLiquidityPayoutsTest is Test {
|
|
|
179
198
|
|
|
180
199
|
currency0Token = new SimpleERC20("Token0", "TK0");
|
|
181
200
|
currency1Token = new SimpleERC20("Token1", "TK1");
|
|
201
|
+
weth = new MockWETH();
|
|
202
|
+
harness.setWeth(address(weth));
|
|
182
203
|
|
|
183
204
|
poolKey = PoolKey({
|
|
184
205
|
currency0: Currency.wrap(address(currency0Token)),
|
|
@@ -249,6 +270,20 @@ contract LimitOrderLiquidityPayoutsTest is Test {
|
|
|
249
270
|
assertEq(Currency.unwrap(poolManager.lastTakeCurrency()), address(currency1Token));
|
|
250
271
|
}
|
|
251
272
|
|
|
273
|
+
function test_burnAndRefundPaysBothCurrenciesWhenPositive() public {
|
|
274
|
+
poolManager.setModifyLiquidityResponse(int128(10), int128(20), 0, 0);
|
|
275
|
+
deal(address(currency0Token), address(poolManager), 100e18);
|
|
276
|
+
deal(address(currency1Token), address(poolManager), 100e18);
|
|
277
|
+
address recipient = makeAddr("dual-recipient");
|
|
278
|
+
|
|
279
|
+
uint128 amountOut = harness.burnAndRefund(poolManager, poolKey, ORDER_ID, recipient);
|
|
280
|
+
|
|
281
|
+
assertEq(amountOut, 10, "amountOut should match order currency payout");
|
|
282
|
+
assertEq(currency0Token.balanceOf(recipient), 10, "recipient receives currency0");
|
|
283
|
+
assertEq(currency1Token.balanceOf(recipient), 20, "recipient receives currency1");
|
|
284
|
+
assertEq(poolManager.takeCalls(), 2, "both currencies should be taken");
|
|
285
|
+
}
|
|
286
|
+
|
|
252
287
|
function test_burnAndPayoutWithoutReferralRoutesAllProceeds() public {
|
|
253
288
|
poolManager.setModifyLiquidityResponse(0, int128(120), 0, 0);
|
|
254
289
|
deal(address(currency1Token), address(poolManager), 200e18);
|
|
@@ -269,6 +304,52 @@ contract LimitOrderLiquidityPayoutsTest is Test {
|
|
|
269
304
|
assertEq(poolManager.takeCalls(), 1, "single take call expected");
|
|
270
305
|
}
|
|
271
306
|
|
|
307
|
+
function test_burnAndPayoutPaysWethForNativePayouts() public {
|
|
308
|
+
PoolKey memory nativeKey = PoolKey({
|
|
309
|
+
currency0: CurrencyLibrary.ADDRESS_ZERO,
|
|
310
|
+
currency1: Currency.wrap(address(currency1Token)),
|
|
311
|
+
fee: 3000,
|
|
312
|
+
tickSpacing: 60,
|
|
313
|
+
hooks: IHooks(address(0))
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
harness.setOrderSide(false);
|
|
317
|
+
poolManager.setModifyLiquidityResponse(int128(50), 0, 0, 0);
|
|
318
|
+
vm.deal(address(poolManager), 1 ether);
|
|
319
|
+
|
|
320
|
+
uint256 wethBefore = weth.balanceOf(maker);
|
|
321
|
+
(, uint128 makerAmount, ) = harness.burnAndPayout(poolManager, nativeKey, ORDER_ID, address(0), address(currency1Token), versionLookup);
|
|
322
|
+
|
|
323
|
+
assertEq(makerAmount, 50, "maker payout should match native delta");
|
|
324
|
+
assertEq(weth.balanceOf(maker), wethBefore + 50, "maker should receive WETH");
|
|
325
|
+
assertEq(maker.balance, 0, "maker should not receive native ETH");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function test_burnAndPayoutPaysBothCurrenciesWhenPositive() public {
|
|
329
|
+
// This test now verifies that when both deltas are positive,
|
|
330
|
+
// only ONE currency is paid out (the payout currency) after swapping
|
|
331
|
+
poolManager.setModifyLiquidityResponse(int128(15), int128(25), 0, 0);
|
|
332
|
+
|
|
333
|
+
// Simulate swap: 15 of currency0 swaps to get some currency1
|
|
334
|
+
// (mock doesn't track amounts, just that swap occurred)
|
|
335
|
+
poolManager.setSwapResponse(0, int128(0));
|
|
336
|
+
|
|
337
|
+
deal(address(currency0Token), address(poolManager), 100e18);
|
|
338
|
+
deal(address(currency1Token), address(poolManager), 100e18);
|
|
339
|
+
|
|
340
|
+
uint256 makerCurrency0Before = currency0Token.balanceOf(maker);
|
|
341
|
+
uint256 makerCurrency1Before = currency1Token.balanceOf(maker);
|
|
342
|
+
|
|
343
|
+
(, uint128 makerAmount, ) = harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(currency0Token), versionLookup);
|
|
344
|
+
|
|
345
|
+
// With the fix: only currency1 is paid out (NOT currency0)
|
|
346
|
+
assertEq(currency0Token.balanceOf(maker), makerCurrency0Before, "maker should NOT receive currency0");
|
|
347
|
+
assertGt(currency1Token.balanceOf(maker), makerCurrency1Before, "maker should receive currency1");
|
|
348
|
+
|
|
349
|
+
// Verify a swap occurred to convert currency0 to currency1
|
|
350
|
+
assertEq(poolManager.swapCalls(), 1, "swap should occur to consolidate to single currency");
|
|
351
|
+
}
|
|
352
|
+
|
|
272
353
|
function test_burnAndPayoutSplitsReferralShares() public {
|
|
273
354
|
poolManager.setModifyLiquidityResponse(0, int128(150), 0, int128(30));
|
|
274
355
|
deal(address(currency1Token), address(poolManager), 300e18);
|
|
@@ -330,4 +411,200 @@ contract LimitOrderLiquidityPayoutsTest is Test {
|
|
|
330
411
|
assertEq(poolManager.swapCalls(), 1, "swap path should execute");
|
|
331
412
|
assertEq(currency1Token.balanceOf(maker), makerBefore + 40, "maker receives swapped currency");
|
|
332
413
|
}
|
|
414
|
+
|
|
415
|
+
/// @notice Test that with dual positive deltas, only currency1 (payout currency) is paid out
|
|
416
|
+
/// @dev This simulates the audit bug scenario where positions have fees in both tokens
|
|
417
|
+
function test_dualPositiveDeltas_payoutsCurrency1Only() public {
|
|
418
|
+
// Order is selling currency0, so payout currency is currency1
|
|
419
|
+
harness.setOrderSide(true); // isCurrency0 = true
|
|
420
|
+
|
|
421
|
+
// Simulate dual positive deltas: both amount0 and amount1 are positive
|
|
422
|
+
// This happens when a position is crossed in both directions
|
|
423
|
+
poolManager.setModifyLiquidityResponse(int128(50), int128(100), 0, 0);
|
|
424
|
+
|
|
425
|
+
// Simulate swap: swap 50 of currency0 to get 45 of currency1
|
|
426
|
+
poolManager.setSwapResponse(0, int128(45));
|
|
427
|
+
|
|
428
|
+
// Fund pool manager with both currencies
|
|
429
|
+
deal(address(currency0Token), address(poolManager), 500e18);
|
|
430
|
+
deal(address(currency1Token), address(poolManager), 500e18);
|
|
431
|
+
|
|
432
|
+
uint256 maker0Before = currency0Token.balanceOf(maker);
|
|
433
|
+
uint256 maker1Before = currency1Token.balanceOf(maker);
|
|
434
|
+
|
|
435
|
+
(Currency coinOut, uint128 makerAmount, ) = harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(0), versionLookup);
|
|
436
|
+
|
|
437
|
+
// Verify only currency1 is paid out (NOT currency0)
|
|
438
|
+
assertEq(Currency.unwrap(coinOut), address(currency1Token), "payout currency should be currency1");
|
|
439
|
+
assertEq(currency0Token.balanceOf(maker), maker0Before, "maker should NOT receive currency0");
|
|
440
|
+
|
|
441
|
+
// Verify total payout in currency1 includes both the original amount1 + swapped amount0
|
|
442
|
+
// Expected: 100 (original currency1) + 45 (swapped from currency0) = 145
|
|
443
|
+
assertEq(currency1Token.balanceOf(maker), maker1Before + 145, "maker should receive combined amount in currency1");
|
|
444
|
+
assertEq(makerAmount, 145, "makerAmount should be combined total");
|
|
445
|
+
|
|
446
|
+
// Verify a swap occurred to convert currency0 to currency1
|
|
447
|
+
assertEq(poolManager.swapCalls(), 1, "swap should occur to convert currency0 to currency1");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// @notice Test that with dual positive deltas, only currency0 (payout currency) is paid out
|
|
451
|
+
/// @dev This tests the opposite direction - selling currency1, expecting currency0 payout
|
|
452
|
+
function test_dualPositiveDeltas_payoutsCurrency0Only() public {
|
|
453
|
+
// Order is selling currency1, so payout currency is currency0
|
|
454
|
+
harness.setOrderSide(false); // isCurrency0 = false
|
|
455
|
+
|
|
456
|
+
// Simulate dual positive deltas
|
|
457
|
+
poolManager.setModifyLiquidityResponse(int128(80), int128(60), 0, 0);
|
|
458
|
+
|
|
459
|
+
// Simulate swap: swap 60 of currency1 to get 55 of currency0
|
|
460
|
+
poolManager.setSwapResponse(int128(55), 0);
|
|
461
|
+
|
|
462
|
+
// Fund pool manager
|
|
463
|
+
deal(address(currency0Token), address(poolManager), 500e18);
|
|
464
|
+
deal(address(currency1Token), address(poolManager), 500e18);
|
|
465
|
+
|
|
466
|
+
uint256 maker0Before = currency0Token.balanceOf(maker);
|
|
467
|
+
uint256 maker1Before = currency1Token.balanceOf(maker);
|
|
468
|
+
|
|
469
|
+
(Currency coinOut, uint128 makerAmount, ) = harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(0), versionLookup);
|
|
470
|
+
|
|
471
|
+
// Verify only currency0 is paid out (NOT currency1)
|
|
472
|
+
assertEq(Currency.unwrap(coinOut), address(currency0Token), "payout currency should be currency0");
|
|
473
|
+
assertEq(currency1Token.balanceOf(maker), maker1Before, "maker should NOT receive currency1");
|
|
474
|
+
|
|
475
|
+
// Verify total payout in currency0 includes both the original amount0 + swapped amount1
|
|
476
|
+
// Expected: 80 (original currency0) + 55 (swapped from currency1) = 135
|
|
477
|
+
assertEq(currency0Token.balanceOf(maker), maker0Before + 135, "maker should receive combined amount in currency0");
|
|
478
|
+
assertEq(makerAmount, 135, "makerAmount should be combined total");
|
|
479
|
+
|
|
480
|
+
// Verify a swap occurred
|
|
481
|
+
assertEq(poolManager.swapCalls(), 1, "swap should occur to convert currency1 to currency0");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/// @notice Test that with dual positive deltas and referral fees, both maker and referral receive single currency
|
|
485
|
+
function test_dualPositiveDeltas_makerAndReferral_singleCurrencyPayout() public {
|
|
486
|
+
address referral = makeAddr("referral");
|
|
487
|
+
harness.setOrderSide(true); // isCurrency0 = true, payout in currency1
|
|
488
|
+
|
|
489
|
+
// Simulate dual positive deltas with fees for referral
|
|
490
|
+
// liquidity: (60 amount0, 120 amount1), fees: (20 amount0, 30 amount1)
|
|
491
|
+
poolManager.setModifyLiquidityResponse(int128(60), int128(120), int128(20), int128(30));
|
|
492
|
+
|
|
493
|
+
// Simulate swaps for both maker and referral portions
|
|
494
|
+
// First swap (maker): swap 40 of currency0 to get 35 of currency1
|
|
495
|
+
// Second swap (referral): swap 20 of currency0 to get 18 of currency1
|
|
496
|
+
poolManager.setSwapResponse(0, int128(35));
|
|
497
|
+
|
|
498
|
+
deal(address(currency0Token), address(poolManager), 500e18);
|
|
499
|
+
deal(address(currency1Token), address(poolManager), 500e18);
|
|
500
|
+
|
|
501
|
+
uint256 maker0Before = currency0Token.balanceOf(maker);
|
|
502
|
+
uint256 maker1Before = currency1Token.balanceOf(maker);
|
|
503
|
+
uint256 ref0Before = currency0Token.balanceOf(referral);
|
|
504
|
+
uint256 ref1Before = currency1Token.balanceOf(referral);
|
|
505
|
+
|
|
506
|
+
(Currency makerCoinOut, uint128 makerAmount, uint128 referralAmount) = harness.burnAndPayout(
|
|
507
|
+
poolManager,
|
|
508
|
+
poolKey,
|
|
509
|
+
ORDER_ID,
|
|
510
|
+
referral,
|
|
511
|
+
address(0),
|
|
512
|
+
versionLookup
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
// Verify maker receives only currency1
|
|
516
|
+
assertEq(Currency.unwrap(makerCoinOut), address(currency1Token), "maker payout should be currency1");
|
|
517
|
+
assertEq(currency0Token.balanceOf(maker), maker0Before, "maker should NOT receive currency0");
|
|
518
|
+
|
|
519
|
+
// Verify referral receives only currency1
|
|
520
|
+
assertEq(currency0Token.balanceOf(referral), ref0Before, "referral should NOT receive currency0");
|
|
521
|
+
|
|
522
|
+
// Both should have increased currency1 balances
|
|
523
|
+
assertGt(currency1Token.balanceOf(maker), maker1Before, "maker should receive currency1");
|
|
524
|
+
assertGt(currency1Token.balanceOf(referral), ref1Before, "referral should receive currency1");
|
|
525
|
+
|
|
526
|
+
// Verify swaps occurred (one for maker, one for referral)
|
|
527
|
+
assertEq(poolManager.swapCalls(), 2, "two swaps should occur (maker + referral)");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function test_burnAndPayoutBypassesSwapPathWhenPayoutCurrencyMismatches() public {
|
|
531
|
+
TestSwapPathCoin swapCoin = new TestSwapPathCoin(Currency.wrap(address(currency0Token)));
|
|
532
|
+
versionLookup.setVersion(address(swapCoin), 4);
|
|
533
|
+
|
|
534
|
+
poolManager.setModifyLiquidityResponse(int128(22), 0, 0, 0);
|
|
535
|
+
poolManager.setSwapResponse(0, int128(22));
|
|
536
|
+
|
|
537
|
+
deal(address(currency0Token), address(poolManager), 200e18);
|
|
538
|
+
deal(address(currency1Token), address(poolManager), 200e18);
|
|
539
|
+
|
|
540
|
+
uint256 makerBefore = currency1Token.balanceOf(maker);
|
|
541
|
+
|
|
542
|
+
harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(swapCoin), versionLookup);
|
|
543
|
+
|
|
544
|
+
assertEq(poolManager.swapCalls(), 1, "swap should occur using fallback single-hop path");
|
|
545
|
+
assertEq(currency1Token.balanceOf(maker), makerBefore + 22, "maker receives currency1 from swap");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/// @notice MKT-27: Documents that wrong coin is passed but validation provides safety net
|
|
549
|
+
/// @dev Demonstrates the root cause bug in _fillOrder():
|
|
550
|
+
/// - _fillOrder() passes INPUT coin (CoinA) to burnAndPayout
|
|
551
|
+
/// - Should pass OUTPUT coin (WETH) instead
|
|
552
|
+
/// - Validation in _resolvePayoutPath() catches the mismatch and falls back
|
|
553
|
+
/// - Result: User correctly receives WETH (due to validation, not correct logic)
|
|
554
|
+
///
|
|
555
|
+
/// Scenario: Pool CoinA/WETH, CoinA has path to USDC, Order: Sell CoinA for WETH
|
|
556
|
+
/// Root bug: Looks at CoinA's path (wrong coin!)
|
|
557
|
+
/// Safety net: Validation sees path leads to USDC not WETH, falls back
|
|
558
|
+
///
|
|
559
|
+
/// This test verifies the validation works, but we should still fix the root cause.
|
|
560
|
+
function test_burnAndPayoutUsesWrongCoinPayoutPath_MKT27() public {
|
|
561
|
+
// Create CoinA with payout path to USDC (currency0Token)
|
|
562
|
+
// This simulates a coin whose payout path leads to unexpected currency
|
|
563
|
+
TestSwapPathCoin coinA = new TestSwapPathCoin(Currency.wrap(address(currency0Token))); // path to USDC
|
|
564
|
+
versionLookup.setVersion(address(coinA), 4);
|
|
565
|
+
|
|
566
|
+
// Create pool: CoinA / WETH (currency1Token)
|
|
567
|
+
PoolKey memory testPoolKey = PoolKey({
|
|
568
|
+
currency0: Currency.wrap(address(coinA)), // CoinA
|
|
569
|
+
currency1: Currency.wrap(address(currency1Token)), // WETH
|
|
570
|
+
fee: 3000,
|
|
571
|
+
tickSpacing: 60,
|
|
572
|
+
hooks: poolKey.hooks
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Order: Sell CoinA (currency0) for WETH (currency1)
|
|
576
|
+
harness.setOrderSide(true); // isCurrency0 = true → payout should be currency1 (WETH)
|
|
577
|
+
|
|
578
|
+
// Simulate liquidity return: 100 units returned
|
|
579
|
+
poolManager.setModifyLiquidityResponse(int128(100), 0, 0, 0);
|
|
580
|
+
|
|
581
|
+
// Simulate fallback swap (validation rejects CoinA's path, uses single-hop)
|
|
582
|
+
poolManager.setSwapResponse(0, int128(100));
|
|
583
|
+
|
|
584
|
+
// Fund pool manager
|
|
585
|
+
deal(address(currency0Token), address(poolManager), 500e18); // USDC
|
|
586
|
+
deal(address(currency1Token), address(poolManager), 500e18); // WETH
|
|
587
|
+
|
|
588
|
+
uint256 makerWethBefore = currency1Token.balanceOf(maker);
|
|
589
|
+
|
|
590
|
+
// ROOT CAUSE BUG: Pass CoinA (input coin) - this is what _fillOrder() does
|
|
591
|
+
// SHOULD PASS: WETH (output coin) - this is what it should do
|
|
592
|
+
(Currency coinOut, , ) = harness.burnAndPayout(
|
|
593
|
+
poolManager,
|
|
594
|
+
testPoolKey,
|
|
595
|
+
ORDER_ID,
|
|
596
|
+
address(0),
|
|
597
|
+
address(coinA), // ← ROOT BUG: passing wrong coin
|
|
598
|
+
versionLookup
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
// VALIDATION SAFETY NET WORKS: User receives correct currency (WETH)
|
|
602
|
+
// Even though we passed wrong coin, validation caught it and fell back
|
|
603
|
+
assertEq(Currency.unwrap(coinOut), address(currency1Token), "validation correctly falls back to WETH");
|
|
604
|
+
assertEq(currency1Token.balanceOf(maker), makerWethBefore + 100, "maker receives WETH (correct)");
|
|
605
|
+
|
|
606
|
+
// NOTE: While validation prevents harm, _fillOrder() should still be fixed to pass
|
|
607
|
+
// the OUTPUT coin, not INPUT coin. This makes the code semantically correct and
|
|
608
|
+
// doesn't rely on validation as a crutch.
|
|
609
|
+
}
|
|
333
610
|
}
|
|
@@ -54,6 +54,33 @@ contract LimitOrderWithdrawTest is BaseTest {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/// @notice Fillable orders must be filled, not withdrawn
|
|
58
|
+
function test_withdrawRevertsForFillableOrder() public {
|
|
59
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
60
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
61
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
62
|
+
|
|
63
|
+
// Create orders directly
|
|
64
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
|
|
65
|
+
uint256 totalSize = orderSizes[0];
|
|
66
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
67
|
+
|
|
68
|
+
vm.recordLogs();
|
|
69
|
+
vm.prank(users.seller);
|
|
70
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
71
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
72
|
+
assertEq(created.length, 1, "expected 1 order");
|
|
73
|
+
|
|
74
|
+
// Move price past order (makes it fillable)
|
|
75
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
76
|
+
|
|
77
|
+
bytes32[] memory orderIds = _orderIds(created);
|
|
78
|
+
|
|
79
|
+
vm.expectRevert(IZoraLimitOrderBook.OrderFillable.selector);
|
|
80
|
+
vm.prank(users.seller);
|
|
81
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
82
|
+
}
|
|
83
|
+
|
|
57
84
|
function test_withdrawOrdersRevertsForMixedCoins() public {
|
|
58
85
|
PoolKey memory creatorKey = creatorCoin.getPoolKey();
|
|
59
86
|
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
@@ -322,7 +349,7 @@ contract LimitOrderWithdrawTest is BaseTest {
|
|
|
322
349
|
|
|
323
350
|
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
324
351
|
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
325
|
-
int24 tick = ticks[0];
|
|
352
|
+
int24 tick = _fillableTick(isCurrency0, ticks[0], key.tickSpacing);
|
|
326
353
|
|
|
327
354
|
// Verify bitmap set
|
|
328
355
|
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, tick, key.tickSpacing), "tick should be initialized");
|
|
@@ -9,17 +9,15 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
|
9
9
|
import {Vm} from "forge-std/Vm.sol";
|
|
10
10
|
|
|
11
11
|
contract SwapWithLimitOrdersTest is BaseTest {
|
|
12
|
-
function
|
|
12
|
+
function test_autosellCreatesOrdersForSmallPurchases() public {
|
|
13
13
|
PoolKey memory key = creatorCoin.getPoolKey();
|
|
14
|
-
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
15
|
-
address orderCoin = _orderCoin(key, isCurrency0);
|
|
16
14
|
|
|
17
15
|
vm.recordLogs();
|
|
18
16
|
_executeSingleHopSwapWithLimitOrders(users.buyer, key, 1e13, _defaultMultiples(), _defaultPercentages());
|
|
19
17
|
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Small purchases create orders (no minimum threshold)
|
|
20
|
+
assertGt(created.length, 0, "small swap should create orders");
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
function test_multiHopAutosellCreatesOrdersOnlyOnLastPool() public {
|