@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,1073 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.13;
|
|
3
|
+
|
|
4
|
+
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
|
+
import {SwapWithLimitOrders} from "../src/router/SwapWithLimitOrders.sol";
|
|
6
|
+
import {V3ToV4SwapLib} from "@zoralabs/coins/src/libs/V3ToV4SwapLib.sol";
|
|
7
|
+
import {ISupportsLimitOrderFill} from "@zoralabs/coins/src/interfaces/ISupportsLimitOrderFill.sol";
|
|
8
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
9
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
10
|
+
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
11
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
12
|
+
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
13
|
+
import {LimitOrderConfig} from "../src/libs/SwapLimitOrders.sol";
|
|
14
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
15
|
+
import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
|
|
16
|
+
import {AddressConstants} from "@zoralabs/coins/test/utils/hookmate/constants/AddressConstants.sol";
|
|
17
|
+
|
|
18
|
+
interface IZoraLimitOrderBookFillTickRange {
|
|
19
|
+
function fill(PoolKey calldata key, bool isCurrency0, int24 startTick, int24 endTick, uint256 maxFillCount, address fillReferral) external;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
import {Vm} from "forge-std/Vm.sol";
|
|
23
|
+
|
|
24
|
+
abstract contract SwapWithLimitOrdersTestBase is BaseTest {
|
|
25
|
+
using PoolIdLibrary for PoolKey;
|
|
26
|
+
|
|
27
|
+
function _executeSwapWithLimitOrders(address caller, SwapWithLimitOrders.SwapWithLimitOrdersParams memory params) internal returns (BalanceDelta delta) {
|
|
28
|
+
vm.startPrank(caller);
|
|
29
|
+
|
|
30
|
+
// Handle ETH transfers
|
|
31
|
+
uint256 value = params.inputCurrency == address(0) ? params.inputAmount : 0;
|
|
32
|
+
|
|
33
|
+
// Handle ERC20 approvals via Permit2
|
|
34
|
+
if (params.inputCurrency != address(0)) {
|
|
35
|
+
address permit2 = AddressConstants.getPermit2Address();
|
|
36
|
+
IERC20(params.inputCurrency).approve(permit2, type(uint256).max);
|
|
37
|
+
|
|
38
|
+
// Approve swapWithLimitOrders as spender in Permit2
|
|
39
|
+
IAllowanceTransfer(permit2).approve(params.inputCurrency, address(swapWithLimitOrders), uint160(type(uint160).max), type(uint48).max);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Execute swap with limit order placement
|
|
43
|
+
delta = swapWithLimitOrders.swapWithLimitOrders{value: value}(params);
|
|
44
|
+
|
|
45
|
+
vm.stopPrank();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _buildDirectV4SwapParams(
|
|
49
|
+
address recipient,
|
|
50
|
+
address inputCurrency,
|
|
51
|
+
uint256 inputAmount,
|
|
52
|
+
PoolKey memory targetPool,
|
|
53
|
+
LimitOrderConfig memory limitOrderConfig
|
|
54
|
+
) internal pure returns (SwapWithLimitOrders.SwapWithLimitOrdersParams memory) {
|
|
55
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
56
|
+
v4Route[0] = targetPool;
|
|
57
|
+
|
|
58
|
+
return
|
|
59
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
60
|
+
recipient: recipient,
|
|
61
|
+
limitOrderConfig: limitOrderConfig,
|
|
62
|
+
inputCurrency: inputCurrency,
|
|
63
|
+
inputAmount: inputAmount,
|
|
64
|
+
v3Route: bytes(""),
|
|
65
|
+
v4Route: v4Route,
|
|
66
|
+
minAmountOut: 0
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _buildMultiHopV4SwapParams(
|
|
71
|
+
address recipient,
|
|
72
|
+
address inputCurrency,
|
|
73
|
+
uint256 inputAmount,
|
|
74
|
+
PoolKey[] memory v4Route,
|
|
75
|
+
LimitOrderConfig memory limitOrderConfig
|
|
76
|
+
) internal pure returns (SwapWithLimitOrders.SwapWithLimitOrdersParams memory) {
|
|
77
|
+
return
|
|
78
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
79
|
+
recipient: recipient,
|
|
80
|
+
limitOrderConfig: limitOrderConfig,
|
|
81
|
+
inputCurrency: inputCurrency,
|
|
82
|
+
inputAmount: inputAmount,
|
|
83
|
+
v3Route: bytes(""),
|
|
84
|
+
v4Route: v4Route,
|
|
85
|
+
minAmountOut: 0
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
contract SwapWithLimitOrdersTestNonForked is SwapWithLimitOrdersTestBase {
|
|
91
|
+
function test_swapWithLimitOrders_directV4Swap() public {
|
|
92
|
+
// Test direct V4 swap with single pool in v4Route
|
|
93
|
+
// No V3 route, single pool, limit orders placed and filled
|
|
94
|
+
|
|
95
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
96
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
97
|
+
|
|
98
|
+
// Give buyer ZORA tokens
|
|
99
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
100
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
101
|
+
|
|
102
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
103
|
+
users.buyer, // recipient
|
|
104
|
+
address(zoraToken), // inputCurrency
|
|
105
|
+
inputAmount,
|
|
106
|
+
poolKey, // targetPool
|
|
107
|
+
limitOrderConfig
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
111
|
+
|
|
112
|
+
// Note: orderIds and ordersFilled are no longer returned
|
|
113
|
+
// They can be extracted from events if needed for testing
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function test_swapWithLimitOrders_multiHopV4Swap() public {
|
|
117
|
+
// Test multi-hop V4 swap (e.g., ZORA -> Creator Coin -> Content Coin)
|
|
118
|
+
// Multiple pools in v4Route, limit orders on final coin
|
|
119
|
+
|
|
120
|
+
PoolKey[] memory v4Route = new PoolKey[](2);
|
|
121
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
122
|
+
v4Route[1] = contentCoin.getPoolKey();
|
|
123
|
+
|
|
124
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
125
|
+
|
|
126
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
127
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
128
|
+
|
|
129
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildMultiHopV4SwapParams(
|
|
130
|
+
users.buyer, // recipient
|
|
131
|
+
address(zoraToken), // inputCurrency
|
|
132
|
+
inputAmount,
|
|
133
|
+
v4Route,
|
|
134
|
+
limitOrderConfig
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
vm.recordLogs();
|
|
138
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
139
|
+
|
|
140
|
+
// Extract order IDs from events
|
|
141
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
142
|
+
require(swaps.length > 0, "expected swap event");
|
|
143
|
+
|
|
144
|
+
// Verify orders created
|
|
145
|
+
assertGt(swaps[0].orders.length, 0, "should have created orders");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function test_swapWithLimitOrders_hookCallsFill() public {
|
|
149
|
+
// Test that hook calls zoraLimitOrderBook.fill during swap
|
|
150
|
+
// We use vm.expectCall to verify the fill function is actually called
|
|
151
|
+
|
|
152
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
153
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
154
|
+
|
|
155
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
156
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
157
|
+
|
|
158
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
159
|
+
users.buyer,
|
|
160
|
+
address(zoraToken),
|
|
161
|
+
inputAmount,
|
|
162
|
+
poolKey,
|
|
163
|
+
limitOrderConfig
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Expect that zoraLimitOrderBook.fill is called by the hook during the swap
|
|
167
|
+
// Use our interface to get the correct selector for the tick-range fill overload
|
|
168
|
+
// Only use the selector without parameters for partial matching
|
|
169
|
+
vm.expectCall(address(limitOrderBook), abi.encodeWithSelector(IZoraLimitOrderBookFillTickRange.fill.selector));
|
|
170
|
+
|
|
171
|
+
vm.recordLogs();
|
|
172
|
+
// Execute swap with limit order placement - this should trigger the hook's afterSwap which calls fill
|
|
173
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
174
|
+
|
|
175
|
+
// Extract order IDs from events
|
|
176
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
177
|
+
require(swaps.length > 0, "expected swap event");
|
|
178
|
+
|
|
179
|
+
// Verify orders were created
|
|
180
|
+
assertGt(swaps[0].orders.length, 0, "should have created orders");
|
|
181
|
+
assertEq(swaps[0].orders.length, 5, "should have 5 orders for 5 percentages");
|
|
182
|
+
|
|
183
|
+
// If we reach here without reverting, vm.expectCall passed, meaning fill was called
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function test_swapWithLimitOrders_hookDoesNotSupportLimitOrderFill() public {
|
|
187
|
+
// Test with hook that doesn't implement ISupportsLimitOrderFill
|
|
188
|
+
// Router SHOULD fill orders (backwards compatibility)
|
|
189
|
+
// ordersFilled should reflect actual fills
|
|
190
|
+
// TODO: Test with legacy hook or current hook before interface is added
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function test_swapWithLimitOrders_oldHookWithoutERC165() public {
|
|
194
|
+
// Test with very old hook that doesn't support ERC165 at all
|
|
195
|
+
// Router should handle fills gracefully
|
|
196
|
+
// TODO: Test with very old hook implementation
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function test_limitOrderPlacement_createsExpectedSizes() public {
|
|
200
|
+
// Verify order sizes match percentages from LimitOrderParams
|
|
201
|
+
// Check allocated + unallocated = total coins purchased
|
|
202
|
+
|
|
203
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
204
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
205
|
+
|
|
206
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
207
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
208
|
+
|
|
209
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
210
|
+
users.buyer,
|
|
211
|
+
address(zoraToken),
|
|
212
|
+
inputAmount,
|
|
213
|
+
poolKey,
|
|
214
|
+
limitOrderConfig
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
vm.recordLogs();
|
|
218
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
219
|
+
|
|
220
|
+
// Extract order IDs from events
|
|
221
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
222
|
+
require(swaps.length > 0, "expected swap event");
|
|
223
|
+
|
|
224
|
+
// Verify orders created with expected count
|
|
225
|
+
assertEq(swaps[0].orders.length, 5, "should have 5 orders for 5 percentages");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function test_limitOrderPlacement_alignsTicks() public {
|
|
229
|
+
// Verify order ticks match multiples from LimitOrderParams
|
|
230
|
+
// Check ticks are aligned to tick spacing
|
|
231
|
+
// TODO: Implement tick calculation verification
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function test_limitOrderPlacement_sendsUnallocatedToMaker() public {
|
|
235
|
+
// Verify any unallocated coins go to maker/recipient
|
|
236
|
+
// Check maker balance increases by unallocated amount
|
|
237
|
+
// TODO: Test unallocated coin handling
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function test_limitOrderPlacement_respectsMinSize() public {
|
|
241
|
+
// Test that small purchases below MIN_LIMIT_ORDER_SIZE don't create orders
|
|
242
|
+
// Should still execute swap but skip limit order ladder creation
|
|
243
|
+
// TODO: Test MIN_LIMIT_ORDER_SIZE threshold
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function test_limitOrderPlacement_supportsMultipleOrders() public {
|
|
247
|
+
// Test creating multiple orders with different multiples
|
|
248
|
+
// Verify all orders are tracked in the limit order book
|
|
249
|
+
|
|
250
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
251
|
+
|
|
252
|
+
// Use more multiples to create more orders
|
|
253
|
+
uint256[] memory multiples = new uint256[](5);
|
|
254
|
+
multiples[0] = 2e18;
|
|
255
|
+
multiples[1] = 3e18;
|
|
256
|
+
multiples[2] = 4e18;
|
|
257
|
+
multiples[3] = 5e18;
|
|
258
|
+
multiples[4] = 10e18;
|
|
259
|
+
|
|
260
|
+
uint256[] memory percentages = new uint256[](5);
|
|
261
|
+
percentages[0] = 2000; // 20%
|
|
262
|
+
percentages[1] = 2000; // 20%
|
|
263
|
+
percentages[2] = 2000; // 20%
|
|
264
|
+
percentages[3] = 2000; // 20%
|
|
265
|
+
percentages[4] = 2000; // 20%
|
|
266
|
+
|
|
267
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, multiples, percentages);
|
|
268
|
+
|
|
269
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
270
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
271
|
+
|
|
272
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
273
|
+
users.buyer,
|
|
274
|
+
address(zoraToken),
|
|
275
|
+
inputAmount,
|
|
276
|
+
poolKey,
|
|
277
|
+
limitOrderConfig
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
vm.recordLogs();
|
|
281
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
282
|
+
|
|
283
|
+
// Extract order IDs from events
|
|
284
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
285
|
+
require(swaps.length > 0, "expected swap event");
|
|
286
|
+
|
|
287
|
+
// Verify all orders were created
|
|
288
|
+
assertEq(swaps[0].orders.length, 5, "should have created 5 orders");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function test_orderFilling_hookFillsLimitOrders() public {
|
|
292
|
+
// Verify that hook fills limit orders during subsequent swaps
|
|
293
|
+
// TODO: Revisit this test later - requires debugging hook's fill logic
|
|
294
|
+
// to ensure swaps cross the exact tick ranges where orders are placed
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function test_orderFilling_respectsMaxFillCount() public {
|
|
298
|
+
// Verify only maxFillCount orders are filled
|
|
299
|
+
// Test with more available orders than maxFillCount
|
|
300
|
+
// TODO: Requires setting up pre-existing orders and crossing ticks
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function test_orderFilling_crossedTickRange() public {
|
|
304
|
+
// Verify only orders in crossed tick range are filled
|
|
305
|
+
// Orders outside range should remain unfilled
|
|
306
|
+
// TODO: Requires controlling tick movement during swap
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function test_orderFilling_invertedDirection() public {
|
|
310
|
+
// Verify fill direction is inverted (!isCoinCurrency0)
|
|
311
|
+
// Check correct orders are targeted for filling
|
|
312
|
+
// TODO: Requires verifying fill direction logic
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function test_reverts_emptyV4Route() public {
|
|
316
|
+
// Should revert with EmptyV4Route() when v4Route is empty
|
|
317
|
+
|
|
318
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
319
|
+
|
|
320
|
+
deal(address(zoraToken), users.buyer, DEFAULT_LIMIT_ORDER_AMOUNT);
|
|
321
|
+
|
|
322
|
+
// Create params with empty v4Route
|
|
323
|
+
PoolKey[] memory emptyRoute = new PoolKey[](0);
|
|
324
|
+
|
|
325
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
326
|
+
recipient: users.buyer,
|
|
327
|
+
limitOrderConfig: limitOrderConfig,
|
|
328
|
+
inputCurrency: address(zoraToken),
|
|
329
|
+
inputAmount: DEFAULT_LIMIT_ORDER_AMOUNT,
|
|
330
|
+
v3Route: bytes(""),
|
|
331
|
+
v4Route: emptyRoute,
|
|
332
|
+
minAmountOut: 0
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
vm.prank(users.buyer);
|
|
336
|
+
IERC20(address(zoraToken)).approve(address(swapWithLimitOrders), DEFAULT_LIMIT_ORDER_AMOUNT);
|
|
337
|
+
|
|
338
|
+
vm.expectRevert(SwapWithLimitOrders.EmptyV4Route.selector);
|
|
339
|
+
vm.prank(users.buyer);
|
|
340
|
+
swapWithLimitOrders.swapWithLimitOrders(params);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function test_reverts_insufficientOutputAmount() public {
|
|
344
|
+
// Should revert when final swap output < minAmountOut
|
|
345
|
+
// Slippage protection test
|
|
346
|
+
|
|
347
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
348
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
349
|
+
|
|
350
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
351
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
352
|
+
|
|
353
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
354
|
+
users.buyer,
|
|
355
|
+
address(zoraToken),
|
|
356
|
+
inputAmount,
|
|
357
|
+
poolKey,
|
|
358
|
+
limitOrderConfig
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Set unrealistically high minAmountOut to trigger revert
|
|
362
|
+
params.minAmountOut = type(uint256).max;
|
|
363
|
+
|
|
364
|
+
// Setup Permit2 approval
|
|
365
|
+
vm.startPrank(users.buyer);
|
|
366
|
+
address permit2 = AddressConstants.getPermit2Address();
|
|
367
|
+
IERC20(address(zoraToken)).approve(permit2, type(uint256).max);
|
|
368
|
+
IAllowanceTransfer(permit2).approve(address(zoraToken), address(swapWithLimitOrders), uint160(type(uint160).max), type(uint48).max);
|
|
369
|
+
vm.stopPrank();
|
|
370
|
+
|
|
371
|
+
vm.expectRevert(SwapWithLimitOrders.InsufficientOutputAmount.selector);
|
|
372
|
+
vm.prank(users.buyer);
|
|
373
|
+
swapWithLimitOrders.swapWithLimitOrders(params);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function test_reverts_insufficientInputCurrency() public {
|
|
377
|
+
// Should revert when msg.value < inputAmount (for ETH)
|
|
378
|
+
// With Permit2: Should revert when there's no Permit2 allowance (AllowanceExpired)
|
|
379
|
+
|
|
380
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
381
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
382
|
+
|
|
383
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
384
|
+
|
|
385
|
+
// Give buyer some ZORA but don't set up Permit2 allowance
|
|
386
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
387
|
+
|
|
388
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
389
|
+
users.buyer,
|
|
390
|
+
address(zoraToken), // ZORA
|
|
391
|
+
inputAmount,
|
|
392
|
+
poolKey,
|
|
393
|
+
limitOrderConfig
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
// Don't approve Permit2 or set allowance - will revert with AllowanceExpired
|
|
397
|
+
vm.prank(users.buyer);
|
|
398
|
+
vm.expectRevert(abi.encodeWithSignature("AllowanceExpired(uint256)", 0));
|
|
399
|
+
swapWithLimitOrders.swapWithLimitOrders(params);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function test_reverts_whenV3OutputDoesNotMatchV4Input() public {
|
|
403
|
+
// Craft mismatched V3/V4 routes so validation fails before any swaps execute.
|
|
404
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
405
|
+
|
|
406
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
407
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
408
|
+
|
|
409
|
+
// Encode a V3 path whose output token is contentCoin, which is NOT part of the first V4 pool.
|
|
410
|
+
bytes memory v3Route = abi.encodePacked(address(zoraToken), uint24(3000), address(contentCoin));
|
|
411
|
+
|
|
412
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
413
|
+
recipient: users.buyer,
|
|
414
|
+
limitOrderConfig: limitOrderConfig,
|
|
415
|
+
inputCurrency: address(zoraToken),
|
|
416
|
+
inputAmount: DEFAULT_LIMIT_ORDER_AMOUNT,
|
|
417
|
+
v3Route: v3Route,
|
|
418
|
+
v4Route: v4Route,
|
|
419
|
+
minAmountOut: 0
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
vm.expectRevert(V3ToV4SwapLib.V3RouteDoesNotConnectToV4RouteStart.selector);
|
|
423
|
+
vm.prank(users.buyer);
|
|
424
|
+
swapWithLimitOrders.swapWithLimitOrders(params);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function test_swapUsesMakerAllowanceEvenWhenCallerDiffers() public {
|
|
428
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
429
|
+
|
|
430
|
+
// Now msg.sender pays for the swap, recipient receives outputs and owns orders
|
|
431
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
432
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
433
|
+
|
|
434
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
435
|
+
|
|
436
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
437
|
+
users.buyer, // Recipient who will own the orders
|
|
438
|
+
address(zoraToken),
|
|
439
|
+
inputAmount,
|
|
440
|
+
poolKey,
|
|
441
|
+
limitOrderConfig
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
uint256 buyerBalanceBefore = IERC20(address(zoraToken)).balanceOf(users.buyer);
|
|
445
|
+
|
|
446
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
447
|
+
|
|
448
|
+
// Buyer (msg.sender) paid for the swap and owns the orders
|
|
449
|
+
assertLt(IERC20(address(zoraToken)).balanceOf(users.buyer), buyerBalanceBefore, "buyer should fund the swap");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function test_routerFallsBackWhenHookDoesNotSupportFill() public {
|
|
453
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
454
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
455
|
+
|
|
456
|
+
// Force IERC165 check to return false so router must handle fills.
|
|
457
|
+
bytes memory callData = abi.encodeWithSelector(IERC165.supportsInterface.selector, type(ISupportsLimitOrderFill).interfaceId);
|
|
458
|
+
vm.mockCall(address(poolKey.hooks), callData, abi.encode(false));
|
|
459
|
+
|
|
460
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
461
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
462
|
+
|
|
463
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
464
|
+
users.buyer,
|
|
465
|
+
address(zoraToken),
|
|
466
|
+
inputAmount,
|
|
467
|
+
poolKey,
|
|
468
|
+
limitOrderConfig
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
vm.recordLogs();
|
|
472
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
473
|
+
|
|
474
|
+
// Extract order IDs from events
|
|
475
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
476
|
+
require(swaps.length > 0, "expected swap event");
|
|
477
|
+
|
|
478
|
+
assertGt(swaps[0].orders.length, 0, "orders should still be created when hook lacks fill support");
|
|
479
|
+
|
|
480
|
+
vm.clearMockedCalls();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function test_routerDoesNotFillWhenMaxFillCountIsZero() public {
|
|
484
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
485
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
486
|
+
|
|
487
|
+
uint256 previousMax = limitOrderBook.getMaxFillCount();
|
|
488
|
+
vm.prank(users.factoryOwner);
|
|
489
|
+
limitOrderBook.setMaxFillCount(0);
|
|
490
|
+
|
|
491
|
+
bytes memory callData = abi.encodeWithSelector(IERC165.supportsInterface.selector, type(ISupportsLimitOrderFill).interfaceId);
|
|
492
|
+
vm.mockCall(address(poolKey.hooks), callData, abi.encode(false));
|
|
493
|
+
|
|
494
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
495
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
496
|
+
|
|
497
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
498
|
+
users.buyer,
|
|
499
|
+
address(zoraToken),
|
|
500
|
+
inputAmount,
|
|
501
|
+
poolKey,
|
|
502
|
+
limitOrderConfig
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
506
|
+
uint256 ordersFilled = 0; // Note: ordersFilled no longer returned
|
|
507
|
+
|
|
508
|
+
assertEq(ordersFilled, 0, "max fill count zero should short-circuit fills");
|
|
509
|
+
|
|
510
|
+
vm.clearMockedCalls();
|
|
511
|
+
vm.prank(users.factoryOwner);
|
|
512
|
+
limitOrderBook.setMaxFillCount(previousMax);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function test_partialAllocationRoutesUnallocatedCoinsToMaker() public {
|
|
516
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
517
|
+
|
|
518
|
+
uint256[] memory customMultiples = new uint256[](2);
|
|
519
|
+
customMultiples[0] = 2e18;
|
|
520
|
+
customMultiples[1] = 4e18;
|
|
521
|
+
uint256[] memory customPercentages = new uint256[](2);
|
|
522
|
+
customPercentages[0] = 2500;
|
|
523
|
+
customPercentages[1] = 2500; // keep 50% unallocated
|
|
524
|
+
|
|
525
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, customMultiples, customPercentages);
|
|
526
|
+
|
|
527
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
528
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
529
|
+
|
|
530
|
+
uint256 makerBalanceBefore = IERC20(address(creatorCoin)).balanceOf(users.buyer);
|
|
531
|
+
|
|
532
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
533
|
+
users.buyer,
|
|
534
|
+
address(zoraToken),
|
|
535
|
+
inputAmount,
|
|
536
|
+
poolKey,
|
|
537
|
+
limitOrderConfig
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
vm.recordLogs();
|
|
541
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
542
|
+
|
|
543
|
+
// Extract order IDs from events
|
|
544
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
545
|
+
require(swaps.length > 0, "expected swap event");
|
|
546
|
+
|
|
547
|
+
assertEq(swaps[0].orders.length, 2, "expected two orders for two percentages");
|
|
548
|
+
uint256 makerBalanceAfter = IERC20(address(creatorCoin)).balanceOf(users.buyer);
|
|
549
|
+
assertGt(makerBalanceAfter, makerBalanceBefore, "unallocated coins should be returned to maker");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function test_emitsSwapAndCreateEvents() public {
|
|
553
|
+
PoolKey memory poolKey = creatorCoin.getPoolKey();
|
|
554
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
555
|
+
|
|
556
|
+
uint256 inputAmount = DEFAULT_LIMIT_ORDER_AMOUNT;
|
|
557
|
+
deal(address(zoraToken), users.buyer, inputAmount);
|
|
558
|
+
|
|
559
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildDirectV4SwapParams(
|
|
560
|
+
users.buyer,
|
|
561
|
+
address(zoraToken),
|
|
562
|
+
inputAmount,
|
|
563
|
+
poolKey,
|
|
564
|
+
limitOrderConfig
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
vm.recordLogs();
|
|
568
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
569
|
+
|
|
570
|
+
Vm.Log[] memory logs = vm.getRecordedLogs();
|
|
571
|
+
bool sawCreated;
|
|
572
|
+
bool sawExecuted;
|
|
573
|
+
|
|
574
|
+
for (uint256 i; i < logs.length; ++i) {
|
|
575
|
+
Vm.Log memory log = logs[i];
|
|
576
|
+
if (log.topics.length == 0) continue;
|
|
577
|
+
|
|
578
|
+
if (log.topics[0] == LIMIT_ORDER_CREATED_TOPIC) {
|
|
579
|
+
address maker = address(uint160(uint256(log.topics[1])));
|
|
580
|
+
assertEq(maker, users.buyer, "maker indexed value mismatch");
|
|
581
|
+
(, , , uint128 orderSize, ) = abi.decode(log.data, (bytes32, bool, int24, uint128, bytes32));
|
|
582
|
+
assertGt(orderSize, 0, "order size should be positive");
|
|
583
|
+
sawCreated = true;
|
|
584
|
+
} else if (log.topics[0] == SWAP_WITH_LIMIT_ORDERS_EXECUTED_TOPIC && log.topics.length >= 3) {
|
|
585
|
+
address sender = address(uint160(uint256(log.topics[1])));
|
|
586
|
+
address recipient = address(uint160(uint256(log.topics[2])));
|
|
587
|
+
assertEq(sender, users.buyer, "sender indexed mismatch");
|
|
588
|
+
assertEq(recipient, users.buyer, "recipient indexed mismatch");
|
|
589
|
+
(PoolKey memory loggedPoolKey, int256 delta, , , CreatedOrder[] memory orders) = abi.decode(
|
|
590
|
+
log.data,
|
|
591
|
+
(PoolKey, int256, int24, int24, CreatedOrder[])
|
|
592
|
+
);
|
|
593
|
+
assertEq(Currency.unwrap(loggedPoolKey.currency0), Currency.unwrap(poolKey.currency0), "pool currency0 mismatch");
|
|
594
|
+
assertEq(Currency.unwrap(loggedPoolKey.currency1), Currency.unwrap(poolKey.currency1), "pool currency1 mismatch");
|
|
595
|
+
assertEq(delta, 0, "returned delta should be zero");
|
|
596
|
+
assertGt(orders.length, 0, "should have created orders");
|
|
597
|
+
sawExecuted = true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
assertTrue(sawCreated, "LimitOrdersCreated event missing");
|
|
602
|
+
assertTrue(sawExecuted, "SwapWithLimitOrdersExecuted event missing");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function test_unlockCallback_revertsForExternalCaller() public {
|
|
606
|
+
vm.expectRevert(SwapWithLimitOrders.OnlyPoolManager.selector);
|
|
607
|
+
vm.prank(users.buyer);
|
|
608
|
+
swapWithLimitOrders.unlockCallback(bytes(""));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function test_reverts_nonPositiveCoinDelta() public {
|
|
612
|
+
// Should revert when swap results in zero or negative coin delta
|
|
613
|
+
// TODO: Implement test for non-positive coin delta
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function test_inputCurrency_ETH() public {
|
|
617
|
+
// Test with ETH as input currency (address(0))
|
|
618
|
+
// Verify msg.value is used correctly
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function test_inputCurrency_ERC20() public {
|
|
622
|
+
// Test with ERC20 as input currency
|
|
623
|
+
// Verify token is transferred from maker
|
|
624
|
+
// Verify allowance is checked
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function test_inputCurrency_transfersFromMaker() public {
|
|
628
|
+
// Verify input currency is pulled from limitOrderConfig.maker
|
|
629
|
+
// Not from msg.sender if different
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function test_settlement_ETHInput() public {
|
|
633
|
+
// Verify ETH is settled correctly with poolManager
|
|
634
|
+
// Check settle() called with value
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function test_settlement_ERC20Input() public {
|
|
638
|
+
// Verify ERC20 is settled correctly
|
|
639
|
+
// Check sync() and transfer() and settle() sequence
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function test_settlement_outputToRecipient() public {
|
|
643
|
+
// Verify output currency is sent to params.recipient
|
|
644
|
+
// Check poolManager.take() called correctly
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function test_event_SwapWithLimitOrdersExecuted() public {
|
|
648
|
+
// Verify SwapWithLimitOrdersExecuted event emitted with correct params
|
|
649
|
+
// Check sender, recipient, poolKey, deltas, ticks, orderIds, ordersFilled
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function test_event_LimitOrdersCreated() public {
|
|
653
|
+
// Verify LimitOrdersCreated event emitted
|
|
654
|
+
// Check maker, orderIds, and totalOrderSize
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function test_event_LimitOrdersFilled() public {
|
|
658
|
+
// Verify LimitOrdersFilled event emitted when router handles fills
|
|
659
|
+
// Should NOT emit when hook handles fills
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function test_integration_fullFlow_ERC20_to_ContentCoin() public {
|
|
663
|
+
// End-to-end test: Creator Coin -> Content Coin (V4 only)
|
|
664
|
+
// Verify ERC20 transfer, swap, order placement, filling
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function test_integration_orderPlacedAndFilledInSameCall() public {
|
|
668
|
+
// Verify orders can be placed and immediately filled if tick range crossed
|
|
669
|
+
// Test round-trip efficiency
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function test_zeroUnallocatedCoins() public {
|
|
673
|
+
// Test when 100% of coins are allocated to orders
|
|
674
|
+
// unallocated should be 0
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function test_partialAllocation() public {
|
|
678
|
+
// Test when percentages don't sum to 100%
|
|
679
|
+
// Some coins should remain unallocated
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function test_roundingInOrderSizes() public {
|
|
683
|
+
// Test order size rounding with small percentages
|
|
684
|
+
// Verify no dust orders are created
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function test_maxTickBoundaries() public {
|
|
688
|
+
// Test limit order placement with extreme multiples that hit max/min tick limits
|
|
689
|
+
// Verify ticks are clamped correctly
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
contract SwapWithLimitOrdersTestForked is SwapWithLimitOrdersTestBase {
|
|
694
|
+
// USDC_ADDRESS and ZORA_TOKEN_ADDRESS are inherited from ContractAddresses
|
|
695
|
+
|
|
696
|
+
function setUp() public override {
|
|
697
|
+
super.setUpWithBlockNumber(37877563);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function _encodeV3Path(address tokenA, uint24 feeA, address tokenB, uint24 feeB, address tokenC) internal pure returns (bytes memory) {
|
|
701
|
+
return abi.encodePacked(tokenA, feeA, tokenB, feeB, tokenC);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function _encodeV3PathSingle(address tokenA, uint24 fee, address tokenB) internal pure returns (bytes memory) {
|
|
705
|
+
return abi.encodePacked(tokenA, fee, tokenB);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function _buildV3ToV4SwapParams(
|
|
709
|
+
address recipient,
|
|
710
|
+
uint256 inputAmount,
|
|
711
|
+
bytes memory v3Route,
|
|
712
|
+
PoolKey[] memory v4Route,
|
|
713
|
+
LimitOrderConfig memory limitOrderConfig
|
|
714
|
+
) internal pure returns (SwapWithLimitOrders.SwapWithLimitOrdersParams memory) {
|
|
715
|
+
return
|
|
716
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
717
|
+
recipient: recipient,
|
|
718
|
+
limitOrderConfig: limitOrderConfig,
|
|
719
|
+
inputCurrency: address(0), // ETH
|
|
720
|
+
inputAmount: inputAmount,
|
|
721
|
+
v3Route: v3Route,
|
|
722
|
+
v4Route: v4Route,
|
|
723
|
+
minAmountOut: 0
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function test_swapWithLimitOrders_withV3Route() public {
|
|
728
|
+
// Test V3 + V4 swap (e.g., ETH -> ZORA via V3, then ZORA -> Coin via V4)
|
|
729
|
+
// V3 route populated, V4 route with target pool
|
|
730
|
+
|
|
731
|
+
uint256 inputAmount = 0.1 ether;
|
|
732
|
+
vm.deal(users.buyer, inputAmount);
|
|
733
|
+
|
|
734
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
735
|
+
bytes memory v3Route = _encodeV3Path(
|
|
736
|
+
address(weth),
|
|
737
|
+
3000, // WETH/USDC 0.3%
|
|
738
|
+
USDC_ADDRESS,
|
|
739
|
+
3000, // USDC/ZORA 0.3%
|
|
740
|
+
ZORA_TOKEN_ADDRESS
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
// V4 route: Just the target pool (coin paired with ZORA)
|
|
744
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
745
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
746
|
+
|
|
747
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
748
|
+
|
|
749
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildV3ToV4SwapParams(users.buyer, inputAmount, v3Route, v4Route, limitOrderConfig);
|
|
750
|
+
|
|
751
|
+
vm.recordLogs();
|
|
752
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
753
|
+
|
|
754
|
+
// Extract order IDs from events
|
|
755
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
756
|
+
require(swaps.length > 0, "expected swap event");
|
|
757
|
+
|
|
758
|
+
// Verify orders were created
|
|
759
|
+
assertGt(swaps[0].orders.length, 0, "should have created orders");
|
|
760
|
+
assertEq(swaps[0].orders.length, 5, "should have 5 orders for 5 percentages");
|
|
761
|
+
|
|
762
|
+
// Verify ETH was spent
|
|
763
|
+
assertEq(users.buyer.balance, 0, "buyer should have spent all ETH");
|
|
764
|
+
|
|
765
|
+
// Note: With default percentages (100% allocated), all coins go into orders
|
|
766
|
+
// The buyer will receive coins when orders are filled by subsequent swaps
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function test_swapWithLimitOrders_withV3AndMultiHopV4() public {
|
|
770
|
+
// Test V3 + multi-hop V4 (e.g., ETH -> ZORA -> Creator -> Content)
|
|
771
|
+
// V3 route + multiple V4 pools
|
|
772
|
+
|
|
773
|
+
uint256 inputAmount = 0.1 ether;
|
|
774
|
+
vm.deal(users.buyer, inputAmount);
|
|
775
|
+
|
|
776
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
777
|
+
bytes memory v3Route = _encodeV3Path(
|
|
778
|
+
address(weth),
|
|
779
|
+
3000, // WETH/USDC 0.3%
|
|
780
|
+
USDC_ADDRESS,
|
|
781
|
+
3000, // USDC/ZORA 0.3%
|
|
782
|
+
ZORA_TOKEN_ADDRESS
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
// Multi-hop V4 route: ZORA -> Creator Coin -> Content Coin
|
|
786
|
+
PoolKey[] memory v4Route = new PoolKey[](2);
|
|
787
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
788
|
+
v4Route[1] = contentCoin.getPoolKey();
|
|
789
|
+
|
|
790
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
791
|
+
|
|
792
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildV3ToV4SwapParams(users.buyer, inputAmount, v3Route, v4Route, limitOrderConfig);
|
|
793
|
+
|
|
794
|
+
vm.recordLogs();
|
|
795
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
796
|
+
|
|
797
|
+
// Extract order IDs from events
|
|
798
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
799
|
+
require(swaps.length > 0, "expected swap event");
|
|
800
|
+
|
|
801
|
+
// Verify orders were created
|
|
802
|
+
assertGt(swaps[0].orders.length, 0, "should have created orders");
|
|
803
|
+
assertEq(swaps[0].orders.length, 5, "should have 5 orders for 5 percentages");
|
|
804
|
+
|
|
805
|
+
// Verify ETH was spent
|
|
806
|
+
assertEq(users.buyer.balance, 0, "buyer should have spent all ETH");
|
|
807
|
+
|
|
808
|
+
// Note: With default percentages (100% allocated), all coins go into orders
|
|
809
|
+
// The buyer will receive content coins when orders are filled by subsequent swaps
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function test_RevertWhen_InsufficientInputCurrencyETH() public {
|
|
813
|
+
uint256 inputAmount = 1 ether;
|
|
814
|
+
uint256 insufficientAmount = 0.5 ether;
|
|
815
|
+
|
|
816
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
817
|
+
bytes memory v3Route = _encodeV3Path(
|
|
818
|
+
address(weth),
|
|
819
|
+
3000, // WETH/USDC 0.3%
|
|
820
|
+
USDC_ADDRESS,
|
|
821
|
+
3000, // USDC/ZORA 0.3%
|
|
822
|
+
ZORA_TOKEN_ADDRESS
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
826
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
827
|
+
|
|
828
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
829
|
+
|
|
830
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildV3ToV4SwapParams(users.buyer, inputAmount, v3Route, v4Route, limitOrderConfig);
|
|
831
|
+
|
|
832
|
+
// Should revert with InsufficientInputCurrency
|
|
833
|
+
vm.deal(users.buyer, insufficientAmount);
|
|
834
|
+
vm.expectRevert(abi.encodeWithSelector(V3ToV4SwapLib.InsufficientInputCurrency.selector, inputAmount, insufficientAmount));
|
|
835
|
+
|
|
836
|
+
vm.prank(users.buyer);
|
|
837
|
+
swapWithLimitOrders.swapWithLimitOrders{value: insufficientAmount}(params);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function test_RevertWhen_InsufficientOutputAmount() public {
|
|
841
|
+
uint256 inputAmount = 0.1 ether;
|
|
842
|
+
vm.deal(users.buyer, inputAmount);
|
|
843
|
+
|
|
844
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
845
|
+
bytes memory v3Route = _encodeV3Path(
|
|
846
|
+
address(weth),
|
|
847
|
+
3000, // WETH/USDC 0.3%
|
|
848
|
+
USDC_ADDRESS,
|
|
849
|
+
3000, // USDC/ZORA 0.3%
|
|
850
|
+
ZORA_TOKEN_ADDRESS
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
854
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
855
|
+
|
|
856
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
857
|
+
|
|
858
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildV3ToV4SwapParams(users.buyer, inputAmount, v3Route, v4Route, limitOrderConfig);
|
|
859
|
+
|
|
860
|
+
// Set impossibly high minAmountOut to trigger revert
|
|
861
|
+
params.minAmountOut = type(uint256).max;
|
|
862
|
+
|
|
863
|
+
// Should revert with InsufficientOutputAmount
|
|
864
|
+
vm.expectRevert(SwapWithLimitOrders.InsufficientOutputAmount.selector);
|
|
865
|
+
|
|
866
|
+
vm.prank(users.buyer);
|
|
867
|
+
swapWithLimitOrders.swapWithLimitOrders{value: inputAmount}(params);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function test_reverts_v3RouteDoesNotConnectToV4Route() public {
|
|
871
|
+
// Should revert when V3 output currency doesn't match V4 route start
|
|
872
|
+
// Route validation test
|
|
873
|
+
|
|
874
|
+
uint256 inputAmount = 1 ether;
|
|
875
|
+
vm.deal(users.buyer, inputAmount);
|
|
876
|
+
|
|
877
|
+
// Create V3 path that ends with USDC
|
|
878
|
+
bytes memory v3Route = _encodeV3PathSingle(
|
|
879
|
+
address(weth),
|
|
880
|
+
3000, // WETH/USDC 0.3%
|
|
881
|
+
USDC_ADDRESS
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
// Create V4 route that starts with ZORA (not USDC - mismatch!)
|
|
885
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
886
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
887
|
+
|
|
888
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
889
|
+
|
|
890
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildV3ToV4SwapParams(users.buyer, inputAmount, v3Route, v4Route, limitOrderConfig);
|
|
891
|
+
|
|
892
|
+
// Should revert with V3RouteDoesNotConnectToV4RouteStart
|
|
893
|
+
vm.prank(users.buyer);
|
|
894
|
+
vm.expectRevert(abi.encodeWithSelector(V3ToV4SwapLib.V3RouteDoesNotConnectToV4RouteStart.selector));
|
|
895
|
+
swapWithLimitOrders.swapWithLimitOrders{value: inputAmount}(params);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function test_integration_fullFlow_ETH_to_ZORA_to_CreatorCoin() public {
|
|
899
|
+
// End-to-end test: ETH -> V3(ZORA) -> V4(Creator Coin)
|
|
900
|
+
// Verify all steps: V3 swap, V4 swap, order placement, order filling
|
|
901
|
+
|
|
902
|
+
uint256 inputAmount = 0.1 ether;
|
|
903
|
+
vm.deal(users.buyer, inputAmount);
|
|
904
|
+
|
|
905
|
+
// Create V3 path: ETH -> USDC -> ZORA
|
|
906
|
+
bytes memory v3Route = _encodeV3Path(
|
|
907
|
+
address(weth),
|
|
908
|
+
3000, // WETH/USDC 0.3%
|
|
909
|
+
USDC_ADDRESS,
|
|
910
|
+
3000, // USDC/ZORA 0.3%
|
|
911
|
+
ZORA_TOKEN_ADDRESS
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
// V4 route: ZORA -> Creator Coin
|
|
915
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
916
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
917
|
+
|
|
918
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
919
|
+
|
|
920
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = _buildV3ToV4SwapParams(users.buyer, inputAmount, v3Route, v4Route, limitOrderConfig);
|
|
921
|
+
|
|
922
|
+
vm.recordLogs();
|
|
923
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
924
|
+
|
|
925
|
+
// Extract order IDs from events
|
|
926
|
+
SwapExecutedLog[] memory swaps = _decodeSwapExecutedLogs(vm.getRecordedLogs());
|
|
927
|
+
require(swaps.length > 0, "expected swap event");
|
|
928
|
+
|
|
929
|
+
// Verify orders were created
|
|
930
|
+
assertGt(swaps[0].orders.length, 0, "should have created orders");
|
|
931
|
+
assertEq(swaps[0].orders.length, 5, "should have 5 orders for 5 percentages");
|
|
932
|
+
|
|
933
|
+
// Verify ETH was spent
|
|
934
|
+
assertEq(users.buyer.balance, 0, "buyer should have spent all ETH");
|
|
935
|
+
|
|
936
|
+
// Note: With default percentages (100% allocated), all coins go into orders
|
|
937
|
+
// The buyer will receive coins when orders are filled by subsequent swaps
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function test_swapWithLimitOrders_ERC20toV3toV4_singleHop() public {
|
|
941
|
+
// Give buyer USDC tokens
|
|
942
|
+
uint256 usdcAmount = 100 * 10 ** 6; // 100 USDC
|
|
943
|
+
deal(USDC_ADDRESS, users.buyer, usdcAmount);
|
|
944
|
+
|
|
945
|
+
// Create V3 path: USDC -> ZORA
|
|
946
|
+
bytes memory v3Route = _encodeV3PathSingle(USDC_ADDRESS, 3000, ZORA_TOKEN_ADDRESS);
|
|
947
|
+
|
|
948
|
+
// Create V4 route: ZORA -> Coin
|
|
949
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
950
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
951
|
+
|
|
952
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
953
|
+
|
|
954
|
+
// Build swap params with USDC as input currency
|
|
955
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
956
|
+
recipient: users.buyer,
|
|
957
|
+
limitOrderConfig: limitOrderConfig,
|
|
958
|
+
inputCurrency: USDC_ADDRESS, // ERC20 input
|
|
959
|
+
inputAmount: usdcAmount,
|
|
960
|
+
v3Route: v3Route,
|
|
961
|
+
v4Route: v4Route,
|
|
962
|
+
minAmountOut: 0
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// Execute swap - this should FAIL with V3RouteCannotStartWithInputCurrency
|
|
966
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
967
|
+
|
|
968
|
+
// If we get here after implementation, verify USDC was spent and coins received
|
|
969
|
+
assertEq(IERC20(USDC_ADDRESS).balanceOf(users.buyer), 0, "buyer should have spent all USDC");
|
|
970
|
+
assertGt(IERC20(address(creatorCoin)).balanceOf(users.buyer), 0, "buyer should have received coins");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function test_swapWithLimitOrders_ZORAtoV4() public {
|
|
974
|
+
// Give buyer ZORA tokens
|
|
975
|
+
uint256 zoraAmount = 10 ether;
|
|
976
|
+
deal(ZORA_TOKEN_ADDRESS, users.buyer, zoraAmount);
|
|
977
|
+
|
|
978
|
+
// Create V4 route: ZORA -> Coin (no V3 swap)
|
|
979
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
980
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
981
|
+
|
|
982
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
983
|
+
|
|
984
|
+
// Build swap params with ZORA as input currency, no V3 route
|
|
985
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
986
|
+
recipient: users.buyer,
|
|
987
|
+
limitOrderConfig: limitOrderConfig,
|
|
988
|
+
inputCurrency: ZORA_TOKEN_ADDRESS, // ERC20 input
|
|
989
|
+
inputAmount: zoraAmount,
|
|
990
|
+
v3Route: bytes(""), // No V3 swap
|
|
991
|
+
v4Route: v4Route,
|
|
992
|
+
minAmountOut: 0
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Execute swap - this should PASS
|
|
996
|
+
_executeSwapWithLimitOrders(users.buyer, params);
|
|
997
|
+
|
|
998
|
+
// Verify ZORA was spent and coins received
|
|
999
|
+
assertEq(IERC20(ZORA_TOKEN_ADDRESS).balanceOf(users.buyer), 0, "buyer should have spent all ZORA");
|
|
1000
|
+
assertGt(IERC20(address(creatorCoin)).balanceOf(users.buyer), 0, "buyer should have received coins");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function test_reverts_ERC20InputWithoutApproval() public {
|
|
1004
|
+
// Give buyer USDC tokens
|
|
1005
|
+
uint256 usdcAmount = 100 * 10 ** 6; // 100 USDC
|
|
1006
|
+
deal(USDC_ADDRESS, users.buyer, usdcAmount);
|
|
1007
|
+
|
|
1008
|
+
// Create V3 path: USDC -> ZORA
|
|
1009
|
+
bytes memory v3Route = _encodeV3PathSingle(USDC_ADDRESS, 3000, ZORA_TOKEN_ADDRESS);
|
|
1010
|
+
|
|
1011
|
+
// Create V4 route: ZORA -> Coin
|
|
1012
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
1013
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
1014
|
+
|
|
1015
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
1016
|
+
|
|
1017
|
+
// Build swap params
|
|
1018
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
1019
|
+
recipient: users.buyer,
|
|
1020
|
+
limitOrderConfig: limitOrderConfig,
|
|
1021
|
+
inputCurrency: USDC_ADDRESS,
|
|
1022
|
+
inputAmount: usdcAmount,
|
|
1023
|
+
v3Route: v3Route,
|
|
1024
|
+
v4Route: v4Route,
|
|
1025
|
+
minAmountOut: 0
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// Execute without approval - should revert
|
|
1029
|
+
vm.startPrank(users.buyer);
|
|
1030
|
+
// Don't approve - just call directly
|
|
1031
|
+
vm.expectRevert();
|
|
1032
|
+
swapWithLimitOrders.swapWithLimitOrders(params);
|
|
1033
|
+
vm.stopPrank();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function test_reverts_ERC20InputInsufficientBalance() public {
|
|
1037
|
+
// Give buyer only 50 USDC but try to swap 100 USDC
|
|
1038
|
+
uint256 balanceAmount = 50 * 10 ** 6; // 50 USDC
|
|
1039
|
+
uint256 swapAmount = 100 * 10 ** 6; // 100 USDC
|
|
1040
|
+
deal(USDC_ADDRESS, users.buyer, balanceAmount);
|
|
1041
|
+
|
|
1042
|
+
// Create V3 path: USDC -> ZORA
|
|
1043
|
+
bytes memory v3Route = _encodeV3PathSingle(USDC_ADDRESS, 3000, ZORA_TOKEN_ADDRESS);
|
|
1044
|
+
|
|
1045
|
+
// Create V4 route: ZORA -> Coin
|
|
1046
|
+
PoolKey[] memory v4Route = new PoolKey[](1);
|
|
1047
|
+
v4Route[0] = creatorCoin.getPoolKey();
|
|
1048
|
+
|
|
1049
|
+
LimitOrderConfig memory limitOrderConfig = _prepareLimitOrderParams(users.buyer, _defaultMultiples(), _defaultPercentages());
|
|
1050
|
+
|
|
1051
|
+
// Build swap params with more than available balance
|
|
1052
|
+
SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = SwapWithLimitOrders.SwapWithLimitOrdersParams({
|
|
1053
|
+
recipient: users.buyer,
|
|
1054
|
+
limitOrderConfig: limitOrderConfig,
|
|
1055
|
+
inputCurrency: USDC_ADDRESS,
|
|
1056
|
+
inputAmount: swapAmount,
|
|
1057
|
+
v3Route: v3Route,
|
|
1058
|
+
v4Route: v4Route,
|
|
1059
|
+
minAmountOut: 0
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Execute with insufficient balance - should revert from Permit2
|
|
1063
|
+
vm.startPrank(users.buyer);
|
|
1064
|
+
address permit2 = AddressConstants.getPermit2Address();
|
|
1065
|
+
IERC20(USDC_ADDRESS).approve(permit2, type(uint256).max);
|
|
1066
|
+
IAllowanceTransfer(permit2).approve(USDC_ADDRESS, address(swapWithLimitOrders), uint160(type(uint160).max), type(uint48).max);
|
|
1067
|
+
|
|
1068
|
+
// With Permit2, when transferring more than balance, the underlying ERC20 transferFrom fails
|
|
1069
|
+
vm.expectRevert("TRANSFER_FROM_FAILED");
|
|
1070
|
+
swapWithLimitOrders.swapWithLimitOrders(params);
|
|
1071
|
+
vm.stopPrank();
|
|
1072
|
+
}
|
|
1073
|
+
}
|