@zoralabs/limit-orders 0.2.5 → 0.2.7

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.
Files changed (57) hide show
  1. package/.abi-stability +0 -21
  2. package/.turbo/turbo-build$colon$js.log +52 -50
  3. package/CHANGELOG.md +21 -6
  4. package/abis/ContractVersionBase.json +15 -0
  5. package/abis/ICoin.json +5 -0
  6. package/abis/IVersionedContract.json +15 -0
  7. package/abis/SwapWithLimitOrders.json +13 -0
  8. package/abis/ZoraLimitOrderBook.json +13 -0
  9. package/cache/solidity-files-cache.json +1 -1
  10. package/dist/index.cjs +14 -0
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +14 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/wagmiGenerated.d.ts +20 -0
  15. package/dist/wagmiGenerated.d.ts.map +1 -1
  16. package/out/BytesLib.sol/BytesLib.json +1 -1
  17. package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
  18. package/out/CoinConstants.sol/CoinConstants.json +1 -1
  19. package/out/ContractVersionBase.sol/ContractVersionBase.json +1 -0
  20. package/out/DopplerMath.sol/DopplerMath.json +1 -1
  21. package/out/ICoin.sol/ICoin.json +1 -1
  22. package/out/ICoin.sol/IHasCoinType.json +1 -1
  23. package/out/ICoin.sol/IHasPoolKey.json +1 -1
  24. package/out/ICoin.sol/IHasSwapPath.json +1 -1
  25. package/out/ICoin.sol/IHasTotalSupplyForPositions.json +1 -1
  26. package/out/ISwapRouter.sol/ISwapRouter.json +1 -1
  27. package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -1
  28. package/out/IVersionedContract.sol/IVersionedContract.json +1 -0
  29. package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -1
  30. package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.json +1 -1
  31. package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -1
  32. package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -1
  33. package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -1
  34. package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -1
  35. package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -1
  36. package/out/LimitOrderQueues.sol/LimitOrderQueues.json +1 -1
  37. package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -1
  38. package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -1
  39. package/out/LimitOrderViews.sol/LimitOrderViews.json +1 -1
  40. package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
  41. package/out/Path.sol/Path.json +1 -1
  42. package/out/Permit2Payments.sol/Permit2Payments.json +1 -1
  43. package/out/PermittedCallers.sol/PermittedCallers.json +1 -1
  44. package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
  45. package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
  46. package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -1
  47. package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
  48. package/out/build-info/{c9f7ee5726bfbd48.json → fcf3d601c2943dea.json} +1 -1
  49. package/package/wagmiGenerated.ts +14 -0
  50. package/package.json +3 -3
  51. package/src/ZoraLimitOrderBook.sol +4 -1
  52. package/src/libs/LimitOrderFill.sol +4 -5
  53. package/src/router/SwapWithLimitOrders.sol +2 -1
  54. package/src/version/ContractVersionBase.sol +14 -0
  55. package/test/DebugSwapWithLimitOrders.t.sol +196 -0
  56. package/test/LimitOrderAccessControl.t.sol +8 -2
  57. package/test/LimitOrderFill.t.sol +118 -0
