@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,1008 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "../utils/BaseTest.sol";
|
|
5
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
6
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
7
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
+
import {LimitOrderCommon} from "../../src/libs/LimitOrderCommon.sol";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @title LimitOrderFillGasTest
|
|
12
|
+
* @notice Gas benchmark tests for limit order fill operations with payout swaps
|
|
13
|
+
* @dev Tests are structured to capture:
|
|
14
|
+
* 1. Baseline swap without limit orders
|
|
15
|
+
* 2. Single-hop fill (direct payout)
|
|
16
|
+
* 3. Multi-hop fill (with intermediate swaps triggering hook recursion)
|
|
17
|
+
* 4. Multi-order fill (5 orders with multi-hop payouts)
|
|
18
|
+
*/
|
|
19
|
+
contract LimitOrderFillGasTest is BaseTest {
|
|
20
|
+
// Gas measurement helpers
|
|
21
|
+
uint256 private gasStart;
|
|
22
|
+
uint256 private gasUsed;
|
|
23
|
+
|
|
24
|
+
function setUp() public virtual override {
|
|
25
|
+
super.setUp();
|
|
26
|
+
// Ensure maxFillCount is set to a reasonable value
|
|
27
|
+
limitOrderBook.setMaxFillCount(50);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// @notice Test 1: Baseline swap without limit orders to establish control measurement
|
|
31
|
+
/// @dev This provides a baseline for comparing hook costs with and without sender check
|
|
32
|
+
function test_gas_baseline_swap_no_limit_orders() public {
|
|
33
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
34
|
+
uint256 swapAmount = 10e18;
|
|
35
|
+
|
|
36
|
+
// Execute swap via the standard test helper and measure gas
|
|
37
|
+
gasStart = gasleft();
|
|
38
|
+
_executeSingleHopSwap(users.buyer, swapAmount, key, bytes(""));
|
|
39
|
+
gasUsed = gasStart - gasleft();
|
|
40
|
+
|
|
41
|
+
// Log result for comparison
|
|
42
|
+
emit log_named_uint("BASELINE_SWAP_GAS", gasUsed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// @notice Test 2: Single order fill with direct payout (0-hop - no conversion)
|
|
46
|
+
/// @dev Measures gas when filling one limit order that pays out directly without swap path
|
|
47
|
+
function test_gas_0hop_single_order_direct_payout() public {
|
|
48
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
49
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
50
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(key, isCurrency0);
|
|
51
|
+
|
|
52
|
+
// Create a single limit order
|
|
53
|
+
uint256 orderSize = 25e18;
|
|
54
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, orderSize);
|
|
55
|
+
|
|
56
|
+
// Fund maker
|
|
57
|
+
if (orderCoin == address(0)) {
|
|
58
|
+
vm.deal(users.seller, orderSize);
|
|
59
|
+
} else {
|
|
60
|
+
deal(orderCoin, users.seller, orderSize);
|
|
61
|
+
vm.prank(users.seller);
|
|
62
|
+
IERC20(orderCoin).approve(address(limitOrderBook), orderSize);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create order
|
|
66
|
+
vm.recordLogs();
|
|
67
|
+
vm.prank(users.seller);
|
|
68
|
+
limitOrderBook.create{value: orderCoin == address(0) ? orderSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
69
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
70
|
+
require(created.length == 1, "Expected 1 order");
|
|
71
|
+
|
|
72
|
+
// Move price to make order fillable (without triggering auto-fill)
|
|
73
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
74
|
+
|
|
75
|
+
// Fill order and measure gas
|
|
76
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
77
|
+
gasStart = gasleft();
|
|
78
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 1, address(0));
|
|
79
|
+
gasUsed = gasStart - gasleft();
|
|
80
|
+
|
|
81
|
+
emit log_named_uint("SINGLE_ORDER_DIRECT_PAYOUT_GAS", gasUsed);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// @notice Test 3: Single order fill with 2-hop payout (content → creator → ZORA)
|
|
85
|
+
/// @dev Measures gas for the 2-hop conversion path using contentCoin which converts through creatorCoin to ZORA
|
|
86
|
+
function test_gas_2hop_single_order_fill() public {
|
|
87
|
+
// Use contentCoin which has a swap path to creatorCoin
|
|
88
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
89
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
90
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
91
|
+
|
|
92
|
+
// Create a single limit order on contentCoin pool
|
|
93
|
+
uint256 orderSize = 25e18;
|
|
94
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 1, orderSize);
|
|
95
|
+
|
|
96
|
+
// Fund maker
|
|
97
|
+
if (orderCoin == address(0)) {
|
|
98
|
+
vm.deal(users.seller, orderSize);
|
|
99
|
+
} else {
|
|
100
|
+
deal(orderCoin, users.seller, orderSize);
|
|
101
|
+
vm.prank(users.seller);
|
|
102
|
+
IERC20(orderCoin).approve(address(limitOrderBook), orderSize);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create order
|
|
106
|
+
vm.recordLogs();
|
|
107
|
+
vm.prank(users.seller);
|
|
108
|
+
limitOrderBook.create{value: orderCoin == address(0) ? orderSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
109
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
110
|
+
require(created.length == 1, "Expected 1 order");
|
|
111
|
+
|
|
112
|
+
// Move price to make order fillable
|
|
113
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
114
|
+
|
|
115
|
+
// Fill order - this will trigger swap path with intermediate swaps
|
|
116
|
+
// Each intermediate swap hits ZoraV4CoinHook._afterSwap() causing:
|
|
117
|
+
// - Fee collection
|
|
118
|
+
// - LP reward distribution
|
|
119
|
+
// - Referral tracking
|
|
120
|
+
// - Epoch updates
|
|
121
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
122
|
+
gasStart = gasleft();
|
|
123
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 1, address(0));
|
|
124
|
+
gasUsed = gasStart - gasleft();
|
|
125
|
+
|
|
126
|
+
emit log_named_uint("SINGLE_ORDER_MULTIHOP_PAYOUT_GAS", gasUsed);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// @notice Test 4: Fill 5 orders with 2-hop payouts (content → creator → ZORA)
|
|
130
|
+
/// @dev Measures cumulative gas cost of filling multiple orders, each with 2-hop payout
|
|
131
|
+
/// This amplifies the hop conversion cost across multiple fills
|
|
132
|
+
function test_gas_2hop_five_orders_fill() public {
|
|
133
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
134
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
135
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
136
|
+
|
|
137
|
+
// Create 5 orders at different price levels
|
|
138
|
+
uint256 orderSize = 25e18;
|
|
139
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 5, orderSize);
|
|
140
|
+
|
|
141
|
+
uint256 totalSize = 0;
|
|
142
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
143
|
+
totalSize += orderSizes[i];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Fund maker
|
|
147
|
+
if (orderCoin == address(0)) {
|
|
148
|
+
vm.deal(users.seller, totalSize);
|
|
149
|
+
} else {
|
|
150
|
+
deal(orderCoin, users.seller, totalSize);
|
|
151
|
+
vm.prank(users.seller);
|
|
152
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create orders
|
|
156
|
+
vm.recordLogs();
|
|
157
|
+
vm.prank(users.seller);
|
|
158
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
159
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
160
|
+
require(created.length == 5, "Expected 5 orders");
|
|
161
|
+
|
|
162
|
+
// Move price to make all orders fillable
|
|
163
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
164
|
+
|
|
165
|
+
// Fill all 5 orders - each triggers multi-hop payout with hook recursion
|
|
166
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
167
|
+
gasStart = gasleft();
|
|
168
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 5, address(0));
|
|
169
|
+
gasUsed = gasStart - gasleft();
|
|
170
|
+
|
|
171
|
+
emit log_named_uint("FIVE_ORDERS_MULTIHOP_PAYOUT_GAS", gasUsed);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// @notice Test 5: Empty fill (no orders to fill) to measure baseline traversal cost
|
|
175
|
+
/// @dev Measures overhead of fill() when no orders exist in the range
|
|
176
|
+
function test_gas_empty_fill() public {
|
|
177
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
178
|
+
|
|
179
|
+
gasStart = gasleft();
|
|
180
|
+
limitOrderBook.fill(key, true, -type(int24).max, type(int24).max, 5, address(0));
|
|
181
|
+
gasUsed = gasStart - gasleft();
|
|
182
|
+
|
|
183
|
+
emit log_named_uint("EMPTY_FILL_GAS", gasUsed);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============ Tier 1 Extended Benchmarks ============
|
|
187
|
+
|
|
188
|
+
/// @notice Test 6: Ten orders 2-hop fill - validates linear scaling
|
|
189
|
+
/// @dev Proves gas scales linearly beyond 5 orders (content → creator → ZORA path)
|
|
190
|
+
function test_gas_2hop_ten_orders_fill() public {
|
|
191
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
192
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
193
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
194
|
+
|
|
195
|
+
// Create 10 orders at different price levels
|
|
196
|
+
uint256 orderSize = 25e18;
|
|
197
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 10, orderSize);
|
|
198
|
+
|
|
199
|
+
uint256 totalSize = 0;
|
|
200
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
201
|
+
totalSize += orderSizes[i];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fund maker
|
|
205
|
+
if (orderCoin == address(0)) {
|
|
206
|
+
vm.deal(users.seller, totalSize);
|
|
207
|
+
} else {
|
|
208
|
+
deal(orderCoin, users.seller, totalSize);
|
|
209
|
+
vm.prank(users.seller);
|
|
210
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Create orders
|
|
214
|
+
vm.recordLogs();
|
|
215
|
+
vm.prank(users.seller);
|
|
216
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
217
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
218
|
+
require(created.length == 10, "Expected 10 orders");
|
|
219
|
+
|
|
220
|
+
// Move price to make all orders fillable
|
|
221
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
222
|
+
|
|
223
|
+
// Fill all 10 orders - measures scaling behavior
|
|
224
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
225
|
+
gasStart = gasleft();
|
|
226
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 10, address(0));
|
|
227
|
+
gasUsed = gasStart - gasleft();
|
|
228
|
+
|
|
229
|
+
emit log_named_uint("TEN_ORDERS_MULTIHOP_PAYOUT_GAS", gasUsed);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// @notice Test 7: User swap with auto-fill (2-hop) - CRITICAL user-facing metric
|
|
233
|
+
/// @dev Measures gas when users swap and orders automatically fill (content → creator → ZORA)
|
|
234
|
+
function test_gas_2hop_user_swap_triggers_autofill() public {
|
|
235
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
236
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
237
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
238
|
+
|
|
239
|
+
// Create 5 fillable orders at different ticks
|
|
240
|
+
uint256 orderSize = 25e18;
|
|
241
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 5, orderSize);
|
|
242
|
+
|
|
243
|
+
uint256 totalSize = 0;
|
|
244
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
245
|
+
totalSize += orderSizes[i];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fund maker
|
|
249
|
+
if (orderCoin == address(0)) {
|
|
250
|
+
vm.deal(users.seller, totalSize);
|
|
251
|
+
} else {
|
|
252
|
+
deal(orderCoin, users.seller, totalSize);
|
|
253
|
+
vm.prank(users.seller);
|
|
254
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Create orders (but don't move price yet)
|
|
258
|
+
vm.prank(users.seller);
|
|
259
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
260
|
+
|
|
261
|
+
// User swaps THROUGH the order ticks, triggering auto-fill
|
|
262
|
+
// This is what users actually experience!
|
|
263
|
+
uint256 swapAmount = 200e18; // Large enough to cross all 5 orders
|
|
264
|
+
|
|
265
|
+
gasStart = gasleft();
|
|
266
|
+
_executeMultiHopSwap(users.buyer, swapAmount, _buildSwapRoute(contentKey), _buildSwapHookData(2));
|
|
267
|
+
gasUsed = gasStart - gasleft();
|
|
268
|
+
|
|
269
|
+
emit log_named_uint("USER_SWAP_WITH_AUTOFILL_GAS", gasUsed);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// @notice Test 8: Fee conversion self-recursion - isolates BIGGEST savings
|
|
273
|
+
/// @dev Measures impact of sender == address(this) check (affects EVERY swap)
|
|
274
|
+
function test_gas_large_swap_with_fee_conversion() public {
|
|
275
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
276
|
+
|
|
277
|
+
// Execute a large swap that generates significant fees
|
|
278
|
+
// The hook will collect fees and convert them to backing currency
|
|
279
|
+
// With optimization: fee conversion swap is skipped (sender == address(this))
|
|
280
|
+
// This optimization benefits EVERY swap, not just limit order fills!
|
|
281
|
+
uint256 largeSwapAmount = 500e18;
|
|
282
|
+
|
|
283
|
+
gasStart = gasleft();
|
|
284
|
+
_executeSingleHopSwap(users.buyer, largeSwapAmount, key, bytes(""));
|
|
285
|
+
gasUsed = gasStart - gasleft();
|
|
286
|
+
|
|
287
|
+
emit log_named_uint("LARGE_SWAP_WITH_FEE_CONVERSION_GAS", gasUsed);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============ Tier 2 Extended Benchmarks ============
|
|
291
|
+
|
|
292
|
+
/// @notice Test 9: Max fill count stress test - validates system handles configured limit
|
|
293
|
+
/// @dev Tests 25 orders (current maxFillCount setting) to ensure system stays under block gas limit
|
|
294
|
+
function test_gas_max_fillcount_stress_test() public {
|
|
295
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
296
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
297
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
298
|
+
|
|
299
|
+
// Create 25 orders at different price levels (current maxFillCount)
|
|
300
|
+
uint256 orderSize = 25e18;
|
|
301
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 25, orderSize);
|
|
302
|
+
|
|
303
|
+
uint256 totalSize = 0;
|
|
304
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
305
|
+
totalSize += orderSizes[i];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Fund maker
|
|
309
|
+
if (orderCoin == address(0)) {
|
|
310
|
+
vm.deal(users.seller, totalSize);
|
|
311
|
+
} else {
|
|
312
|
+
deal(orderCoin, users.seller, totalSize);
|
|
313
|
+
vm.prank(users.seller);
|
|
314
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Create orders
|
|
318
|
+
vm.recordLogs();
|
|
319
|
+
vm.prank(users.seller);
|
|
320
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
321
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
322
|
+
require(created.length == 25, "Expected 25 orders");
|
|
323
|
+
|
|
324
|
+
// Move price to make all orders fillable
|
|
325
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
326
|
+
|
|
327
|
+
// Fill all 25 orders - stress test at max configured limit
|
|
328
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
329
|
+
gasStart = gasleft();
|
|
330
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 25, address(0));
|
|
331
|
+
gasUsed = gasStart - gasleft();
|
|
332
|
+
|
|
333
|
+
emit log_named_uint("MAX_FILLCOUNT_25_ORDERS_GAS", gasUsed);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// @notice Test 10: Mixed order sizes - tests realistic workload with varying liquidity
|
|
337
|
+
/// @dev Creates 5 orders with different sizes to test if gas is proportional to liquidity
|
|
338
|
+
function test_gas_mixed_order_sizes() public {
|
|
339
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
340
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
341
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
342
|
+
|
|
343
|
+
// Create 5 orders with varying sizes: 1, 10, 50, 100, 250 ETH
|
|
344
|
+
uint256[] memory customSizes = new uint256[](5);
|
|
345
|
+
customSizes[0] = 1e18;
|
|
346
|
+
customSizes[1] = 10e18;
|
|
347
|
+
customSizes[2] = 50e18;
|
|
348
|
+
customSizes[3] = 100e18;
|
|
349
|
+
customSizes[4] = 250e18;
|
|
350
|
+
|
|
351
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 5, 0);
|
|
352
|
+
|
|
353
|
+
// Override with custom sizes
|
|
354
|
+
for (uint256 i = 0; i < customSizes.length; i++) {
|
|
355
|
+
orderSizes[i] = customSizes[i];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
uint256 totalSize = 0;
|
|
359
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
360
|
+
totalSize += orderSizes[i];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Fund maker
|
|
364
|
+
if (orderCoin == address(0)) {
|
|
365
|
+
vm.deal(users.seller, totalSize);
|
|
366
|
+
} else {
|
|
367
|
+
deal(orderCoin, users.seller, totalSize);
|
|
368
|
+
vm.prank(users.seller);
|
|
369
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Create orders
|
|
373
|
+
vm.recordLogs();
|
|
374
|
+
vm.prank(users.seller);
|
|
375
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
376
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
377
|
+
require(created.length == 5, "Expected 5 orders");
|
|
378
|
+
|
|
379
|
+
// Move price to make all orders fillable
|
|
380
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
381
|
+
|
|
382
|
+
// Fill all orders - measures if gas scales with liquidity
|
|
383
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
384
|
+
gasStart = gasleft();
|
|
385
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 5, address(0));
|
|
386
|
+
gasUsed = gasStart - gasleft();
|
|
387
|
+
|
|
388
|
+
emit log_named_uint("MIXED_ORDER_SIZES_GAS", gasUsed);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// @notice Test 11: Same-tick orders - tests dense liquidity with multiple orders at identical price
|
|
392
|
+
/// @dev Creates 5 orders all at the same tick to test FIFO traversal efficiency
|
|
393
|
+
function test_gas_same_tick_orders() public {
|
|
394
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
395
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
396
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
397
|
+
|
|
398
|
+
// Use _buildDeterministicOrders to get ticks, then set them all to the same value
|
|
399
|
+
uint256 orderSize = 25e18;
|
|
400
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 5, orderSize);
|
|
401
|
+
|
|
402
|
+
// Override all ticks to be the same (use the first tick as the common tick)
|
|
403
|
+
int24 commonTick = orderTicks[0];
|
|
404
|
+
for (uint256 i = 1; i < orderTicks.length; i++) {
|
|
405
|
+
orderTicks[i] = commonTick;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
uint256 totalSize = 0;
|
|
409
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
410
|
+
totalSize += orderSizes[i];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Fund maker
|
|
414
|
+
if (orderCoin == address(0)) {
|
|
415
|
+
vm.deal(users.seller, totalSize);
|
|
416
|
+
} else {
|
|
417
|
+
deal(orderCoin, users.seller, totalSize);
|
|
418
|
+
vm.prank(users.seller);
|
|
419
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Create orders
|
|
423
|
+
vm.recordLogs();
|
|
424
|
+
vm.prank(users.seller);
|
|
425
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
426
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
427
|
+
require(created.length == 5, "Expected 5 orders");
|
|
428
|
+
|
|
429
|
+
// Move price to make all orders fillable
|
|
430
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
431
|
+
|
|
432
|
+
// Fill all orders - measures dense liquidity traversal cost
|
|
433
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
434
|
+
gasStart = gasleft();
|
|
435
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 5, address(0));
|
|
436
|
+
gasUsed = gasStart - gasleft();
|
|
437
|
+
|
|
438
|
+
emit log_named_uint("SAME_TICK_ORDERS_GAS", gasUsed);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ============ Phase 1 Stress Tests: User Auto-Fill at Scale ============
|
|
442
|
+
// Critical tests for determining optimal maxFillCount in production
|
|
443
|
+
|
|
444
|
+
/// @notice Test 12: User swap auto-fill with 10 orders
|
|
445
|
+
/// @dev Establishes baseline for linear scaling validation
|
|
446
|
+
function test_gas_user_swap_autofill_10_orders() public {
|
|
447
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
448
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
449
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
450
|
+
|
|
451
|
+
// Create 10 orders at different ticks
|
|
452
|
+
uint256 orderSize = 25e18;
|
|
453
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 10, orderSize);
|
|
454
|
+
|
|
455
|
+
uint256 totalSize = 0;
|
|
456
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
457
|
+
totalSize += orderSizes[i];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Fund maker
|
|
461
|
+
if (orderCoin == address(0)) {
|
|
462
|
+
vm.deal(users.seller, totalSize);
|
|
463
|
+
} else {
|
|
464
|
+
deal(orderCoin, users.seller, totalSize);
|
|
465
|
+
vm.prank(users.seller);
|
|
466
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Create orders (but don't move price yet)
|
|
470
|
+
vm.prank(users.seller);
|
|
471
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
472
|
+
|
|
473
|
+
// User swaps THROUGH all 10 order ticks, triggering auto-fill
|
|
474
|
+
uint256 swapAmount = 500e18; // Large enough to cross all 10 orders
|
|
475
|
+
|
|
476
|
+
gasStart = gasleft();
|
|
477
|
+
_executeMultiHopSwap(users.buyer, swapAmount, _buildSwapRoute(contentKey), _buildSwapHookData(2));
|
|
478
|
+
gasUsed = gasStart - gasleft();
|
|
479
|
+
|
|
480
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_10_ORDERS_GAS", gasUsed);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// @notice Test 13: User swap auto-fill with 25 orders (current maxFillCount)
|
|
484
|
+
/// @dev Tests current production configuration
|
|
485
|
+
function test_gas_user_swap_autofill_25_orders() public {
|
|
486
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
487
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
488
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
489
|
+
|
|
490
|
+
// Create 25 orders at different ticks
|
|
491
|
+
uint256 orderSize = 25e18;
|
|
492
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 25, orderSize);
|
|
493
|
+
|
|
494
|
+
uint256 totalSize = 0;
|
|
495
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
496
|
+
totalSize += orderSizes[i];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Fund maker
|
|
500
|
+
if (orderCoin == address(0)) {
|
|
501
|
+
vm.deal(users.seller, totalSize);
|
|
502
|
+
} else {
|
|
503
|
+
deal(orderCoin, users.seller, totalSize);
|
|
504
|
+
vm.prank(users.seller);
|
|
505
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Create orders (but don't move price yet)
|
|
509
|
+
vm.prank(users.seller);
|
|
510
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
511
|
+
|
|
512
|
+
// User swaps THROUGH all 25 order ticks, triggering auto-fill
|
|
513
|
+
uint256 swapAmount = 1000e18; // Large enough to cross all 25 orders
|
|
514
|
+
|
|
515
|
+
gasStart = gasleft();
|
|
516
|
+
_executeMultiHopSwap(users.buyer, swapAmount, _buildSwapRoute(contentKey), _buildSwapHookData(2));
|
|
517
|
+
gasUsed = gasStart - gasleft();
|
|
518
|
+
|
|
519
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_25_ORDERS_GAS", gasUsed);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// @notice Test 14: User swap auto-fill with 40 orders (recommended candidate)
|
|
523
|
+
/// @dev Tests recommended maxFillCount value (37% block utilization, 2.7× safety margin)
|
|
524
|
+
function test_gas_user_swap_autofill_40_orders() public {
|
|
525
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
526
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
527
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
528
|
+
|
|
529
|
+
// Create 40 orders at different ticks
|
|
530
|
+
uint256 orderSize = 25e18;
|
|
531
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 40, orderSize);
|
|
532
|
+
|
|
533
|
+
uint256 totalSize = 0;
|
|
534
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
535
|
+
totalSize += orderSizes[i];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Fund maker
|
|
539
|
+
if (orderCoin == address(0)) {
|
|
540
|
+
vm.deal(users.seller, totalSize);
|
|
541
|
+
} else {
|
|
542
|
+
deal(orderCoin, users.seller, totalSize);
|
|
543
|
+
vm.prank(users.seller);
|
|
544
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Create orders (but don't move price yet)
|
|
548
|
+
vm.prank(users.seller);
|
|
549
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
550
|
+
|
|
551
|
+
// User swaps THROUGH all 40 order ticks, triggering auto-fill
|
|
552
|
+
uint256 swapAmount = 1500e18; // Large enough to cross all 40 orders
|
|
553
|
+
|
|
554
|
+
gasStart = gasleft();
|
|
555
|
+
_executeMultiHopSwap(users.buyer, swapAmount, _buildSwapRoute(contentKey), _buildSwapHookData(2));
|
|
556
|
+
gasUsed = gasStart - gasleft();
|
|
557
|
+
|
|
558
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_40_ORDERS_GAS", gasUsed);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/// @notice Test 15: User swap auto-fill with 50 orders (aggressive candidate)
|
|
562
|
+
/// @dev Tests aggressive maxFillCount value (43% block utilization, 2.3× safety margin)
|
|
563
|
+
function test_gas_user_swap_autofill_50_orders() public {
|
|
564
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
565
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
566
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
567
|
+
|
|
568
|
+
// Create 50 orders at different ticks
|
|
569
|
+
uint256 orderSize = 25e18;
|
|
570
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 50, orderSize);
|
|
571
|
+
|
|
572
|
+
uint256 totalSize = 0;
|
|
573
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
574
|
+
totalSize += orderSizes[i];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Fund maker
|
|
578
|
+
if (orderCoin == address(0)) {
|
|
579
|
+
vm.deal(users.seller, totalSize);
|
|
580
|
+
} else {
|
|
581
|
+
deal(orderCoin, users.seller, totalSize);
|
|
582
|
+
vm.prank(users.seller);
|
|
583
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Create orders (but don't move price yet)
|
|
587
|
+
vm.prank(users.seller);
|
|
588
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
589
|
+
|
|
590
|
+
// User swaps THROUGH all 50 order ticks, triggering auto-fill
|
|
591
|
+
uint256 swapAmount = 2000e18; // Large enough to cross all 50 orders
|
|
592
|
+
|
|
593
|
+
gasStart = gasleft();
|
|
594
|
+
_executeMultiHopSwap(users.buyer, swapAmount, _buildSwapRoute(contentKey), _buildSwapHookData(2));
|
|
595
|
+
gasUsed = gasStart - gasleft();
|
|
596
|
+
|
|
597
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_50_ORDERS_GAS", gasUsed);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ============ Phase 2 Stress Tests: Extended Scaling Validation ============
|
|
601
|
+
// Tests to validate safety margins beyond recommended maxFillCount
|
|
602
|
+
|
|
603
|
+
/// @notice Test 16: User swap auto-fill with 75 orders (stress test)
|
|
604
|
+
/// @dev Tests well beyond recommended maxFillCount to validate safety margins
|
|
605
|
+
function test_gas_user_swap_autofill_75_orders() public {
|
|
606
|
+
// Increase maxFillCount for this test
|
|
607
|
+
vm.prank(users.factoryOwner);
|
|
608
|
+
limitOrderBook.setMaxFillCount(100);
|
|
609
|
+
|
|
610
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
611
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
612
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
613
|
+
|
|
614
|
+
// Create 75 orders at different ticks
|
|
615
|
+
uint256 orderSize = 25e18;
|
|
616
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 75, orderSize);
|
|
617
|
+
|
|
618
|
+
uint256 totalSize = 0;
|
|
619
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
620
|
+
totalSize += orderSizes[i];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Fund maker
|
|
624
|
+
if (orderCoin == address(0)) {
|
|
625
|
+
vm.deal(users.seller, totalSize);
|
|
626
|
+
} else {
|
|
627
|
+
deal(orderCoin, users.seller, totalSize);
|
|
628
|
+
vm.prank(users.seller);
|
|
629
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Create orders (but don't move price yet)
|
|
633
|
+
vm.prank(users.seller);
|
|
634
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
635
|
+
|
|
636
|
+
// User swaps THROUGH all 75 order ticks, triggering auto-fill
|
|
637
|
+
uint256 swapAmount = 3000e18; // Large enough to cross all 75 orders
|
|
638
|
+
|
|
639
|
+
gasStart = gasleft();
|
|
640
|
+
_executeMultiHopSwap(users.buyer, swapAmount, _buildSwapRoute(contentKey), _buildSwapHookData(2));
|
|
641
|
+
gasUsed = gasStart - gasleft();
|
|
642
|
+
|
|
643
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_75_ORDERS_GAS", gasUsed);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/// @notice Test 17: User swap auto-fill with 100 orders (max stress test)
|
|
647
|
+
/// @dev Tests maximum scaling to identify absolute upper limits
|
|
648
|
+
function test_gas_user_swap_autofill_100_orders() public {
|
|
649
|
+
// Increase maxFillCount for this test
|
|
650
|
+
vm.prank(users.factoryOwner);
|
|
651
|
+
limitOrderBook.setMaxFillCount(100);
|
|
652
|
+
|
|
653
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
654
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
655
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
656
|
+
|
|
657
|
+
// Create 100 orders at different ticks
|
|
658
|
+
uint256 orderSize = 25e18;
|
|
659
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 100, orderSize);
|
|
660
|
+
|
|
661
|
+
uint256 totalSize = 0;
|
|
662
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
663
|
+
totalSize += orderSizes[i];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Fund maker
|
|
667
|
+
if (orderCoin == address(0)) {
|
|
668
|
+
vm.deal(users.seller, totalSize);
|
|
669
|
+
} else {
|
|
670
|
+
deal(orderCoin, users.seller, totalSize);
|
|
671
|
+
vm.prank(users.seller);
|
|
672
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Create orders (but don't move price yet)
|
|
676
|
+
vm.prank(users.seller);
|
|
677
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
678
|
+
|
|
679
|
+
// User swaps THROUGH all 100 order ticks, triggering auto-fill
|
|
680
|
+
uint256 swapAmount = 4000e18; // Large enough to cross all 100 orders
|
|
681
|
+
|
|
682
|
+
gasStart = gasleft();
|
|
683
|
+
_executeMultiHopSwap(users.buyer, swapAmount, _buildSwapRoute(contentKey), _buildSwapHookData(2));
|
|
684
|
+
gasUsed = gasStart - gasleft();
|
|
685
|
+
|
|
686
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_100_ORDERS_GAS", gasUsed);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/// @notice Test 18: Backend manual fill with 50 orders
|
|
690
|
+
/// @dev Tests backend/bot operations - isolated fill() call without user swap overhead
|
|
691
|
+
function test_gas_backend_manual_fill_50_orders() public {
|
|
692
|
+
// Increase maxFillCount for this test
|
|
693
|
+
vm.prank(users.factoryOwner);
|
|
694
|
+
limitOrderBook.setMaxFillCount(100);
|
|
695
|
+
|
|
696
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
697
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
698
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
699
|
+
|
|
700
|
+
// Create 50 orders at different ticks
|
|
701
|
+
uint256 orderSize = 25e18;
|
|
702
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 50, orderSize);
|
|
703
|
+
|
|
704
|
+
uint256 totalSize = 0;
|
|
705
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
706
|
+
totalSize += orderSizes[i];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Fund maker
|
|
710
|
+
if (orderCoin == address(0)) {
|
|
711
|
+
vm.deal(users.seller, totalSize);
|
|
712
|
+
} else {
|
|
713
|
+
deal(orderCoin, users.seller, totalSize);
|
|
714
|
+
vm.prank(users.seller);
|
|
715
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Create orders
|
|
719
|
+
vm.recordLogs();
|
|
720
|
+
vm.prank(users.seller);
|
|
721
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
722
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
723
|
+
require(created.length == 50, "Expected 50 orders");
|
|
724
|
+
|
|
725
|
+
// Move price to make all orders fillable
|
|
726
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
727
|
+
|
|
728
|
+
// Backend fills orders - measures isolated fill operation
|
|
729
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
730
|
+
gasStart = gasleft();
|
|
731
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 50, address(0));
|
|
732
|
+
gasUsed = gasStart - gasleft();
|
|
733
|
+
|
|
734
|
+
emit log_named_uint("BACKEND_MANUAL_FILL_50_ORDERS_GAS", gasUsed);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/// @notice Test 19: Backend manual fill with 100 orders
|
|
738
|
+
/// @dev Tests large backend batch operations
|
|
739
|
+
function test_gas_backend_manual_fill_100_orders() public {
|
|
740
|
+
// Increase maxFillCount for this test
|
|
741
|
+
vm.prank(users.factoryOwner);
|
|
742
|
+
limitOrderBook.setMaxFillCount(100);
|
|
743
|
+
|
|
744
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
745
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
746
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
747
|
+
|
|
748
|
+
// Create 100 orders at different ticks
|
|
749
|
+
uint256 orderSize = 25e18;
|
|
750
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 100, orderSize);
|
|
751
|
+
|
|
752
|
+
uint256 totalSize = 0;
|
|
753
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
754
|
+
totalSize += orderSizes[i];
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Fund maker
|
|
758
|
+
if (orderCoin == address(0)) {
|
|
759
|
+
vm.deal(users.seller, totalSize);
|
|
760
|
+
} else {
|
|
761
|
+
deal(orderCoin, users.seller, totalSize);
|
|
762
|
+
vm.prank(users.seller);
|
|
763
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Create orders
|
|
767
|
+
vm.recordLogs();
|
|
768
|
+
vm.prank(users.seller);
|
|
769
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
770
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
771
|
+
require(created.length == 100, "Expected 100 orders");
|
|
772
|
+
|
|
773
|
+
// Move price to make all orders fillable
|
|
774
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
775
|
+
|
|
776
|
+
// Backend fills orders - measures isolated fill operation
|
|
777
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
778
|
+
gasStart = gasleft();
|
|
779
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 100, address(0));
|
|
780
|
+
gasUsed = gasStart - gasleft();
|
|
781
|
+
|
|
782
|
+
emit log_named_uint("BACKEND_MANUAL_FILL_100_ORDERS_GAS", gasUsed);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/// @notice Test 20: Backend manual fill with 150 orders (pathological case)
|
|
786
|
+
/// @dev Tests extreme backend batch - likely exceeds reasonable block gas limits
|
|
787
|
+
function test_gas_backend_manual_fill_150_orders() public {
|
|
788
|
+
// Increase maxFillCount for this test
|
|
789
|
+
vm.prank(users.factoryOwner);
|
|
790
|
+
limitOrderBook.setMaxFillCount(200);
|
|
791
|
+
|
|
792
|
+
PoolKey memory contentKey = contentCoin.getPoolKey();
|
|
793
|
+
bool isCurrency0 = Currency.unwrap(contentKey.currency0) == address(contentCoin);
|
|
794
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(contentKey, isCurrency0);
|
|
795
|
+
|
|
796
|
+
// Create 150 orders at different ticks
|
|
797
|
+
uint256 orderSize = 25e18;
|
|
798
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(contentKey, isCurrency0, 150, orderSize);
|
|
799
|
+
|
|
800
|
+
uint256 totalSize = 0;
|
|
801
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
802
|
+
totalSize += orderSizes[i];
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Fund maker
|
|
806
|
+
if (orderCoin == address(0)) {
|
|
807
|
+
vm.deal(users.seller, totalSize);
|
|
808
|
+
} else {
|
|
809
|
+
deal(orderCoin, users.seller, totalSize);
|
|
810
|
+
vm.prank(users.seller);
|
|
811
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Create orders
|
|
815
|
+
vm.recordLogs();
|
|
816
|
+
vm.prank(users.seller);
|
|
817
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(contentKey, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
818
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
819
|
+
require(created.length == 150, "Expected 150 orders");
|
|
820
|
+
|
|
821
|
+
// Move price to make all orders fillable
|
|
822
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
823
|
+
|
|
824
|
+
// Backend fills orders - measures extreme batch operation
|
|
825
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, contentKey);
|
|
826
|
+
gasStart = gasleft();
|
|
827
|
+
limitOrderBook.fill(contentKey, isCurrency0, startTick, endTick, 150, address(0));
|
|
828
|
+
gasUsed = gasStart - gasleft();
|
|
829
|
+
|
|
830
|
+
emit log_named_uint("BACKEND_MANUAL_FILL_150_ORDERS_GAS", gasUsed);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ============ Helper Functions ============
|
|
834
|
+
|
|
835
|
+
/// @dev Helper to build swap route for multi-hop swaps
|
|
836
|
+
function _buildSwapRoute(PoolKey memory contentKey) internal view returns (PoolKey[] memory) {
|
|
837
|
+
PoolKey[] memory route = new PoolKey[](2);
|
|
838
|
+
route[0] = creatorCoin.getPoolKey();
|
|
839
|
+
route[1] = contentKey;
|
|
840
|
+
return route;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/// @dev Helper to build hook data for multi-hop swaps
|
|
844
|
+
function _buildSwapHookData(uint256 hops) internal pure returns (bytes[] memory) {
|
|
845
|
+
bytes[] memory hookData = new bytes[](hops);
|
|
846
|
+
for (uint256 i = 0; i < hops; i++) {
|
|
847
|
+
hookData[i] = bytes("");
|
|
848
|
+
}
|
|
849
|
+
return hookData;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/// @notice Helper to move price beyond order ticks without triggering automatic fills
|
|
853
|
+
/// @dev Disables hook's auto-fill, executes swap, re-enables auto-fill
|
|
854
|
+
function _movePriceBeyondTicksWithAutoFillDisabled(CreatedOrderLog[] memory created) internal override {
|
|
855
|
+
uint256 previousMaxFillCount = limitOrderBook.getMaxFillCount();
|
|
856
|
+
vm.prank(users.factoryOwner);
|
|
857
|
+
limitOrderBook.setMaxFillCount(0);
|
|
858
|
+
|
|
859
|
+
_movePriceBeyondTicks(created);
|
|
860
|
+
|
|
861
|
+
vm.prank(users.factoryOwner);
|
|
862
|
+
limitOrderBook.setMaxFillCount(previousMaxFillCount);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/// @notice Helper to actually move the price by executing a large swap
|
|
866
|
+
function _movePriceBeyondTicks(CreatedOrderLog[] memory created) internal override {
|
|
867
|
+
if (created.length == 0) return;
|
|
868
|
+
|
|
869
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
870
|
+
uint256 swapAmount = 1000e18;
|
|
871
|
+
|
|
872
|
+
// Content coin pools sit behind creator coin pools, so we need to route ZORA -> Creator -> Content
|
|
873
|
+
if (created[0].coin == address(contentCoin)) {
|
|
874
|
+
PoolKey[] memory route = new PoolKey[](2);
|
|
875
|
+
route[0] = creatorCoin.getPoolKey();
|
|
876
|
+
route[1] = contentCoin.getPoolKey();
|
|
877
|
+
|
|
878
|
+
bytes[] memory hookData = new bytes[](2);
|
|
879
|
+
hookData[0] = bytes("");
|
|
880
|
+
hookData[1] = bytes("");
|
|
881
|
+
|
|
882
|
+
_executeMultiHopSwap(users.buyer, swapAmount, route, hookData);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Execute a large swap to move price across all order ticks
|
|
887
|
+
_executeSingleHopSwap(users.buyer, swapAmount, key, bytes(""));
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ============ Hop Coverage Tests ============
|
|
891
|
+
|
|
892
|
+
/// @notice Test H1: 1-hop single order fill (creator coin → ZORA)
|
|
893
|
+
/// @dev Most common production path - creator coin limit orders payout in ZORA
|
|
894
|
+
function test_gas_1hop_single_order_fill() public {
|
|
895
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
896
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
897
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(key, isCurrency0);
|
|
898
|
+
|
|
899
|
+
// Create 1 order on creatorCoin pool
|
|
900
|
+
uint256 orderSize = 25e18;
|
|
901
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, orderSize);
|
|
902
|
+
|
|
903
|
+
// Fund maker
|
|
904
|
+
if (orderCoin == address(0)) {
|
|
905
|
+
vm.deal(users.seller, orderSize);
|
|
906
|
+
} else {
|
|
907
|
+
deal(orderCoin, users.seller, orderSize);
|
|
908
|
+
vm.prank(users.seller);
|
|
909
|
+
IERC20(orderCoin).approve(address(limitOrderBook), orderSize);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Create order
|
|
913
|
+
vm.recordLogs();
|
|
914
|
+
vm.prank(users.seller);
|
|
915
|
+
limitOrderBook.create{value: orderCoin == address(0) ? orderSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
916
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
917
|
+
require(created.length == 1, "Expected 1 order");
|
|
918
|
+
|
|
919
|
+
// Move price to make order fillable
|
|
920
|
+
_movePriceBeyondTicksWithAutoFillDisabled(created);
|
|
921
|
+
|
|
922
|
+
// Fill order - measures 1-hop payout (creator coin → ZORA)
|
|
923
|
+
(int24 startTick, int24 endTick) = _tickWindow(created, key);
|
|
924
|
+
gasStart = gasleft();
|
|
925
|
+
limitOrderBook.fill(key, isCurrency0, startTick, endTick, 1, address(0));
|
|
926
|
+
gasUsed = gasStart - gasleft();
|
|
927
|
+
|
|
928
|
+
emit log_named_uint("SINGLE_ORDER_1HOP_GAS", gasUsed);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/// @notice Test H2: 1-hop user auto-fill with 40 orders
|
|
932
|
+
/// @dev Production-scale measurement for most common creator coin scenario
|
|
933
|
+
function test_gas_1hop_user_swap_autofill_40_orders() public {
|
|
934
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
935
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
936
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(key, isCurrency0);
|
|
937
|
+
|
|
938
|
+
// Create 40 orders at different ticks
|
|
939
|
+
uint256 orderSize = 25e18;
|
|
940
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 40, orderSize);
|
|
941
|
+
|
|
942
|
+
uint256 totalSize = 0;
|
|
943
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
944
|
+
totalSize += orderSizes[i];
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Fund maker
|
|
948
|
+
if (orderCoin == address(0)) {
|
|
949
|
+
vm.deal(users.seller, totalSize);
|
|
950
|
+
} else {
|
|
951
|
+
deal(orderCoin, users.seller, totalSize);
|
|
952
|
+
vm.prank(users.seller);
|
|
953
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Create orders (but don't move price yet)
|
|
957
|
+
vm.prank(users.seller);
|
|
958
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
959
|
+
|
|
960
|
+
// User swaps THROUGH all 40 orders with 1-hop payout (creator → ZORA)
|
|
961
|
+
uint256 swapAmount = 1500e18; // Large enough to cross all 40 orders
|
|
962
|
+
|
|
963
|
+
gasStart = gasleft();
|
|
964
|
+
_executeSingleHopSwap(users.buyer, swapAmount, key, bytes(""));
|
|
965
|
+
gasUsed = gasStart - gasleft();
|
|
966
|
+
|
|
967
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_1HOP_40_ORDERS_GAS", gasUsed);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/// @notice Test H3: 1-hop user auto-fill with 50 orders (PRODUCTION DEFAULT)
|
|
971
|
+
/// @dev Validates recommended maxFillCount=50 for most common creator coin scenario
|
|
972
|
+
function test_gas_1hop_user_swap_autofill_50_orders() public {
|
|
973
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
974
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
975
|
+
address orderCoin = LimitOrderCommon.getOrderCoin(key, isCurrency0);
|
|
976
|
+
|
|
977
|
+
// Create 50 orders at different ticks
|
|
978
|
+
uint256 orderSize = 25e18;
|
|
979
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 50, orderSize);
|
|
980
|
+
|
|
981
|
+
uint256 totalSize = 0;
|
|
982
|
+
for (uint256 i = 0; i < orderSizes.length; i++) {
|
|
983
|
+
totalSize += orderSizes[i];
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Fund maker
|
|
987
|
+
if (orderCoin == address(0)) {
|
|
988
|
+
vm.deal(users.seller, totalSize);
|
|
989
|
+
} else {
|
|
990
|
+
deal(orderCoin, users.seller, totalSize);
|
|
991
|
+
vm.prank(users.seller);
|
|
992
|
+
IERC20(orderCoin).approve(address(limitOrderBook), totalSize);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Create orders (but don't move price yet)
|
|
996
|
+
vm.prank(users.seller);
|
|
997
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
998
|
+
|
|
999
|
+
// User swaps THROUGH all 50 orders with 1-hop payout (creator → ZORA)
|
|
1000
|
+
uint256 swapAmount = 1875e18; // Large enough to cross all 50 orders
|
|
1001
|
+
|
|
1002
|
+
gasStart = gasleft();
|
|
1003
|
+
_executeSingleHopSwap(users.buyer, swapAmount, key, bytes(""));
|
|
1004
|
+
gasUsed = gasStart - gasleft();
|
|
1005
|
+
|
|
1006
|
+
emit log_named_uint("USER_SWAP_AUTOFILL_1HOP_50_ORDERS_GAS", gasUsed);
|
|
1007
|
+
}
|
|
1008
|
+
}
|