@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,354 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
|
+
import {LimitOrderQueues} from "../src/libs/LimitOrderQueues.sol";
|
|
6
|
+
import {LimitOrderTypes} from "../src/libs/LimitOrderTypes.sol";
|
|
7
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
8
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
9
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
10
|
+
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
11
|
+
|
|
12
|
+
contract LimitOrderLibrariesTest is BaseTest {
|
|
13
|
+
using LimitOrderQueues for LimitOrderTypes.Queue;
|
|
14
|
+
using PoolIdLibrary for PoolKey;
|
|
15
|
+
|
|
16
|
+
function test_enqueueIntoEmptyQueue() public {
|
|
17
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
18
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
19
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
20
|
+
|
|
21
|
+
// Create single order to test empty queue enqueue
|
|
22
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
|
|
23
|
+
|
|
24
|
+
_fundAndApprove(users.seller, orderCoin, orderSizes[0]);
|
|
25
|
+
|
|
26
|
+
vm.recordLogs();
|
|
27
|
+
vm.prank(users.seller);
|
|
28
|
+
limitOrderBook.create{value: orderCoin == address(0) ? orderSizes[0] : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
29
|
+
|
|
30
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
31
|
+
assertEq(created.length, 1, "should create one order");
|
|
32
|
+
|
|
33
|
+
// Verify tick queue state after enqueue into empty queue
|
|
34
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, created[0].tick);
|
|
35
|
+
assertEq(tickQueue.length, 1, "queue length should be 1");
|
|
36
|
+
assertEq(tickQueue.head, created[0].orderId, "head should be the order");
|
|
37
|
+
assertEq(tickQueue.tail, created[0].orderId, "tail should be the order");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function test_enqueueIntoNonEmptyQueue() public {
|
|
41
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
42
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
43
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
44
|
+
|
|
45
|
+
// Create multiple orders at the same tick to test non-empty queue enqueue
|
|
46
|
+
int24 tick = _getValidTick(key, isCurrency0);
|
|
47
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
48
|
+
orderSizes[0] = 25e18;
|
|
49
|
+
orderSizes[1] = 25e18;
|
|
50
|
+
orderSizes[2] = 25e18;
|
|
51
|
+
int24[] memory orderTicks = new int24[](3);
|
|
52
|
+
orderTicks[0] = tick;
|
|
53
|
+
orderTicks[1] = tick;
|
|
54
|
+
orderTicks[2] = tick;
|
|
55
|
+
|
|
56
|
+
uint256 totalSize;
|
|
57
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
58
|
+
totalSize += orderSizes[i];
|
|
59
|
+
}
|
|
60
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
61
|
+
|
|
62
|
+
vm.recordLogs();
|
|
63
|
+
vm.prank(users.seller);
|
|
64
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
65
|
+
|
|
66
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
67
|
+
assertEq(created.length, 3, "should create three orders");
|
|
68
|
+
|
|
69
|
+
// Verify tick queue linked list structure
|
|
70
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, tick);
|
|
71
|
+
assertEq(tickQueue.length, 3, "queue length should be 3");
|
|
72
|
+
assertEq(tickQueue.head, created[0].orderId, "head should be first order");
|
|
73
|
+
assertEq(tickQueue.tail, created[2].orderId, "tail should be last order");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function test_unlinkHead() public {
|
|
77
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
78
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
79
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
80
|
+
|
|
81
|
+
// Create 3 orders at the same tick to test unlink head
|
|
82
|
+
int24 tick = _getValidTick(key, isCurrency0);
|
|
83
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
84
|
+
orderSizes[0] = 25e18;
|
|
85
|
+
orderSizes[1] = 25e18;
|
|
86
|
+
orderSizes[2] = 25e18;
|
|
87
|
+
int24[] memory orderTicks = new int24[](3);
|
|
88
|
+
orderTicks[0] = tick;
|
|
89
|
+
orderTicks[1] = tick;
|
|
90
|
+
orderTicks[2] = tick;
|
|
91
|
+
|
|
92
|
+
uint256 totalSize;
|
|
93
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
94
|
+
totalSize += orderSizes[i];
|
|
95
|
+
}
|
|
96
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
97
|
+
|
|
98
|
+
vm.recordLogs();
|
|
99
|
+
vm.prank(users.seller);
|
|
100
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
101
|
+
|
|
102
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
103
|
+
bytes32 headId = created[0].orderId;
|
|
104
|
+
|
|
105
|
+
// Withdraw head order
|
|
106
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
107
|
+
orderIds[0] = headId;
|
|
108
|
+
vm.prank(users.seller);
|
|
109
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
110
|
+
|
|
111
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, tick);
|
|
112
|
+
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
113
|
+
assertEq(tickQueue.head, created[1].orderId, "head should be second order");
|
|
114
|
+
assertEq(tickQueue.tail, created[2].orderId, "tail unchanged");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function test_unlinkTail() public {
|
|
118
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
119
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
120
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
121
|
+
|
|
122
|
+
// Create 3 orders at the same tick to test unlink tail
|
|
123
|
+
int24 tick = _getValidTick(key, isCurrency0);
|
|
124
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
125
|
+
orderSizes[0] = 25e18;
|
|
126
|
+
orderSizes[1] = 25e18;
|
|
127
|
+
orderSizes[2] = 25e18;
|
|
128
|
+
int24[] memory orderTicks = new int24[](3);
|
|
129
|
+
orderTicks[0] = tick;
|
|
130
|
+
orderTicks[1] = tick;
|
|
131
|
+
orderTicks[2] = tick;
|
|
132
|
+
|
|
133
|
+
uint256 totalSize;
|
|
134
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
135
|
+
totalSize += orderSizes[i];
|
|
136
|
+
}
|
|
137
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
138
|
+
|
|
139
|
+
vm.recordLogs();
|
|
140
|
+
vm.prank(users.seller);
|
|
141
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
142
|
+
|
|
143
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
144
|
+
bytes32 tailId = created[2].orderId;
|
|
145
|
+
|
|
146
|
+
// Withdraw tail order
|
|
147
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
148
|
+
orderIds[0] = tailId;
|
|
149
|
+
vm.prank(users.seller);
|
|
150
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
151
|
+
|
|
152
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, tick);
|
|
153
|
+
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
154
|
+
assertEq(tickQueue.head, created[0].orderId, "head unchanged");
|
|
155
|
+
assertEq(tickQueue.tail, created[1].orderId, "tail should be second order");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function test_unlinkMiddle() public {
|
|
159
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
160
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
161
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
162
|
+
|
|
163
|
+
// Create 3 orders at the same tick to test unlink middle
|
|
164
|
+
int24 tick = _getValidTick(key, isCurrency0);
|
|
165
|
+
uint256[] memory orderSizes = new uint256[](3);
|
|
166
|
+
orderSizes[0] = 25e18;
|
|
167
|
+
orderSizes[1] = 25e18;
|
|
168
|
+
orderSizes[2] = 25e18;
|
|
169
|
+
int24[] memory orderTicks = new int24[](3);
|
|
170
|
+
orderTicks[0] = tick;
|
|
171
|
+
orderTicks[1] = tick;
|
|
172
|
+
orderTicks[2] = tick;
|
|
173
|
+
|
|
174
|
+
uint256 totalSize;
|
|
175
|
+
for (uint256 i; i < orderSizes.length; ++i) {
|
|
176
|
+
totalSize += orderSizes[i];
|
|
177
|
+
}
|
|
178
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
179
|
+
|
|
180
|
+
vm.recordLogs();
|
|
181
|
+
vm.prank(users.seller);
|
|
182
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
183
|
+
|
|
184
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
185
|
+
bytes32 middleId = created[1].orderId;
|
|
186
|
+
|
|
187
|
+
// Withdraw middle order
|
|
188
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
189
|
+
orderIds[0] = middleId;
|
|
190
|
+
vm.prank(users.seller);
|
|
191
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
192
|
+
|
|
193
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, tick);
|
|
194
|
+
assertEq(tickQueue.length, 2, "queue length should be 2");
|
|
195
|
+
assertEq(tickQueue.head, created[0].orderId, "head unchanged");
|
|
196
|
+
assertEq(tickQueue.tail, created[2].orderId, "tail unchanged");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function test_unlinkSingleElement() public {
|
|
200
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
201
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
202
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
203
|
+
|
|
204
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
|
|
205
|
+
|
|
206
|
+
_fundAndApprove(users.seller, orderCoin, orderSizes[0]);
|
|
207
|
+
|
|
208
|
+
vm.recordLogs();
|
|
209
|
+
vm.prank(users.seller);
|
|
210
|
+
limitOrderBook.create{value: orderCoin == address(0) ? orderSizes[0] : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
211
|
+
|
|
212
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
213
|
+
|
|
214
|
+
// Withdraw only order
|
|
215
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
216
|
+
orderIds[0] = created[0].orderId;
|
|
217
|
+
vm.prank(users.seller);
|
|
218
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
219
|
+
|
|
220
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(created[0].poolKeyHash, orderCoin, created[0].tick);
|
|
221
|
+
assertEq(tickQueue.length, 0, "queue should be empty");
|
|
222
|
+
assertEq(tickQueue.head, bytes32(0), "head should be cleared");
|
|
223
|
+
assertEq(tickQueue.tail, bytes32(0), "tail should be cleared");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function test_bitmapSetIfFirstWhenEmpty() public {
|
|
227
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
228
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
229
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
230
|
+
|
|
231
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
|
|
232
|
+
|
|
233
|
+
_fundAndApprove(users.seller, orderCoin, orderSizes[0]);
|
|
234
|
+
|
|
235
|
+
vm.prank(users.seller);
|
|
236
|
+
limitOrderBook.create{value: orderCoin == address(0) ? orderSizes[0] : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
237
|
+
|
|
238
|
+
// Verify bitmap is set
|
|
239
|
+
bytes32 poolKeyHash = keccak256(abi.encode(key));
|
|
240
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, orderTicks[0], key.tickSpacing), "tick should be initialized");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function test_bitmapSetIfFirstWhenNonEmpty() public {
|
|
244
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
245
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
246
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
247
|
+
|
|
248
|
+
// Create two orders at same tick
|
|
249
|
+
int24 tick = _getValidTick(key, isCurrency0);
|
|
250
|
+
uint256[] memory orderSizes = new uint256[](2);
|
|
251
|
+
orderSizes[0] = 25e18;
|
|
252
|
+
orderSizes[1] = 25e18;
|
|
253
|
+
int24[] memory orderTicks = new int24[](2);
|
|
254
|
+
orderTicks[0] = tick;
|
|
255
|
+
orderTicks[1] = tick;
|
|
256
|
+
|
|
257
|
+
uint256 totalSize = orderSizes[0] + orderSizes[1];
|
|
258
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
259
|
+
|
|
260
|
+
vm.prank(users.seller);
|
|
261
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
262
|
+
|
|
263
|
+
// Verify bitmap is still set (second enqueue didn't break it)
|
|
264
|
+
bytes32 poolKeyHash = keccak256(abi.encode(key));
|
|
265
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, tick, key.tickSpacing), "tick should be initialized");
|
|
266
|
+
|
|
267
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, tick);
|
|
268
|
+
assertEq(tickQueue.length, 2, "should have 2 orders at same tick");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function test_bitmapClearIfEmptyWhenLastRemoved() public {
|
|
272
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
273
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
274
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
275
|
+
|
|
276
|
+
(uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrders(key, isCurrency0, 1, 25e18);
|
|
277
|
+
|
|
278
|
+
_fundAndApprove(users.seller, orderCoin, orderSizes[0]);
|
|
279
|
+
|
|
280
|
+
vm.recordLogs();
|
|
281
|
+
vm.prank(users.seller);
|
|
282
|
+
limitOrderBook.create{value: orderCoin == address(0) ? orderSizes[0] : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
283
|
+
|
|
284
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
285
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
286
|
+
|
|
287
|
+
// Withdraw order - should clear bitmap
|
|
288
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
289
|
+
orderIds[0] = created[0].orderId;
|
|
290
|
+
vm.prank(users.seller);
|
|
291
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
292
|
+
|
|
293
|
+
assertFalse(_isTickInitialized(poolKeyHash, orderCoin, orderTicks[0], key.tickSpacing), "tick should be cleared");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function test_bitmapClearIfEmptyWhenStillHasOrders() public {
|
|
297
|
+
PoolKey memory key = creatorCoin.getPoolKey();
|
|
298
|
+
bool isCurrency0 = Currency.unwrap(key.currency0) == address(creatorCoin);
|
|
299
|
+
address orderCoin = _orderCoin(key, isCurrency0);
|
|
300
|
+
|
|
301
|
+
// Create two orders at same tick
|
|
302
|
+
int24 tick = _getValidTick(key, isCurrency0);
|
|
303
|
+
uint256[] memory orderSizes = new uint256[](2);
|
|
304
|
+
orderSizes[0] = 25e18;
|
|
305
|
+
orderSizes[1] = 25e18;
|
|
306
|
+
int24[] memory orderTicks = new int24[](2);
|
|
307
|
+
orderTicks[0] = tick;
|
|
308
|
+
orderTicks[1] = tick;
|
|
309
|
+
|
|
310
|
+
uint256 totalSize = orderSizes[0] + orderSizes[1];
|
|
311
|
+
_fundAndApprove(users.seller, orderCoin, totalSize);
|
|
312
|
+
|
|
313
|
+
vm.recordLogs();
|
|
314
|
+
vm.prank(users.seller);
|
|
315
|
+
limitOrderBook.create{value: orderCoin == address(0) ? totalSize : 0}(key, isCurrency0, orderSizes, orderTicks, users.seller);
|
|
316
|
+
|
|
317
|
+
CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
|
|
318
|
+
bytes32 poolKeyHash = created[0].poolKeyHash;
|
|
319
|
+
|
|
320
|
+
// Withdraw first order - bitmap should still be set
|
|
321
|
+
bytes32[] memory orderIds = new bytes32[](1);
|
|
322
|
+
orderIds[0] = created[0].orderId;
|
|
323
|
+
vm.prank(users.seller);
|
|
324
|
+
limitOrderBook.withdraw(orderIds, orderCoin, 0, users.seller);
|
|
325
|
+
|
|
326
|
+
assertTrue(_isTickInitialized(poolKeyHash, orderCoin, tick, key.tickSpacing), "tick should still be initialized");
|
|
327
|
+
|
|
328
|
+
QueueSnapshot memory tickQueue = _tickQueueSnapshot(poolKeyHash, orderCoin, tick);
|
|
329
|
+
assertEq(tickQueue.length, 1, "should have 1 order remaining");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function _fundAndApprove(address user, address token, uint256 amount) internal {
|
|
333
|
+
if (token == address(0)) {
|
|
334
|
+
vm.deal(user, amount);
|
|
335
|
+
} else {
|
|
336
|
+
deal(token, user, amount);
|
|
337
|
+
vm.prank(user);
|
|
338
|
+
IERC20(token).approve(address(limitOrderBook), amount);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _getValidTick(PoolKey memory key, bool isCurrency0) internal view returns (int24) {
|
|
343
|
+
int24 currentTick = _currentTick(key);
|
|
344
|
+
|
|
345
|
+
// Get a tick away from current price based on direction
|
|
346
|
+
int24 offset = isCurrency0 ? key.tickSpacing * 2 : -key.tickSpacing * 2;
|
|
347
|
+
int24 targetTick = currentTick + offset;
|
|
348
|
+
|
|
349
|
+
// Align to tick spacing
|
|
350
|
+
targetTick = _alignedTick(targetTick, key.tickSpacing);
|
|
351
|
+
|
|
352
|
+
return targetTick;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {LimitOrderLiquidity} from "../src/libs/LimitOrderLiquidity.sol";
|
|
7
|
+
import {LimitOrderTypes} from "../src/libs/LimitOrderTypes.sol";
|
|
8
|
+
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
9
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
10
|
+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
|
11
|
+
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
12
|
+
import {BalanceDelta, toBalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
13
|
+
import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
|
|
14
|
+
import {IHasSwapPath} from "@zoralabs/coins/src/interfaces/ICoin.sol";
|
|
15
|
+
import {IDeployedCoinVersionLookup} from "@zoralabs/coins/src/interfaces/IDeployedCoinVersionLookup.sol";
|
|
16
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
17
|
+
|
|
18
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
19
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
20
|
+
|
|
21
|
+
import {SimpleERC20} from "@zoralabs/coins/test/mocks/SimpleERC20.sol";
|
|
22
|
+
|
|
23
|
+
contract MockPoolManager {
|
|
24
|
+
using CurrencyLibrary for Currency;
|
|
25
|
+
|
|
26
|
+
BalanceDelta private liquidityDelta;
|
|
27
|
+
BalanceDelta private feeDelta;
|
|
28
|
+
BalanceDelta private swapDelta;
|
|
29
|
+
|
|
30
|
+
uint256 public swapCalls;
|
|
31
|
+
uint256 public takeCalls;
|
|
32
|
+
Currency public lastTakeCurrency;
|
|
33
|
+
uint256 public lastTakeAmount;
|
|
34
|
+
|
|
35
|
+
function setModifyLiquidityResponse(int128 amount0, int128 amount1, int128 fee0, int128 fee1) external {
|
|
36
|
+
liquidityDelta = toBalanceDelta(amount0, amount1);
|
|
37
|
+
feeDelta = toBalanceDelta(fee0, fee1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setSwapResponse(int128 amount0, int128 amount1) external {
|
|
41
|
+
swapDelta = toBalanceDelta(amount0, amount1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory, bytes calldata) external returns (BalanceDelta, BalanceDelta) {
|
|
45
|
+
return (liquidityDelta, feeDelta);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function swap(PoolKey memory, SwapParams memory, bytes calldata) external returns (BalanceDelta) {
|
|
49
|
+
++swapCalls;
|
|
50
|
+
return swapDelta;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function take(Currency currency, address to, uint256 amount) external {
|
|
54
|
+
++takeCalls;
|
|
55
|
+
lastTakeCurrency = currency;
|
|
56
|
+
lastTakeAmount = amount;
|
|
57
|
+
|
|
58
|
+
if (currency.isAddressZero()) {
|
|
59
|
+
(bool ok, ) = to.call{value: amount}("");
|
|
60
|
+
require(ok, "native transfer failed");
|
|
61
|
+
} else {
|
|
62
|
+
require(IERC20(Currency.unwrap(currency)).transfer(to, amount), "ERC20 transfer failed");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sync(Currency) external {}
|
|
67
|
+
|
|
68
|
+
function settle() external payable returns (uint256) {
|
|
69
|
+
return msg.value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
receive() external payable {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
contract LimitOrderLiquidityHarness {
|
|
76
|
+
LimitOrderTypes.LimitOrder internal order;
|
|
77
|
+
|
|
78
|
+
function configureOrder(address maker, bool isCurrency0, int24 tickLower, int24 tickUpper, uint128 liquidity) external {
|
|
79
|
+
order.maker = maker;
|
|
80
|
+
order.isCurrency0 = isCurrency0;
|
|
81
|
+
order.tickLower = tickLower;
|
|
82
|
+
order.tickUpper = tickUpper;
|
|
83
|
+
order.liquidity = liquidity;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function setOrderSide(bool isCurrency0) external {
|
|
87
|
+
order.isCurrency0 = isCurrency0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setOrderMaker(address maker) external {
|
|
91
|
+
order.maker = maker;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function burnAndPayout(
|
|
95
|
+
MockPoolManager poolManager,
|
|
96
|
+
PoolKey memory key,
|
|
97
|
+
bytes32 orderId,
|
|
98
|
+
address feeRecipient,
|
|
99
|
+
address coinIn,
|
|
100
|
+
IDeployedCoinVersionLookup versionLookup
|
|
101
|
+
) external returns (Currency coinOut, uint128 makerAmount, uint128 referralAmount) {
|
|
102
|
+
return LimitOrderLiquidity.burnAndPayout(IPoolManager(address(poolManager)), key, order, orderId, feeRecipient, coinIn, versionLookup);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function burnAndRefund(MockPoolManager poolManager, PoolKey memory key, bytes32 orderId, address recipient) external returns (uint128 amountOut) {
|
|
106
|
+
return
|
|
107
|
+
LimitOrderLiquidity.burnAndRefund(
|
|
108
|
+
IPoolManager(address(poolManager)),
|
|
109
|
+
key,
|
|
110
|
+
order.tickLower,
|
|
111
|
+
order.tickUpper,
|
|
112
|
+
order.liquidity,
|
|
113
|
+
orderId,
|
|
114
|
+
recipient,
|
|
115
|
+
order.isCurrency0
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function refundResidual(PoolKey memory key, bool isCurrency0, address maker, uint128 amount) external {
|
|
120
|
+
LimitOrderLiquidity.refundResidual(key, isCurrency0, maker, amount);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
contract MockCoinVersionLookup is IDeployedCoinVersionLookup {
|
|
125
|
+
mapping(address => uint8) internal versions;
|
|
126
|
+
bool public forceRevert;
|
|
127
|
+
|
|
128
|
+
function setVersion(address coin, uint8 version) external {
|
|
129
|
+
versions[coin] = version;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setShouldRevert(bool value) external {
|
|
133
|
+
forceRevert = value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getVersionForDeployedCoin(address coin) external view returns (uint8) {
|
|
137
|
+
if (forceRevert) {
|
|
138
|
+
revert("version lookup revert");
|
|
139
|
+
}
|
|
140
|
+
return versions[coin];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
contract TestSwapPathCoin is IHasSwapPath, IERC165 {
|
|
145
|
+
Currency internal payoutCurrency;
|
|
146
|
+
|
|
147
|
+
constructor(Currency payoutCurrency_) {
|
|
148
|
+
payoutCurrency = payoutCurrency_;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getPayoutSwapPath(IDeployedCoinVersionLookup) external view returns (IHasSwapPath.PayoutSwapPath memory payout) {
|
|
152
|
+
payout.currencyIn = Currency.wrap(address(this));
|
|
153
|
+
payout.path = new PathKey[](1);
|
|
154
|
+
payout.path[0] = PathKey({intermediateCurrency: payoutCurrency, fee: 3000, tickSpacing: 60, hooks: IHooks(address(0)), hookData: bytes("")});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
|
|
158
|
+
return interfaceId == type(IHasSwapPath).interfaceId || interfaceId == type(IERC165).interfaceId;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
contract LimitOrderLiquidityPayoutsTest is Test {
|
|
163
|
+
using CurrencyLibrary for Currency;
|
|
164
|
+
|
|
165
|
+
LimitOrderLiquidityHarness internal harness;
|
|
166
|
+
MockPoolManager internal poolManager;
|
|
167
|
+
MockCoinVersionLookup internal versionLookup;
|
|
168
|
+
SimpleERC20 internal currency0Token;
|
|
169
|
+
SimpleERC20 internal currency1Token;
|
|
170
|
+
PoolKey internal poolKey;
|
|
171
|
+
address internal maker;
|
|
172
|
+
|
|
173
|
+
bytes32 internal constant ORDER_ID = keccak256("order-id");
|
|
174
|
+
|
|
175
|
+
function setUp() public {
|
|
176
|
+
harness = new LimitOrderLiquidityHarness();
|
|
177
|
+
poolManager = new MockPoolManager();
|
|
178
|
+
versionLookup = new MockCoinVersionLookup();
|
|
179
|
+
|
|
180
|
+
currency0Token = new SimpleERC20("Token0", "TK0");
|
|
181
|
+
currency1Token = new SimpleERC20("Token1", "TK1");
|
|
182
|
+
|
|
183
|
+
poolKey = PoolKey({
|
|
184
|
+
currency0: Currency.wrap(address(currency0Token)),
|
|
185
|
+
currency1: Currency.wrap(address(currency1Token)),
|
|
186
|
+
fee: 3000,
|
|
187
|
+
tickSpacing: 60,
|
|
188
|
+
hooks: IHooks(address(0))
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
maker = makeAddr("maker");
|
|
192
|
+
harness.configureOrder(maker, true, -120, 120, 1_000);
|
|
193
|
+
versionLookup.setVersion(address(currency0Token), 3); // default to pre-v4 so swap path disabled unless overridden
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function test_refundResidualNativeCurrency() public {
|
|
197
|
+
PoolKey memory nativeKey = PoolKey({
|
|
198
|
+
currency0: CurrencyLibrary.ADDRESS_ZERO,
|
|
199
|
+
currency1: Currency.wrap(address(currency1Token)),
|
|
200
|
+
fee: 3000,
|
|
201
|
+
tickSpacing: 60,
|
|
202
|
+
hooks: IHooks(address(0))
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
uint128 refundAmount = 1 ether;
|
|
206
|
+
vm.deal(address(harness), refundAmount);
|
|
207
|
+
|
|
208
|
+
uint256 makerBalanceBefore = maker.balance;
|
|
209
|
+
harness.refundResidual(nativeKey, true, maker, refundAmount);
|
|
210
|
+
assertEq(maker.balance, makerBalanceBefore + refundAmount, "maker should receive native refund");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function test_refundResidualErc20Currency() public {
|
|
214
|
+
uint128 refundAmount = 500e18;
|
|
215
|
+
deal(address(currency0Token), address(harness), refundAmount);
|
|
216
|
+
|
|
217
|
+
uint256 makerBalanceBefore = currency0Token.balanceOf(maker);
|
|
218
|
+
harness.refundResidual(poolKey, true, maker, refundAmount);
|
|
219
|
+
assertEq(currency0Token.balanceOf(maker), makerBalanceBefore + refundAmount, "maker should receive ERC20 refund");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function test_refundResidualZeroAmountSkipsTransfer() public {
|
|
223
|
+
uint256 makerBalanceBefore = currency0Token.balanceOf(maker);
|
|
224
|
+
harness.refundResidual(poolKey, true, maker, 0);
|
|
225
|
+
assertEq(currency0Token.balanceOf(maker), makerBalanceBefore, "zero amount should not change balance");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function test_burnAndRefundPaysCurrency0Orders() public {
|
|
229
|
+
poolManager.setModifyLiquidityResponse(int128(80), 0, 0, 0);
|
|
230
|
+
deal(address(currency0Token), address(poolManager), 100e18);
|
|
231
|
+
address recipient = makeAddr("currency0-recipient");
|
|
232
|
+
|
|
233
|
+
uint128 amountOut = harness.burnAndRefund(poolManager, poolKey, ORDER_ID, recipient);
|
|
234
|
+
assertEq(amountOut, 80, "should pay full amount0 delta");
|
|
235
|
+
assertEq(currency0Token.balanceOf(recipient), 80, "recipient receives currency0");
|
|
236
|
+
assertEq(poolManager.takeCalls(), 1, "single take call expected");
|
|
237
|
+
assertEq(Currency.unwrap(poolManager.lastTakeCurrency()), address(currency0Token));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function test_burnAndRefundPaysCurrency1Orders() public {
|
|
241
|
+
harness.setOrderSide(false);
|
|
242
|
+
poolManager.setModifyLiquidityResponse(0, int128(55), 0, 0);
|
|
243
|
+
deal(address(currency1Token), address(poolManager), 100e18);
|
|
244
|
+
address recipient = makeAddr("currency1-recipient");
|
|
245
|
+
|
|
246
|
+
uint128 amountOut = harness.burnAndRefund(poolManager, poolKey, ORDER_ID, recipient);
|
|
247
|
+
assertEq(amountOut, 55, "should pay full amount1 delta");
|
|
248
|
+
assertEq(currency1Token.balanceOf(recipient), 55, "recipient receives currency1");
|
|
249
|
+
assertEq(Currency.unwrap(poolManager.lastTakeCurrency()), address(currency1Token));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function test_burnAndPayoutWithoutReferralRoutesAllProceeds() public {
|
|
253
|
+
poolManager.setModifyLiquidityResponse(0, int128(120), 0, 0);
|
|
254
|
+
deal(address(currency1Token), address(poolManager), 200e18);
|
|
255
|
+
|
|
256
|
+
uint256 balanceBefore = currency1Token.balanceOf(maker);
|
|
257
|
+
(, uint128 makerAmount, uint128 referralAmount) = harness.burnAndPayout(
|
|
258
|
+
poolManager,
|
|
259
|
+
poolKey,
|
|
260
|
+
ORDER_ID,
|
|
261
|
+
address(0),
|
|
262
|
+
address(currency0Token),
|
|
263
|
+
versionLookup
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
assertEq(makerAmount, 120, "maker payout should match liquidity delta");
|
|
267
|
+
assertEq(referralAmount, 0, "referral should be zero when address(0)");
|
|
268
|
+
assertEq(currency1Token.balanceOf(maker), balanceBefore + 120, "maker receives counter-asset");
|
|
269
|
+
assertEq(poolManager.takeCalls(), 1, "single take call expected");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function test_burnAndPayoutSplitsReferralShares() public {
|
|
273
|
+
poolManager.setModifyLiquidityResponse(0, int128(150), 0, int128(30));
|
|
274
|
+
deal(address(currency1Token), address(poolManager), 300e18);
|
|
275
|
+
|
|
276
|
+
address referral = makeAddr("referral");
|
|
277
|
+
|
|
278
|
+
uint256 makerBefore = currency1Token.balanceOf(maker);
|
|
279
|
+
uint256 referralBefore = currency1Token.balanceOf(referral);
|
|
280
|
+
|
|
281
|
+
(, uint128 makerAmount, uint128 referralAmount) = harness.burnAndPayout(
|
|
282
|
+
poolManager,
|
|
283
|
+
poolKey,
|
|
284
|
+
ORDER_ID,
|
|
285
|
+
referral,
|
|
286
|
+
address(currency0Token),
|
|
287
|
+
versionLookup
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
assertEq(makerAmount, 120, "maker receives liquidity minus fees");
|
|
291
|
+
assertEq(referralAmount, 30, "referral receives fee delta");
|
|
292
|
+
assertEq(currency1Token.balanceOf(maker), makerBefore + makerAmount, "maker balance increases");
|
|
293
|
+
assertEq(currency1Token.balanceOf(referral), referralBefore + referralAmount, "referral balance increases");
|
|
294
|
+
assertEq(poolManager.takeCalls(), 2, "maker + referral take calls");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function test_burnAndPayoutHandlesNativeCoinWithoutSwapPath() public {
|
|
298
|
+
poolManager.setModifyLiquidityResponse(0, int128(90), 0, 0);
|
|
299
|
+
deal(address(currency1Token), address(poolManager), 200e18);
|
|
300
|
+
|
|
301
|
+
harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(0), versionLookup);
|
|
302
|
+
|
|
303
|
+
assertEq(poolManager.swapCalls(), 0, "swap path should not be used for native coin");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function test_burnAndPayoutFallsBackWhenLookupReverts() public {
|
|
307
|
+
poolManager.setModifyLiquidityResponse(0, int128(75), 0, 0);
|
|
308
|
+
deal(address(currency1Token), address(poolManager), 100e18);
|
|
309
|
+
versionLookup.setShouldRevert(true);
|
|
310
|
+
|
|
311
|
+
uint256 balanceBefore = currency1Token.balanceOf(maker);
|
|
312
|
+
harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(currency0Token), versionLookup);
|
|
313
|
+
assertEq(currency1Token.balanceOf(maker), balanceBefore + 75, "maker still paid when lookup reverts");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function test_burnAndPayoutUsesSwapPathWhenAvailable() public {
|
|
317
|
+
TestSwapPathCoin swapCoin = new TestSwapPathCoin(Currency.wrap(address(currency1Token)));
|
|
318
|
+
versionLookup.setVersion(address(swapCoin), 4);
|
|
319
|
+
|
|
320
|
+
// Maker has amount0 which must be swapped before payout.
|
|
321
|
+
poolManager.setModifyLiquidityResponse(int128(40), 0, 0, 0);
|
|
322
|
+
poolManager.setSwapResponse(0, int128(40));
|
|
323
|
+
|
|
324
|
+
deal(address(currency1Token), address(poolManager), 200e18);
|
|
325
|
+
|
|
326
|
+
uint256 makerBefore = currency1Token.balanceOf(maker);
|
|
327
|
+
|
|
328
|
+
harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(swapCoin), versionLookup);
|
|
329
|
+
|
|
330
|
+
assertEq(poolManager.swapCalls(), 1, "swap path should execute");
|
|
331
|
+
assertEq(currency1Token.balanceOf(maker), makerBefore + 40, "maker receives swapped currency");
|
|
332
|
+
}
|
|
333
|
+
}
|