@@ -1 +1 @@
1
- {"id":"c9f7ee5726bfbd48","source_id_to_path":{"0":"node_modules/@openzeppelin/contracts/access/Ownable.sol","1":"node_modules/@openzeppelin/contracts/access/Ownable2Step.sol","2":"node_modules/@openzeppelin/contracts/interfaces/IERC1363.sol","3":"node_modules/@openzeppelin/contracts/interfaces/IERC165.sol","4":"node_modules/@openzeppelin/contracts/interfaces/IERC20.sol","5":"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol","6":"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol","7":"node_modules/@openzeppelin/contracts/utils/Context.sol","8":"node_modules/@openzeppelin/contracts/utils/TransientSlot.sol","9":"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol","10":"node_modules/@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol","11":"node_modules/@uniswap/permit2/src/interfaces/IEIP712.sol","12":"node_modules/@uniswap/permit2/src/libraries/SafeCast160.sol","13":"node_modules/@uniswap/v4-core/src/interfaces/IExtsload.sol","14":"node_modules/@uniswap/v4-core/src/interfaces/IExttload.sol","15":"node_modules/@uniswap/v4-core/src/interfaces/IHooks.sol","16":"node_modules/@uniswap/v4-core/src/interfaces/IPoolManager.sol","17":"node_modules/@uniswap/v4-core/src/interfaces/IProtocolFees.sol","18":"node_modules/@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol","19":"node_modules/@uniswap/v4-core/src/interfaces/external/IERC6909Claims.sol","20":"node_modules/@uniswap/v4-core/src/libraries/BitMath.sol","21":"node_modules/@uniswap/v4-core/src/libraries/CurrencyReserves.sol","22":"node_modules/@uniswap/v4-core/src/libraries/CustomRevert.sol","23":"node_modules/@uniswap/v4-core/src/libraries/FixedPoint128.sol","24":"node_modules/@uniswap/v4-core/src/libraries/FullMath.sol","25":"node_modules/@uniswap/v4-core/src/libraries/LiquidityMath.sol","26":"node_modules/@uniswap/v4-core/src/libraries/Lock.sol","27":"node_modules/@uniswap/v4-core/src/libraries/NonzeroDeltaCount.sol","28":"node_modules/@uniswap/v4-core/src/libraries/Position.sol","29":"node_modules/@uniswap/v4-core/src/libraries/SafeCast.sol","30":"node_modules/@uniswap/v4-core/src/libraries/StateLibrary.sol","31":"node_modules/@uniswap/v4-core/src/libraries/TickBitmap.sol","32":"node_modules/@uniswap/v4-core/src/libraries/TickMath.sol","33":"node_modules/@uniswap/v4-core/src/libraries/TransientStateLibrary.sol","34":"node_modules/@uniswap/v4-core/src/types/BalanceDelta.sol","35":"node_modules/@uniswap/v4-core/src/types/BeforeSwapDelta.sol","36":"node_modules/@uniswap/v4-core/src/types/Currency.sol","37":"node_modules/@uniswap/v4-core/src/types/PoolId.sol","38":"node_modules/@uniswap/v4-core/src/types/PoolKey.sol","39":"node_modules/@uniswap/v4-core/src/types/PoolOperation.sol","40":"node_modules/@uniswap/v4-periphery/src/libraries/PathKey.sol","41":"node_modules/@zoralabs/coins/src/interfaces/ICoin.sol","42":"node_modules/@zoralabs/coins/src/interfaces/IDeployedCoinVersionLookup.sol","43":"node_modules/@zoralabs/coins/src/interfaces/IDopplerErrors.sol","44":"node_modules/@zoralabs/coins/src/interfaces/IERC7572.sol","45":"node_modules/@zoralabs/coins/src/interfaces/IHasRewardsRecipients.sol","46":"node_modules/@zoralabs/coins/src/interfaces/IMsgSender.sol","47":"node_modules/@zoralabs/coins/src/interfaces/ISupportsLimitOrderFill.sol","48":"node_modules/@zoralabs/coins/src/interfaces/ISwapPathRouter.sol","49":"node_modules/@zoralabs/coins/src/interfaces/ISwapRouter.sol","50":"node_modules/@zoralabs/coins/src/interfaces/IUpgradeableV4Hook.sol","51":"node_modules/@zoralabs/coins/src/interfaces/IWETH.sol","52":"node_modules/@zoralabs/coins/src/interfaces/IZoraHookRegistry.sol","53":"node_modules/@zoralabs/coins/src/interfaces/IZoraLimitOrderBookCoinsInterface.sol","54":"node_modules/@zoralabs/coins/src/interfaces/IZoraV4CoinHook.sol","55":"node_modules/@zoralabs/coins/src/libs/CoinCommon.sol","56":"node_modules/@zoralabs/coins/src/libs/CoinConfigurationVersions.sol","57":"node_modules/@zoralabs/coins/src/libs/CoinConstants.sol","58":"node_modules/@zoralabs/coins/src/libs/DopplerMath.sol","59":"node_modules/@zoralabs/coins/src/libs/UniV4SwapToCurrency.sol","60":"node_modules/@zoralabs/coins/src/libs/V3ToV4SwapLib.sol","61":"node_modules/@zoralabs/coins/src/types/LpPosition.sol","62":"node_modules/@zoralabs/coins/src/types/PoolConfiguration.sol","63":"node_modules/@zoralabs/coins/src/utils/uniswap/BitMath.sol","64":"node_modules/@zoralabs/coins/src/utils/uniswap/CustomRevert.sol","65":"node_modules/@zoralabs/coins/src/utils/uniswap/FixedPoint96.sol","66":"node_modules/@zoralabs/coins/src/utils/uniswap/FullMath.sol","67":"node_modules/@zoralabs/coins/src/utils/uniswap/LiquidityAmounts.sol","68":"node_modules/@zoralabs/coins/src/utils/uniswap/SafeCast.sol","69":"node_modules/@zoralabs/coins/src/utils/uniswap/SqrtPriceMath.sol","70":"node_modules/@zoralabs/coins/src/utils/uniswap/TickMath.sol","71":"node_modules/@zoralabs/coins/src/utils/uniswap/UnsafeMath.sol","72":"node_modules/@zoralabs/shared-contracts/src/interfaces/uniswap/ISwapRouter.sol","73":"node_modules/@zoralabs/shared-contracts/src/interfaces/uniswap/IUniswapV3SwapCallback.sol","74":"node_modules/@zoralabs/shared-contracts/src/libs/UniswapV3/BytesLib.sol","75":"node_modules/@zoralabs/shared-contracts/src/libs/UniswapV3/Path.sol","76":"src/IZoraLimitOrderBook.sol","77":"src/ZoraLimitOrderBook.sol","78":"src/access/PermittedCallers.sol","79":"src/libs/LimitOrderBitmap.sol","80":"src/libs/LimitOrderCommon.sol","81":"src/libs/LimitOrderCreate.sol","82":"src/libs/LimitOrderFill.sol","83":"src/libs/LimitOrderLiquidity.sol","84":"src/libs/LimitOrderQueues.sol","85":"src/libs/LimitOrderStorage.sol","86":"src/libs/LimitOrderTypes.sol","87":"src/libs/LimitOrderViews.sol","88":"src/libs/LimitOrderWithdraw.sol","89":"src/libs/Permit2Payments.sol","90":"src/libs/SwapLimitOrders.sol","91":"src/router/SwapWithLimitOrders.sol"},"language":"Solidity"}
1
+ {"id":"fcf3d601c2943dea","source_id_to_path":{"0":"node_modules/@openzeppelin/contracts/access/Ownable.sol","1":"node_modules/@openzeppelin/contracts/access/Ownable2Step.sol","2":"node_modules/@openzeppelin/contracts/interfaces/IERC1363.sol","3":"node_modules/@openzeppelin/contracts/interfaces/IERC165.sol","4":"node_modules/@openzeppelin/contracts/interfaces/IERC20.sol","5":"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol","6":"node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol","7":"node_modules/@openzeppelin/contracts/utils/Context.sol","8":"node_modules/@openzeppelin/contracts/utils/TransientSlot.sol","9":"node_modules/@openzeppelin/contracts/utils/introspection/IERC165.sol","10":"node_modules/@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol","11":"node_modules/@uniswap/permit2/src/interfaces/IEIP712.sol","12":"node_modules/@uniswap/permit2/src/libraries/SafeCast160.sol","13":"node_modules/@uniswap/v4-core/src/interfaces/IExtsload.sol","14":"node_modules/@uniswap/v4-core/src/interfaces/IExttload.sol","15":"node_modules/@uniswap/v4-core/src/interfaces/IHooks.sol","16":"node_modules/@uniswap/v4-core/src/interfaces/IPoolManager.sol","17":"node_modules/@uniswap/v4-core/src/interfaces/IProtocolFees.sol","18":"node_modules/@uniswap/v4-core/src/interfaces/external/IERC20Minimal.sol","19":"node_modules/@uniswap/v4-core/src/interfaces/external/IERC6909Claims.sol","20":"node_modules/@uniswap/v4-core/src/libraries/BitMath.sol","21":"node_modules/@uniswap/v4-core/src/libraries/CurrencyReserves.sol","22":"node_modules/@uniswap/v4-core/src/libraries/CustomRevert.sol","23":"node_modules/@uniswap/v4-core/src/libraries/FixedPoint128.sol","24":"node_modules/@uniswap/v4-core/src/libraries/FullMath.sol","25":"node_modules/@uniswap/v4-core/src/libraries/LiquidityMath.sol","26":"node_modules/@uniswap/v4-core/src/libraries/Lock.sol","27":"node_modules/@uniswap/v4-core/src/libraries/NonzeroDeltaCount.sol","28":"node_modules/@uniswap/v4-core/src/libraries/Position.sol","29":"node_modules/@uniswap/v4-core/src/libraries/SafeCast.sol","30":"node_modules/@uniswap/v4-core/src/libraries/StateLibrary.sol","31":"node_modules/@uniswap/v4-core/src/libraries/TickBitmap.sol","32":"node_modules/@uniswap/v4-core/src/libraries/TickMath.sol","33":"node_modules/@uniswap/v4-core/src/libraries/TransientStateLibrary.sol","34":"node_modules/@uniswap/v4-core/src/types/BalanceDelta.sol","35":"node_modules/@uniswap/v4-core/src/types/BeforeSwapDelta.sol","36":"node_modules/@uniswap/v4-core/src/types/Currency.sol","37":"node_modules/@uniswap/v4-core/src/types/PoolId.sol","38":"node_modules/@uniswap/v4-core/src/types/PoolKey.sol","39":"node_modules/@uniswap/v4-core/src/types/PoolOperation.sol","40":"node_modules/@uniswap/v4-periphery/src/libraries/PathKey.sol","41":"node_modules/@zoralabs/coins/src/interfaces/ICoin.sol","42":"node_modules/@zoralabs/coins/src/interfaces/IDeployedCoinVersionLookup.sol","43":"node_modules/@zoralabs/coins/src/interfaces/IDopplerErrors.sol","44":"node_modules/@zoralabs/coins/src/interfaces/IERC7572.sol","45":"node_modules/@zoralabs/coins/src/interfaces/IHasRewardsRecipients.sol","46":"node_modules/@zoralabs/coins/src/interfaces/IMsgSender.sol","47":"node_modules/@zoralabs/coins/src/interfaces/ISupportsLimitOrderFill.sol","48":"node_modules/@zoralabs/coins/src/interfaces/ISwapPathRouter.sol","49":"node_modules/@zoralabs/coins/src/interfaces/ISwapRouter.sol","50":"node_modules/@zoralabs/coins/src/interfaces/IUpgradeableV4Hook.sol","51":"node_modules/@zoralabs/coins/src/interfaces/IWETH.sol","52":"node_modules/@zoralabs/coins/src/interfaces/IZoraHookRegistry.sol","53":"node_modules/@zoralabs/coins/src/interfaces/IZoraLimitOrderBookCoinsInterface.sol","54":"node_modules/@zoralabs/coins/src/interfaces/IZoraV4CoinHook.sol","55":"node_modules/@zoralabs/coins/src/libs/CoinCommon.sol","56":"node_modules/@zoralabs/coins/src/libs/CoinConfigurationVersions.sol","57":"node_modules/@zoralabs/coins/src/libs/CoinConstants.sol","58":"node_modules/@zoralabs/coins/src/libs/DopplerMath.sol","59":"node_modules/@zoralabs/coins/src/libs/UniV4SwapToCurrency.sol","60":"node_modules/@zoralabs/coins/src/libs/V3ToV4SwapLib.sol","61":"node_modules/@zoralabs/coins/src/types/LpPosition.sol","62":"node_modules/@zoralabs/coins/src/types/PoolConfiguration.sol","63":"node_modules/@zoralabs/coins/src/utils/uniswap/BitMath.sol","64":"node_modules/@zoralabs/coins/src/utils/uniswap/CustomRevert.sol","65":"node_modules/@zoralabs/coins/src/utils/uniswap/FixedPoint96.sol","66":"node_modules/@zoralabs/coins/src/utils/uniswap/FullMath.sol","67":"node_modules/@zoralabs/coins/src/utils/uniswap/LiquidityAmounts.sol","68":"node_modules/@zoralabs/coins/src/utils/uniswap/SafeCast.sol","69":"node_modules/@zoralabs/coins/src/utils/uniswap/SqrtPriceMath.sol","70":"node_modules/@zoralabs/coins/src/utils/uniswap/TickMath.sol","71":"node_modules/@zoralabs/coins/src/utils/uniswap/UnsafeMath.sol","72":"node_modules/@zoralabs/shared-contracts/src/interfaces/IVersionedContract.sol","73":"node_modules/@zoralabs/shared-contracts/src/interfaces/uniswap/ISwapRouter.sol","74":"node_modules/@zoralabs/shared-contracts/src/interfaces/uniswap/IUniswapV3SwapCallback.sol","75":"node_modules/@zoralabs/shared-contracts/src/libs/UniswapV3/BytesLib.sol","76":"node_modules/@zoralabs/shared-contracts/src/libs/UniswapV3/Path.sol","77":"src/IZoraLimitOrderBook.sol","78":"src/ZoraLimitOrderBook.sol","79":"src/access/PermittedCallers.sol","80":"src/libs/LimitOrderBitmap.sol","81":"src/libs/LimitOrderCommon.sol","82":"src/libs/LimitOrderCreate.sol","83":"src/libs/LimitOrderFill.sol","84":"src/libs/LimitOrderLiquidity.sol","85":"src/libs/LimitOrderQueues.sol","86":"src/libs/LimitOrderStorage.sol","87":"src/libs/LimitOrderTypes.sol","88":"src/libs/LimitOrderViews.sol","89":"src/libs/LimitOrderWithdraw.sol","90":"src/libs/Permit2Payments.sol","91":"src/libs/SwapLimitOrders.sol","92":"src/router/SwapWithLimitOrders.sol","93":"src/version/ContractVersionBase.sol"},"language":"Solidity"}
@@ -66,6 +66,13 @@ export const swapWithLimitOrdersABI = [
66
66
  outputs: [],
67
67
  stateMutability: 'nonpayable',
68
68
  },
69
+ {
70
+ type: 'function',
71
+ inputs: [],
72
+ name: 'contractVersion',
73
+ outputs: [{ name: '', internalType: 'string', type: 'string' }],
74
+ stateMutability: 'pure',
75
+ },
69
76
  {
70
77
  type: 'function',
71
78
  inputs: [],
@@ -431,6 +438,13 @@ export const zoraLimitOrderBookABI = [
431
438
  outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
432
439
  stateMutability: 'view',
433
440
  },
441
+ {
442
+ type: 'function',
443
+ inputs: [],
444
+ name: 'contractVersion',
445
+ outputs: [{ name: '', internalType: 'string', type: 'string' }],
446
+ stateMutability: 'pure',
447
+ },
434
448
  {
435
449
  type: 'function',
436
450
  inputs: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/limit-orders",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -34,10 +34,10 @@
34
34
  "tsup": "^7.2.0",
35
35
  "tsx": "^3.13.0",
36
36
  "typescript": "^5.2.2",
37
- "viem": "^2.21.18",
37
+ "viem": "2.22.12",
38
38
  "@zoralabs/shared-contracts": "^0.0.5",
39
39
  "@zoralabs/shared-scripts": "^0.0.0",
40
- "@zoralabs/coins": "^2.5.0",
40
+ "@zoralabs/coins": "^2.6.0",
41
41
  "@zoralabs/tsconfig": "^0.0.1"
42
42
  },
43
43
  "scripts": {
@@ -21,8 +21,9 @@ import {LimitOrderWithdraw} from "./libs/LimitOrderWithdraw.sol";
21
21
  import {LimitOrderViews} from "./libs/LimitOrderViews.sol";
22
22
  import {LimitOrderTypes} from "./libs/LimitOrderTypes.sol";
23
23
  import {PermittedCallers} from "./access/PermittedCallers.sol";
24
+ import {ContractVersionBase} from "./version/ContractVersionBase.sol";
24
25
 
25
- contract ZoraLimitOrderBook is IZoraLimitOrderBook, PermittedCallers {
26
+ contract ZoraLimitOrderBook is IZoraLimitOrderBook, ContractVersionBase, PermittedCallers {
26
27
  IPoolManager public immutable poolManager;
27
28
  IDeployedCoinVersionLookup public immutable zoraCoinVersionLookup;
28
29
  IZoraHookRegistry public immutable zoraHookRegistry;
@@ -38,6 +39,8 @@ contract ZoraLimitOrderBook is IZoraLimitOrderBook, PermittedCallers {
38
39
  zoraCoinVersionLookup = IDeployedCoinVersionLookup(zoraCoinVersionLookup_);
39
40
  zoraHookRegistry = IZoraHookRegistry(zoraHookRegistry_);
40
41
  weth = weth_;
42
+
43
+ LimitOrderStorage.layout().maxFillCount = 50;
41
44
  }
42
45
 
43
46
  /// @inheritdoc IZoraLimitOrderBook
@@ -180,19 +180,18 @@ library LimitOrderFill {
180
180
  ) private {
181
181
  order.status = LimitOrderTypes.OrderStatus.FILLED;
182
182
 
183
- // Get both input and output currencies
183
+ // Get the input currency (the coin being sold in the order)
184
184
  address coinIn = LimitOrderCommon.getOrderCoin(key, order.isCurrency0);
185
- address coinOut = LimitOrderCommon.getOrderCoin(key, !order.isCurrency0);
186
185
 
187
- // Pass output currency to burnAndPayout (not input currency)
188
- // This ensures payout uses the OUTPUT coin's configured payout path
186
+ // Pass input currency to burnAndPayout (not output currency)
187
+ // This ensures payout uses the INPUT coin's configured payout path for multi-hop resolution
189
188
  (Currency coinOutCurrency, uint128 makerAmount, uint128 referralAmount) = LimitOrderLiquidity.burnAndPayout(
190
189
  ctx.poolManager,
191
190
  key,
192
191
  order,
193
192
  orderId,
194
193
  fillReferral,
195
- coinOut,
194
+ coinIn,
196
195
  ctx.versionLookup,
197
196
  ctx.weth
198
197
  );
@@ -28,6 +28,7 @@ import {V3ToV4SwapLib} from "@zoralabs/coins/src/libs/V3ToV4SwapLib.sol";
28
28
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
29
29
  import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
30
30
  import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
31
+ import {ContractVersionBase} from "../version/ContractVersionBase.sol";
31
32
 
32
33
  /// @title SwapWithLimitOrders
33
34
  /// @notice Standalone router contract that executes swaps with automatic limit order placement and filling.
@@ -36,7 +37,7 @@ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"
36
37
  /// Users call swapWithLimitOrders() directly, which triggers the unlock callback flow.
37
38
  /// Uses Permit2 for token approvals, matching the universal-router pattern.
38
39
  /// @author oveddan
39
- contract SwapWithLimitOrders is Ownable2Step, IMsgSender {
40
+ contract SwapWithLimitOrders is ContractVersionBase, Ownable2Step, IMsgSender {
40
41
  using SafeERC20 for IERC20;
41
42
  using BalanceDeltaLibrary for BalanceDelta;
42
43
  using CurrencyLibrary for Currency;
@@ -0,0 +1,14 @@
1
+ // This file is automatically generated by code; do not manually update
2
+ // SPDX-License-Identifier: MIT
3
+ pragma solidity ^0.8.23;
4
+
5
+ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersionedContract.sol";
6
+
7
+ /// @title ContractVersionBase
8
+ /// @notice Base contract for versioning contracts
9
+ contract ContractVersionBase is IVersionedContract {
10
+ /// @notice The version of the contract
11
+ function contractVersion() external pure override returns (string memory) {
12
+ return "0.2.7";
13
+ }
14
+ }
@@ -0,0 +1,196 @@
1
+ // SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6
+ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
7
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
8
+ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
9
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
10
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
+ import {SwapWithLimitOrders} from "../src/router/SwapWithLimitOrders.sol";
12
+ import {IZoraLimitOrderBook} from "../src/IZoraLimitOrderBook.sol";
13
+ import {ISwapRouter} from "@zoralabs/shared-contracts/interfaces/uniswap/ISwapRouter.sol";
14
+ import {LimitOrderConfig} from "../src/libs/SwapLimitOrders.sol";
15
+ import {ZoraLimitOrderBook} from "../src/ZoraLimitOrderBook.sol";
16
+ import {IUniversalRouter} from "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol";
17
+ import {UniV4SwapHelper} from "@zoralabs/coins/src/libs/UniV4SwapHelper.sol";
18
+ import {IPermit2} from "permit2/src/interfaces/IPermit2.sol";
19
+ import {HooksDeployment} from "@zoralabs/coins/src/libs/HooksDeployment.sol";
20
+ import {ITrustedMsgSenderProviderLookup} from "@zoralabs/coins/src/interfaces/ITrustedMsgSenderProviderLookup.sol";
21
+
22
+ /// @notice Standalone fork test to reproduce a failing swapWithLimitOrders call on Base mainnet.
23
+ /// Uses deployCodeTo to etch the modified SwapWithLimitOrders (with console.log) onto the deployed router address.
24
+ contract DebugSwapWithLimitOrders is Test {
25
+ address constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
26
+ address payable constant ZORA_ROUTER = payable(0x77777777Eb762Cf86F634763e79d17dE44330887);
27
+ address constant UNIVERSAL_ROUTER = 0x6fF5693b99212Da76ad316178A184AB56D299b43;
28
+
29
+ // Constructor args read from deployed contract
30
+ address constant POOL_MANAGER = 0x498581fF718922c3f8e6A244956aF099B2652b2b;
31
+ address constant LIMIT_ORDER_BOOK = 0x7777777C783bAD88daCaf9A19E04238341E4497B;
32
+ address constant SWAP_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481;
33
+ address constant OWNER = 0x004d6611884B4A661749B64b2ADc78505c3e1AB3;
34
+
35
+ // Limit order book constructor args
36
+ address constant ZORA_COIN_VERSION_LOOKUP = 0x777777751622c0d3258f214F9DF38E35BF45baF3;
37
+ address constant ZORA_HOOK_REGISTRY = 0x777777C4c14b133858c3982D41Dbf02509fc18d7;
38
+ address constant LOB_OWNER = 0x004d6611884B4A661749B64b2ADc78505c3e1AB3;
39
+ address constant WETH = 0x4200000000000000000000000000000000000006;
40
+
41
+ address constant CALLER = 0x67dc68F5d1b9d48fdA1784b309d7d3d609876196;
42
+ address constant INPUT_CURRENCY = 0xdD9B9E272b8812D441eB15da566DEB6b86816E6a;
43
+
44
+ // Pool key addresses
45
+ address constant CURRENCY0 = 0x63c4AcFADEcd03F30874a95868d895891DB9a4FE;
46
+ address constant CURRENCY1_POOL1 = 0xdD9B9E272b8812D441eB15da566DEB6b86816E6a;
47
+ address constant CURRENCY1_POOL2 = 0xD05f95b389bbe3679A4F0D77caEFf265422Af2cb;
48
+ address constant HOOKS = 0xC8d077444625eB300A427a6dfB2b1DBf9b159040;
49
+ address constant HOOK_UPGRADE_GATE = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2;
50
+ address constant TRUSTED_MSG_SENDER_LOOKUP = 0x2183ECF857Ade81c7fAcE1dbAa98C381520619c4;
51
+
52
+ uint256 constant INPUT_AMOUNT = 58189250000000000000000000;
53
+ uint256 constant MIN_AMOUNT_OUT = 52576599598595138086707355;
54
+
55
+ function setUp() public {
56
+ vm.createSelectFork("base", 41766928);
57
+
58
+ // Etch the modified SwapWithLimitOrders (with console.log) onto the deployed router address
59
+ deployCodeTo(
60
+ "SwapWithLimitOrders.sol:SwapWithLimitOrders",
61
+ abi.encode(IPoolManager(POOL_MANAGER), IZoraLimitOrderBook(LIMIT_ORDER_BOOK), ISwapRouter(SWAP_ROUTER), PERMIT2, OWNER),
62
+ ZORA_ROUTER
63
+ );
64
+
65
+ // Etch the modified ZoraLimitOrderBook (with console.log) onto the deployed LOB address
66
+ deployCodeTo(
67
+ "ZoraLimitOrderBook.sol:ZoraLimitOrderBook",
68
+ abi.encode(POOL_MANAGER, ZORA_COIN_VERSION_LOOKUP, ZORA_HOOK_REGISTRY, LOB_OWNER, WETH),
69
+ LIMIT_ORDER_BOOK
70
+ );
71
+
72
+ // Deploy new hook from current source and etch onto the existing hook address
73
+ bytes memory hookCreationCode = HooksDeployment.makeHookCreationCode(
74
+ POOL_MANAGER,
75
+ ZORA_COIN_VERSION_LOOKUP, // ZoraFactory implements IDeployedCoinVersionLookup
76
+ ITrustedMsgSenderProviderLookup(TRUSTED_MSG_SENDER_LOOKUP),
77
+ HOOK_UPGRADE_GATE,
78
+ LIMIT_ORDER_BOOK,
79
+ ZORA_HOOK_REGISTRY
80
+ );
81
+ (IHooks newHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), hookCreationCode, bytes32(0));
82
+ vm.etch(HOOKS, address(newHook).code);
83
+
84
+ // Also etch onto the hook used by intermediate pools in the payout swap path
85
+ // (coinA/creator pool uses a different hook address on-chain)
86
+ vm.etch(0xd61A675F8a0c67A73DC3B54FB7318B4D91409040, address(newHook).code);
87
+ }
88
+
89
+ function test_debugFailingSwapWithLimitOrders() public {
90
+ // Fund caller to original INPUT_AMOUNT by transferring from PoolManager (holds V4 liquidity)
91
+ // Cannot use deal() as it corrupts proxy coin storage
92
+ uint256 callerBalance = IERC20(INPUT_CURRENCY).balanceOf(CALLER);
93
+ if (callerBalance < INPUT_AMOUNT) {
94
+ uint256 needed = INPUT_AMOUNT - callerBalance;
95
+ vm.prank(POOL_MANAGER);
96
+ IERC20(INPUT_CURRENCY).transfer(CALLER, needed);
97
+ }
98
+ uint256 inputAmount = INPUT_AMOUNT;
99
+
100
+ vm.startPrank(CALLER);
101
+
102
+ // Set up Permit2 approvals
103
+ IERC20(INPUT_CURRENCY).approve(PERMIT2, type(uint256).max);
104
+ IAllowanceTransfer(PERMIT2).approve(INPUT_CURRENCY, ZORA_ROUTER, type(uint160).max, type(uint48).max);
105
+
106
+ // Build V4 route: 2 pools (multi-hop)
107
+ PoolKey[] memory v4Route = new PoolKey[](2);
108
+
109
+ v4Route[0] = PoolKey({
110
+ currency0: Currency.wrap(CURRENCY0),
111
+ currency1: Currency.wrap(CURRENCY1_POOL1),
112
+ fee: 10000,
113
+ tickSpacing: int24(200),
114
+ hooks: IHooks(HOOKS)
115
+ });
116
+
117
+ v4Route[1] = PoolKey({
118
+ currency0: Currency.wrap(CURRENCY0),
119
+ currency1: Currency.wrap(CURRENCY1_POOL2),
120
+ fee: 10000,
121
+ tickSpacing: int24(200),
122
+ hooks: IHooks(HOOKS)
123
+ });
124
+
125
+ uint256[] memory multiples = new uint256[](5);
126
+ multiples[0] = 2e18;
127
+ multiples[1] = 4e18;
128
+ multiples[2] = 8e18;
129
+ multiples[3] = 16e18;
130
+ multiples[4] = 32e18;
131
+
132
+ uint256[] memory percentages = new uint256[](5);
133
+ for (uint256 i = 0; i < 5; i++) {
134
+ percentages[i] = 2000;
135
+ }
136
+
137
+ LimitOrderConfig memory limitOrderConfig = LimitOrderConfig({multiples: multiples, percentages: percentages});
138
+
139
+ SwapWithLimitOrders.SwapWithLimitOrdersParams memory params = SwapWithLimitOrders.SwapWithLimitOrdersParams({
140
+ recipient: CALLER,
141
+ limitOrderConfig: limitOrderConfig,
142
+ inputCurrency: INPUT_CURRENCY,
143
+ inputAmount: inputAmount,
144
+ v3Route: "", // empty V3 route
145
+ v4Route: v4Route,
146
+ minAmountOut: 0 // no slippage check for debugging
147
+ });
148
+
149
+ // Execute the swap
150
+ SwapWithLimitOrders(ZORA_ROUTER).swapWithLimitOrders(params);
151
+
152
+ vm.stopPrank();
153
+ }
154
+
155
+ /// @notice Test a vanilla single-hop swap through pool 0 via the universal router
156
+ /// to see if the hook's afterSwap works correctly when used normally
157
+ function test_vanillaUniversalRouterSwapPool0() public {
158
+ // Fund caller to original INPUT_AMOUNT
159
+ uint256 callerBalance = IERC20(INPUT_CURRENCY).balanceOf(CALLER);
160
+ if (callerBalance < INPUT_AMOUNT) {
161
+ uint256 needed = INPUT_AMOUNT - callerBalance;
162
+ vm.prank(POOL_MANAGER);
163
+ IERC20(INPUT_CURRENCY).transfer(CALLER, needed);
164
+ }
165
+
166
+ vm.startPrank(CALLER);
167
+
168
+ // Approve Permit2 for universal router
169
+ UniV4SwapHelper.approveTokenWithPermit2(IPermit2(PERMIT2), UNIVERSAL_ROUTER, INPUT_CURRENCY, type(uint160).max, type(uint48).max);
170
+
171
+ // Build pool key for pool 0
172
+ PoolKey memory pool0 = PoolKey({
173
+ currency0: Currency.wrap(CURRENCY0),
174
+ currency1: Currency.wrap(CURRENCY1_POOL1),
175
+ fee: 10000,
176
+ tickSpacing: int24(200),
177
+ hooks: IHooks(HOOKS)
178
+ });
179
+
180
+ uint128 swapAmount = uint128(INPUT_AMOUNT);
181
+
182
+ // Single-hop swap: INPUT_CURRENCY (currency1) -> CURRENCY0
183
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
184
+ INPUT_CURRENCY, // currencyIn
185
+ swapAmount, // amountIn
186
+ CURRENCY0, // currencyOut
187
+ 0, // minAmountOut
188
+ pool0,
189
+ "" // hookData
190
+ );
191
+
192
+ IUniversalRouter(UNIVERSAL_ROUTER).execute(commands, inputs, block.timestamp + 1 days);
193
+
194
+ vm.stopPrank();
195
+ }
196
+ }
@@ -332,9 +332,15 @@ contract LimitOrderAccessControlTest is BaseTest {
332
332
  payable(address(limitOrderBook)).transfer(1 wei);
333
333
  }
334
334
 
335
+ // Test: maxFillCount defaults to 50 after construction
336
+ function test_maxFillCount_defaultsTo50() public {
337
+ // Max fill count should be initialized to 50 in constructor
338
+ assertEq(limitOrderBook.getMaxFillCount(), 50);
339
+ }
340
+
335
341
  // Test: setMaxFillCount - owner can set
336
342
  function test_setMaxFillCount_ownerCanSet() public {
337
- // Initially max fill count should be 50 (set in BaseTest)
343
+ // Initially max fill count should be 50 (set in constructor)
338
344
  assertEq(limitOrderBook.getMaxFillCount(), 50);
339
345
 
340
346
  // Owner (this contract) should be able to set it
@@ -350,7 +356,7 @@ contract LimitOrderAccessControlTest is BaseTest {
350
356
  vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, unauthorizedUser));
351
357
  limitOrderBook.setMaxFillCount(20);
352
358
 
353
- // Verify value hasn't changed (still 50 from BaseTest)
359
+ // Verify value hasn't changed (still 50 from constructor)
354
360
  assertEq(limitOrderBook.getMaxFillCount(), 50);
355
361
  }
356
362
 
@@ -764,6 +764,124 @@ contract LimitOrderFillTest is BaseTest {
764
764
  }
765
765
  }
766
766
 
767
+ /// @notice Tests that content coin orders pay out in the ultimate backing currency (ZORA)
768
+ /// @dev This test demonstrates the bug where the wrong coin is passed to burnAndPayout.
769
+ /// For a content coin (backed by creator coin, which is backed by ZORA):
770
+ /// - The multi-hop payout path should be: contentCoin → creatorCoin → ZORA
771
+ /// - Previously failed because coinOut (creator coin) was passed instead of coinIn (content coin)
772
+ /// - The validation check rejected creator coin's path (leads to ZORA) when payoutCurrency is creator coin
773
+ function test_contentCoinOrderPaysOutInZora() public {
774
+ // Setup: content coin is backed by creator coin, which is backed by zoraToken
775
+ PoolKey memory contentPoolKey = contentCoin.getPoolKey();
776
+
777
+ // Verify pool structure: content coin paired with creator coin
778
+ bool contentIsCurrency0 = Currency.unwrap(contentPoolKey.currency0) == address(contentCoin);
779
+ address poolCurrency = contentIsCurrency0 ? Currency.unwrap(contentPoolKey.currency1) : Currency.unwrap(contentPoolKey.currency0);
780
+ assertEq(poolCurrency, address(creatorCoin), "content coin should be paired with creator coin");
781
+
782
+ // Step 1: Acquire content coin through proper swap path (ZORA -> creator coin -> content coin)
783
+ uint128 initialZoraAmount = 1000 ether;
784
+ deal(address(zoraToken), users.seller, initialZoraAmount);
785
+
786
+ // Swap ZORA -> creator coin
787
+ _swapSomeCurrencyForCoin(ICoin(address(creatorCoin)), address(zoraToken), initialZoraAmount, users.seller);
788
+ uint128 creatorCoinBalance = uint128(creatorCoin.balanceOf(users.seller));
789
+ assertGt(creatorCoinBalance, 0, "should have creator coin after swap");
790
+
791
+ // Swap creator coin -> content coin
792
+ _swapSomeCurrencyForCoin(ICoin(address(contentCoin)), address(creatorCoin), creatorCoinBalance, users.seller);
793
+ uint256 contentCoinBalance = contentCoin.balanceOf(users.seller);
794
+ assertGt(contentCoinBalance, 0, "should have content coin after swap");
795
+
796
+ // Step 2: Create a limit order to sell content coin
797
+ bool isCurrency0 = contentIsCurrency0;
798
+ uint256 orderSize = contentCoinBalance / 2; // Use half for the order
799
+
800
+ (uint256[] memory orderSizes, int24[] memory orderTicks) = _buildDeterministicOrdersForPool(contentPoolKey, isCurrency0, 1, orderSize);
801
+
802
+ vm.prank(users.seller);
803
+ IERC20(address(contentCoin)).approve(address(limitOrderBook), orderSize);
804
+
805
+ vm.recordLogs();
806
+ vm.prank(users.seller);
807
+ limitOrderBook.create(contentPoolKey, isCurrency0, orderSizes, orderTicks, users.seller);
808
+ CreatedOrderLog[] memory created = _decodeCreatedLogs(vm.getRecordedLogs());
809
+ assertEq(created.length, 1, "should create 1 order");
810
+
811
+ // Step 3: Move price to cross the order (with auto-fill disabled so we can fill manually)
812
+ _movePriceBeyondTicksForContentCoin(created);
813
+
814
+ // Step 4: Record balances before fill
815
+ uint256 sellerZoraBefore = zoraToken.balanceOf(users.seller);
816
+ uint256 sellerCreatorCoinBefore = creatorCoin.balanceOf(users.seller);
817
+
818
+ // Step 5: Fill the order
819
+ (int24 startTick, int24 endTick) = _tickWindow(created, contentPoolKey);
820
+ vm.recordLogs();
821
+ limitOrderBook.fill(contentPoolKey, isCurrency0, startTick, endTick, 1, address(0));
822
+ FilledOrderLog[] memory fills = _decodeFilledLogs(vm.getRecordedLogs());
823
+
824
+ assertEq(fills.length, 1, "should fill 1 order");
825
+ assertEq(fills[0].coinIn, address(contentCoin), "coinIn should be content coin");
826
+
827
+ // Step 6: Verify payout - with the fix, seller receives ZORA (multi-hop path: content → creator → ZORA)
828
+ uint256 sellerZoraAfter = zoraToken.balanceOf(users.seller);
829
+ uint256 sellerCreatorCoinAfter = creatorCoin.balanceOf(users.seller);
830
+
831
+ // Seller should receive ZORA via multi-hop path
832
+ assertGt(sellerZoraAfter, sellerZoraBefore, "seller should receive ZORA via multi-hop path");
833
+ assertEq(sellerCreatorCoinAfter, sellerCreatorCoinBefore, "seller should NOT receive creator coin directly");
834
+ }
835
+
836
+ function _buildDeterministicOrdersForPool(
837
+ PoolKey memory key,
838
+ bool isCurrency0,
839
+ uint256 rungCount,
840
+ uint256 orderSize
841
+ ) internal view returns (uint256[] memory sizes, int24[] memory ticks) {
842
+ sizes = new uint256[](rungCount);
843
+ ticks = new int24[](rungCount);
844
+
845
+ int24 baseTick = _alignedTick(_currentTick(key), key.tickSpacing);
846
+ for (uint256 i; i < rungCount; ++i) {
847
+ sizes[i] = orderSize;
848
+ ticks[i] = _alignedTickForOrder(isCurrency0, baseTick, key.tickSpacing, i);
849
+ }
850
+ }
851
+
852
+ function _movePriceBeyondTicksForContentCoin(CreatedOrderLog[] memory created) internal {
853
+ if (created.length == 0) return;
854
+
855
+ uint256 previousMax = _disableAutoFill();
856
+
857
+ address mover = makeAddr("content-price-mover");
858
+
859
+ // For currency0 orders: need tick UP (currentTick >= tickUpper)
860
+ // For currency1 orders: need tick DOWN (currentTick <= tickLower)
861
+ bool needTickUp = created[0].isCurrency0;
862
+
863
+ // To move price, we need to do swaps through the proper path
864
+ // First get creator coin by swapping ZORA
865
+ uint128 swapAmount = 5000 ether;
866
+ deal(address(zoraToken), mover, swapAmount * 2);
867
+ _swapSomeCurrencyForCoin(ICoin(address(creatorCoin)), address(zoraToken), swapAmount, mover);
868
+ uint128 creatorBalance = uint128(creatorCoin.balanceOf(mover));
869
+
870
+ if (needTickUp) {
871
+ // Need tick to go up - buy content coin with creator coin
872
+ _swapSomeCurrencyForCoin(ICoin(address(contentCoin)), address(creatorCoin), creatorBalance, mover);
873
+ } else {
874
+ // Need tick to go down - first get content coin, then sell it
875
+ _swapSomeCurrencyForCoin(ICoin(address(contentCoin)), address(creatorCoin), creatorBalance / 2, mover);
876
+ uint128 contentBalance = uint128(contentCoin.balanceOf(mover));
877
+ if (contentBalance > 0) {
878
+ _swapSomeCoinForCurrency(ICoin(address(contentCoin)), address(creatorCoin), contentBalance, mover);
879
+ }
880
+ }
881
+
882
+ _restoreAutoFill(previousMax);
883
+ }
884
+
767
885
  /// @notice Tests fill() with maxFillCount=0 (line 86-88)
768
886
  /// @dev This tests the branch: if (maxFillCount == 0) maxFillCount = getMaxFillCount();
769
887
  function test_fill_maxFillCountZero_usesDefault() public {