@zoralabs/limit-orders 0.2.2 → 0.2.4

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 (37) hide show
  1. package/.abi-stability +70 -0
  2. package/.turbo/turbo-build$colon$js.log +40 -40
  3. package/CHANGELOG.md +23 -0
  4. package/cache/solidity-files-cache.json +1 -1
  5. package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
  6. package/out/CoinConstants.sol/CoinConstants.json +1 -1
  7. package/out/DopplerMath.sol/DopplerMath.json +1 -1
  8. package/out/ISetLimitOrderConfig.sol/ISetLimitOrderConfig.json +1 -1
  9. package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -1
  10. package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -1
  11. package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -1
  12. package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -1
  13. package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -1
  14. package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
  15. package/out/PermittedCallers.sol/PermittedCallers.json +1 -1
  16. package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
  17. package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
  18. package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -1
  19. package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -1
  20. package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
  21. package/out/build-info/{37e0124d88d60569.json → 68b2e124c4a02a45.json} +1 -1
  22. package/package.json +4 -2
  23. package/src/access/PermittedCallers.sol +5 -1
  24. package/src/libs/LimitOrderBitmap.sol +0 -1
  25. package/src/libs/LimitOrderCommon.sol +14 -0
  26. package/src/libs/LimitOrderCreate.sol +6 -0
  27. package/src/libs/LimitOrderFill.sol +3 -20
  28. package/src/libs/LimitOrderLiquidity.sol +61 -38
  29. package/src/libs/LimitOrderWithdraw.sol +2 -6
  30. package/src/libs/SwapLimitOrders.sol +13 -7
  31. package/src/router/SwapWithLimitOrders.sol +1 -0
  32. package/test/LimitOrderLiquidityPayouts.t.sol +14 -14
  33. package/test/SwapWithLimitOrdersRouter.t.sol +4 -0
  34. package/test/unit/LimitOrderCreateUnit.t.sol +17 -17
  35. package/test/unit/SwapLimitOrdersUnit.t.sol +47 -86
  36. package/test/unit/SwapLimitOrdersValidation.t.sol +3 -11
  37. package/test/utils/BaseTest.sol +1 -1
@@ -1 +1 @@
1
- {"id":"37e0124d88d60569","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/ISetLimitOrderConfig.sol","92":"src/router/SwapWithLimitOrders.sol"},"language":"Solidity"}
1
+ {"id":"68b2e124c4a02a45","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/ISetLimitOrderConfig.sol","92":"src/router/SwapWithLimitOrders.sol"},"language":"Solidity"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/limit-orders",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "viem": "^2.21.18",
38
38
  "@zoralabs/shared-contracts": "^0.0.5",
39
39
  "@zoralabs/shared-scripts": "^0.0.0",
40
- "@zoralabs/coins": "^2.4.1",
40
+ "@zoralabs/coins": "^2.5.0",
41
41
  "@zoralabs/tsconfig": "^0.0.1"
42
42
  },
43
43
  "scripts": {
@@ -50,6 +50,8 @@
50
50
  "prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol'",
51
51
  "prettier:write": "prettier --write 'src/**/*.sol' 'test/**/*.sol'",
52
52
  "test": "forge test -vv",
53
+ "abi-check:check": "../../scripts/abi-check.sh check",
54
+ "abi-check:generate": "../../scripts/abi-check.sh generate",
53
55
  "test-gas": "forge test --gas-report",
54
56
  "update-contract-version": "pnpm exec update-contract-version",
55
57
  "wagmi:generate": "pnpm run build:contracts:minimal && wagmi generate && pnpm exec rename-generated-abi-casing ./package/wagmiGenerated.ts"
@@ -19,10 +19,14 @@ abstract contract PermittedCallers is Ownable2Step {
19
19
  }
20
20
 
21
21
  modifier onlyPermitted() {
22
- require(_isPermitted(msg.sender), CallerNotPermitted());
22
+ _onlyPermitted();
23
23
  _;
24
24
  }
25
25
 
26
+ function _onlyPermitted() internal view {
27
+ require(_isPermitted(msg.sender), CallerNotPermitted());
28
+ }
29
+
26
30
  function isPermittedCaller(address caller) public view returns (bool) {
27
31
  return _isPermitted(caller);
28
32
  }
@@ -8,7 +8,6 @@
8
8
  pragma solidity ^0.8.23;
9
9
 
10
10
  import {TickBitmap} from "@uniswap/v4-core/src/libraries/TickBitmap.sol";
11
- import {LimitOrderTypes} from "./LimitOrderTypes.sol";
12
11
 
13
12
  library LimitOrderBitmap {
14
13
  function setIfFirst(mapping(int16 => uint256) storage bm, int24 tick, int24 spacing, uint256 sizeBefore) internal {
@@ -9,6 +9,9 @@ pragma solidity ^0.8.28;
9
9
 
10
10
  import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
11
11
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
12
+ import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
13
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
14
+ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
12
15
 
13
16
  import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
14
17
  import {LimitOrderStorage} from "./LimitOrderStorage.sol";
@@ -18,6 +21,8 @@ import {LimitOrderBitmap} from "./LimitOrderBitmap.sol";
18
21
  import {LimitOrderCreate} from "./LimitOrderCreate.sol";
19
22
 
20
23
  library LimitOrderCommon {
24
+ using PoolIdLibrary for PoolKey;
25
+
21
26
  /// @dev Currency0 orders are executed when the price rises to the upper tick.
22
27
  /// Currency1 orders are executed when the price falls to the lower tick.
23
28
  function getOrderTick(LimitOrderTypes.LimitOrder storage order) internal view returns (int24) {
@@ -28,6 +33,15 @@ library LimitOrderCommon {
28
33
  return Currency.unwrap(isCurrency0 ? key.currency0 : key.currency1);
29
34
  }
30
35
 
36
+ /// @dev Returns true if the pool tick has fully crossed the order's range.
37
+ function hasCrossed(LimitOrderTypes.LimitOrder storage order, int24 currentTick) internal view returns (bool) {
38
+ return order.isCurrency0 ? currentTick >= order.tickUpper : currentTick < order.tickLower;
39
+ }
40
+
41
+ function currentPoolTick(IPoolManager poolManager, PoolKey memory key) internal view returns (int24 tick) {
42
+ (, tick, , ) = StateLibrary.getSlot0(poolManager, key.toId());
43
+ }
44
+
31
45
  function recordCreation(
32
46
  LimitOrderStorage.Layout storage state,
33
47
  LimitOrderCreate.CreateContext memory ctx,
@@ -223,6 +223,8 @@ library LimitOrderCreate {
223
223
  ModifyLiquidityParams({
224
224
  tickLower: params.tickLower,
225
225
  tickUpper: params.tickUpper,
226
+ // This is safe because liquidity is always positive
227
+ //forge-lint: disable-next-line(unsafe-typecast)
226
228
  liquidityDelta: int256(uint256(params.liquidity)),
227
229
  salt: params.orderId
228
230
  }),
@@ -233,8 +235,12 @@ library LimitOrderCreate {
233
235
  int128 amount1 = delta.amount1();
234
236
 
235
237
  if (isCurrency0) {
238
+ // This is safe because amount0 is always negative
239
+ //forge-lint: disable-next-line(unsafe-typecast)
236
240
  realizedSize = amount0 < 0 ? uint128(uint256(int256(-amount0))) : 0;
237
241
  } else {
242
+ // This is safe because amount1 is always negative
243
+ //forge-lint: disable-next-line(unsafe-typecast)
238
244
  realizedSize = amount1 < 0 ? uint128(uint256(int256(-amount1))) : 0;
239
245
  }
240
246
 
@@ -10,8 +10,6 @@ pragma solidity ^0.8.28;
10
10
  import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
11
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
12
12
  import {TickBitmap} from "@uniswap/v4-core/src/libraries/TickBitmap.sol";
13
- import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
14
- import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
15
13
  import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
16
14
 
17
15
  import {LimitOrderStorage} from "./LimitOrderStorage.sol";
@@ -19,13 +17,10 @@ import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
19
17
  import {LimitOrderTypes} from "./LimitOrderTypes.sol";
20
18
  import {LimitOrderLiquidity} from "./LimitOrderLiquidity.sol";
21
19
  import {LimitOrderCommon} from "./LimitOrderCommon.sol";
22
- import {LimitOrderViews} from "./LimitOrderViews.sol";
23
20
  import {CoinCommon} from "@zoralabs/coins/src/libs/CoinCommon.sol";
24
21
  import {IDeployedCoinVersionLookup} from "@zoralabs/coins/src/interfaces/IDeployedCoinVersionLookup.sol";
25
22
 
26
23
  library LimitOrderFill {
27
- using PoolIdLibrary for PoolKey;
28
-
29
24
  struct Context {
30
25
  IPoolManager poolManager;
31
26
  IDeployedCoinVersionLookup versionLookup;
@@ -67,12 +62,9 @@ library LimitOrderFill {
67
62
  order.status == LimitOrderTypes.OrderStatus.OPEN &&
68
63
  order.poolKeyHash == poolKeyHash &&
69
64
  order.isCurrency0 == isCurrency0 &&
70
- order.createdEpoch < currentEpoch
65
+ order.createdEpoch < currentEpoch &&
66
+ LimitOrderCommon.hasCrossed(order, LimitOrderCommon.currentPoolTick(ctx.poolManager, key))
71
67
  ) {
72
- if (!_hasCrossed(order, _currentPoolTick(ctx.poolManager, key))) {
73
- continue;
74
- }
75
-
76
68
  int24 orderTick = LimitOrderCommon.getOrderTick(order);
77
69
  LimitOrderTypes.Queue storage tickQueue = tickQueues[orderTick];
78
70
 
@@ -160,7 +152,7 @@ library LimitOrderFill {
160
152
  if (order.createdEpoch == currentEpoch) {
161
153
  break;
162
154
  }
163
- if (!_hasCrossed(order, _currentPoolTick(ctx.poolManager, data.poolKey))) {
155
+ if (!LimitOrderCommon.hasCrossed(order, LimitOrderCommon.currentPoolTick(ctx.poolManager, data.poolKey))) {
164
156
  return;
165
157
  }
166
158
 
@@ -221,13 +213,4 @@ library LimitOrderFill {
221
213
  orderId
222
214
  );
223
215
  }
224
-
225
- function _currentPoolTick(IPoolManager poolManager, PoolKey memory key) private view returns (int24 tick) {
226
- (, tick, , ) = StateLibrary.getSlot0(poolManager, key.toId());
227
- }
228
-
229
- /// @dev Returns true if the pool tick has fully crossed the order's range.
230
- function _hasCrossed(LimitOrderTypes.LimitOrder storage order, int24 currentTick) private view returns (bool) {
231
- return order.isCurrency0 ? currentTick >= order.tickUpper : currentTick <= order.tickLower;
232
- }
233
216
  }
@@ -45,6 +45,21 @@ library LimitOrderLiquidity {
45
45
  settleDeltas(poolManager, key, delta0, delta1, payout0, payout1);
46
46
  }
47
47
 
48
+ /// @notice Burns a filled limit order and pays out proceeds to maker and fee recipient
49
+ /// @dev Consolidates payouts into the target currency (opposite of the deposit currency).
50
+ /// Handles fee distribution and ensures payouts are in a single currency by swapping
51
+ /// when necessary. Makers receive proceeds in the target currency they were expecting.
52
+ /// @param poolManager The Uniswap V4 pool manager
53
+ /// @param key The pool key for the limit order
54
+ /// @param order The limit order storage struct containing order details
55
+ /// @param orderId The unique identifier for the limit order
56
+ /// @param feeRecipient The address to receive referral fees (address(0) if no fees)
57
+ /// @param coinIn The coin contract address for multi-hop swap paths (if applicable)
58
+ /// @param coinLookup The deployed coin version lookup contract
59
+ /// @param weth The WETH contract address for ETH handling
60
+ /// @return makerCoinOut The currency paid to the maker
61
+ /// @return makerAmountOut The amount paid to the maker
62
+ /// @return referralAmountOut The amount paid to the fee recipient
48
63
  function burnAndPayout(
49
64
  IPoolManager poolManager,
50
65
  PoolKey memory key,
@@ -82,6 +97,20 @@ library LimitOrderLiquidity {
82
97
  }
83
98
  }
84
99
 
100
+ /// @notice Burns limit order liquidity and refunds the proceeds to a recipient
101
+ /// @dev Consolidates payouts into the original deposit currency. If both currencies
102
+ /// have positive amounts after burning, the counter-asset is swapped into the original
103
+ /// deposit currency, ensuring the recipient receives a single consolidated payout.
104
+ /// @param poolManager The Uniswap V4 pool manager
105
+ /// @param key The pool key for the limit order
106
+ /// @param tickLower The lower tick of the limit order position
107
+ /// @param tickUpper The upper tick of the limit order position
108
+ /// @param liquidity The amount of liquidity to burn
109
+ /// @param salt The salt used for position identification
110
+ /// @param recipient The address to receive the refund
111
+ /// @param isCurrency0 True if the original deposit was in currency0, false if currency1
112
+ /// @param weth The WETH contract address for ETH handling
113
+ /// @return amountOut The total amount refunded in the original deposit currency
85
114
  function burnAndRefund(
86
115
  IPoolManager poolManager,
87
116
  PoolKey memory key,
@@ -95,20 +124,9 @@ library LimitOrderLiquidity {
95
124
  ) internal returns (uint128 amountOut) {
96
125
  (int128 amount0, int128 amount1) = _burnLiquidity(poolManager, key, tickLower, tickUpper, liquidity, salt);
97
126
 
98
- if (amount0 > 0) {
99
- uint128 amount0Out = uint128(amount0);
100
- _takeCurrency(poolManager, key.currency0, recipient, amount0Out, weth);
101
- if (isCurrency0) {
102
- amountOut = amount0Out;
103
- }
104
- }
105
- if (amount1 > 0) {
106
- uint128 amount1Out = uint128(amount1);
107
- _takeCurrency(poolManager, key.currency1, recipient, amount1Out, weth);
108
- if (!isCurrency0) {
109
- amountOut = amount1Out;
110
- }
111
- }
127
+ // Refund to original deposit currency (swaps any accumulated counter-asset)
128
+ Currency payoutCurrency = isCurrency0 ? key.currency0 : key.currency1;
129
+ (, amountOut) = _payoutRecipient(poolManager, recipient, amount0, amount1, _buildSingleHopPath(key, payoutCurrency), weth);
112
130
  }
113
131
 
114
132
  function settleDeltas(IPoolManager poolManager, PoolKey memory key, int256 d0, int256 d1, address payout0, address payout1) internal {
@@ -116,8 +134,13 @@ library LimitOrderLiquidity {
116
134
  poolManager.take(key.currency0, payout0, uint256(d0));
117
135
  }
118
136
  if (d0 < 0) {
119
- uint256 amount = uint256(uint256(-d0));
137
+ // This is safe because d0 is always negative
138
+ //forge-lint: disable-next-line(unsafe-typecast)
139
+ uint256 amount = uint256(-d0);
120
140
  poolManager.sync(key.currency0);
141
+
142
+ // For native ETH, settle with value
143
+ // Ensured to be currency0 by token ordering
121
144
  if (key.currency0.isAddressZero()) {
122
145
  poolManager.settle{value: amount}();
123
146
  } else {
@@ -127,17 +150,17 @@ library LimitOrderLiquidity {
127
150
  }
128
151
 
129
152
  if (d1 > 0 && payout1 != address(0)) {
153
+ // This is safe because d1 is always positive
154
+ //forge-lint: disable-next-line(unsafe-typecast)
130
155
  poolManager.take(key.currency1, payout1, uint256(d1));
131
156
  }
132
157
  if (d1 < 0) {
133
- uint256 amount = uint256(uint256(-d1));
158
+ // This is safe because d1 is always negative
159
+ //forge-lint: disable-next-line(unsafe-typecast)
160
+ uint256 amount = uint256(-d1);
134
161
  poolManager.sync(key.currency1);
135
- if (key.currency1.isAddressZero()) {
136
- poolManager.settle{value: amount}();
137
- } else {
138
- key.currency1.transfer(address(poolManager), amount);
139
- poolManager.settle();
140
- }
162
+ key.currency1.transfer(address(poolManager), amount);
163
+ poolManager.settle();
141
164
  }
142
165
  }
143
166
 
@@ -178,8 +201,12 @@ library LimitOrderLiquidity {
178
201
  }
179
202
 
180
203
  // Fallback: construct simple single-hop path
181
- Currency coinCurrency = payoutCurrency == key.currency0 ? key.currency1 : key.currency0;
182
- payoutPath.currencyIn = coinCurrency;
204
+ return _buildSingleHopPath(key, payoutCurrency);
205
+ }
206
+
207
+ function _buildSingleHopPath(PoolKey memory key, Currency payoutCurrency) private pure returns (IHasSwapPath.PayoutSwapPath memory payoutPath) {
208
+ Currency inputCurrency = payoutCurrency == key.currency0 ? key.currency1 : key.currency0;
209
+ payoutPath.currencyIn = inputCurrency;
183
210
  payoutPath.path = new PathKey[](1);
184
211
  payoutPath.path[0] = PathKey({intermediateCurrency: payoutCurrency, fee: key.fee, tickSpacing: key.tickSpacing, hooks: key.hooks, hookData: bytes("")});
185
212
  }
@@ -192,15 +219,6 @@ library LimitOrderLiquidity {
192
219
  }
193
220
  }
194
221
 
195
- function _settleNegativeDeltas(IPoolManager poolManager, PoolKey memory key, int128 amount0, int128 amount1) private {
196
- int256 repay0 = amount0 < 0 ? int256(amount0) : int256(0);
197
- int256 repay1 = amount1 < 0 ? int256(amount1) : int256(0);
198
-
199
- if (repay0 != 0 || repay1 != 0) {
200
- settleDeltas(poolManager, key, repay0, repay1, address(0), address(0));
201
- }
202
- }
203
-
204
222
  function _takeCurrency(IPoolManager poolManager, Currency currency, address recipient, uint128 amount, address weth) private {
205
223
  if (!currency.isAddressZero()) {
206
224
  poolManager.take(currency, recipient, amount);
@@ -222,15 +240,20 @@ library LimitOrderLiquidity {
222
240
  IHasSwapPath.PayoutSwapPath memory payoutPath,
223
241
  address weth
224
242
  ) private returns (Currency coinOut, uint128 amountOut) {
225
- // Convert to uint128, treating negative/zero as zero
226
- uint128 amt0 = amount0 > 0 ? uint128(amount0) : 0;
227
- uint128 amt1 = amount1 > 0 ? uint128(amount1) : 0;
228
-
229
243
  // Use swapToPath which handles all cases:
230
244
  // - Single positive delta: returns that currency
231
245
  // - Dual positive deltas: swaps one to the other and returns combined amount
232
246
  // - Multi-hop paths: handles coin -> backingCoin -> backingCoin's currency
233
- (coinOut, amountOut) = UniV4SwapToCurrency.swapToPath(poolManager, amt0, amt1, payoutPath.currencyIn, payoutPath.path);
247
+ (coinOut, amountOut) = UniV4SwapToCurrency.swapToPath(
248
+ poolManager,
249
+ // This is safe because amount0 and amount1 are only needed if positive in this function.
250
+ //forge-lint: disable-next-line(unsafe-typecast)
251
+ amount0 > 0 ? uint128(amount0) : 0,
252
+ //forge-lint: disable-next-line(unsafe-typecast)
253
+ amount1 > 0 ? uint128(amount1) : 0,
254
+ payoutPath.currencyIn,
255
+ payoutPath.path
256
+ );
234
257
 
235
258
  if (amountOut > 0) {
236
259
  Currency payoutCurrency = coinOut;
@@ -9,8 +9,6 @@ pragma solidity ^0.8.28;
9
9
 
10
10
  import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
11
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
12
- import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
13
- import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
14
12
 
15
13
  import {LimitOrderStorage} from "./LimitOrderStorage.sol";
16
14
  import {IZoraLimitOrderBook} from "../IZoraLimitOrderBook.sol";
@@ -81,10 +79,8 @@ library LimitOrderWithdraw {
81
79
  PoolKey memory key = state.poolKeys[order.poolKeyHash];
82
80
  require(key.tickSpacing != 0, IZoraLimitOrderBook.InvalidOrder());
83
81
 
84
- // Prevent withdrawal of fillable orders - they must be filled instead
85
- (, int24 currentTick, , ) = StateLibrary.getSlot0(poolManager, PoolIdLibrary.toId(key));
86
- bool fillable = order.isCurrency0 ? currentTick >= order.tickUpper : currentTick <= order.tickLower;
87
- require(!fillable, IZoraLimitOrderBook.OrderFillable());
82
+ // Prevent withdrawal of crossed orders - they must be filled instead
83
+ require(!LimitOrderCommon.hasCrossed(order, LimitOrderCommon.currentPoolTick(poolManager, key)), IZoraLimitOrderBook.OrderFillable());
88
84
 
89
85
  int24 orderTick = LimitOrderCommon.getOrderTick(order);
90
86
  coin = LimitOrderCommon.getOrderCoin(key, order.isCurrency0);
@@ -104,13 +104,12 @@ library SwapLimitOrders {
104
104
  return (o, allocated, unallocated);
105
105
  }
106
106
 
107
- // Skip order creation when at tick boundaries
108
- // For currency0 (buy orders): cannot place if baseTick is at maxTick
109
- // For currency1 (sell orders): cannot place if baseTick is at minTick
110
- int24 maxTick = TickMath.maxUsableTick(key.tickSpacing);
111
- int24 alignedBaseTick = DopplerMath.alignTickToTickSpacing(isCurrency0, baseTick, key.tickSpacing);
112
-
113
- if (isCurrency0 ? alignedBaseTick >= maxTick : alignedBaseTick <= -maxTick) {
107
+ // Skip order creation when at hard price boundaries
108
+ // For currency0 (buy orders): cannot place if price is at max
109
+ // For currency1 (sell orders): cannot place if price is at min
110
+ uint160 maxPrice = TickMath.MAX_SQRT_PRICE - 1;
111
+ uint160 minPrice = TickMath.MIN_SQRT_PRICE + 1;
112
+ if (isCurrency0 ? sqrtPriceX96 >= maxPrice : sqrtPriceX96 <= minPrice) {
114
113
  unallocated = totalSize;
115
114
  return (o, allocated, unallocated);
116
115
  }
@@ -129,7 +128,11 @@ library SwapLimitOrders {
129
128
  uint256 orderSize = FullMath.mulDiv(uint256(remaining), config.percentages[i], PERCENT_SCALE);
130
129
  if (orderSize == 0) continue;
131
130
 
131
+ // This is safe because orderSize is bounded by remaining which is uint128
132
+ //forge-lint: disable-next-line(unsafe-typecast)
132
133
  allocated += uint128(orderSize);
134
+ // This is safe because orderSize is bounded by remaining which is uint128
135
+ //forge-lint: disable-next-line(unsafe-typecast)
133
136
  remaining -= uint128(orderSize);
134
137
 
135
138
  int24 targetTick = _tickForMultiple(key, isCurrency0, baseTick, sqrtPriceX96, config.multiples[i]);
@@ -139,6 +142,7 @@ library SwapLimitOrders {
139
142
  o.multiples[count] = config.multiples[i];
140
143
  o.percentages[count] = config.percentages[i];
141
144
 
145
+ // This is safe because orderCount is validated to be an array length which cannot overflow by definition
142
146
  unchecked {
143
147
  ++count;
144
148
  }
@@ -179,6 +183,8 @@ library SwapLimitOrders {
179
183
  uint256 scaled = FullMath.mulDiv(uint256(sqrtPriceX96), sqrtMultiplier, SQRT_MULTIPLE_SCALE);
180
184
  if (scaled > type(uint160).max) scaled = type(uint160).max;
181
185
 
186
+ // This is safe because scaled is always less than type(uint160).max
187
+ //forge-lint: disable-next-line(unsafe-typecast)
182
188
  int24 rawTick = TickMath.getTickAtSqrtPrice(uint160(scaled));
183
189
  aligned = DopplerMath.alignTickToTickSpacing(isCurrency0, rawTick, key.tickSpacing);
184
190
 
@@ -45,6 +45,7 @@ contract SwapWithLimitOrders is ISetLimitOrderConfig, Ownable2Step, IMsgSender {
45
45
  using Path for bytes;
46
46
 
47
47
  /// @notice The Uniswap V4 pool manager
48
+ // forge-lint-ignore screaming-snake-case-immutable
48
49
  IPoolManager public immutable poolManager;
49
50
 
50
51
  /// @notice The limit order book contract
@@ -46,7 +46,7 @@ contract MockPoolManager {
46
46
  swapDelta = toBalanceDelta(amount0, amount1);
47
47
  }
48
48
 
49
- function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory, bytes calldata) external returns (BalanceDelta, BalanceDelta) {
49
+ function modifyLiquidity(PoolKey memory, ModifyLiquidityParams memory, bytes calldata) external view returns (BalanceDelta, BalanceDelta) {
50
50
  return (liquidityDelta, feeDelta);
51
51
  }
52
52
 
@@ -270,18 +270,25 @@ contract LimitOrderLiquidityPayoutsTest is Test {
270
270
  assertEq(Currency.unwrap(poolManager.lastTakeCurrency()), address(currency1Token));
271
271
  }
272
272
 
273
+ /// @notice Test that dual positive deltas are consolidated into single payout currency
273
274
  function test_burnAndRefundPaysBothCurrenciesWhenPositive() public {
275
+ // isCurrency0 = true (default), so refund should be in currency0
274
276
  poolManager.setModifyLiquidityResponse(int128(10), int128(20), 0, 0);
277
+
278
+ // Simulate swap: 20 of currency1 → 18 of currency0
279
+ poolManager.setSwapResponse(int128(18), 0);
280
+
275
281
  deal(address(currency0Token), address(poolManager), 100e18);
276
282
  deal(address(currency1Token), address(poolManager), 100e18);
277
283
  address recipient = makeAddr("dual-recipient");
278
284
 
279
285
  uint128 amountOut = harness.burnAndRefund(poolManager, poolKey, ORDER_ID, recipient);
280
286
 
281
- assertEq(amountOut, 10, "amountOut should match order currency payout");
282
- assertEq(currency0Token.balanceOf(recipient), 10, "recipient receives currency0");
283
- assertEq(currency1Token.balanceOf(recipient), 20, "recipient receives currency1");
284
- assertEq(poolManager.takeCalls(), 2, "both currencies should be taken");
287
+ // Verify consolidated payout: 10 (original) + 18 (swapped) = 28
288
+ assertEq(amountOut, 28, "amountOut should be combined total");
289
+ assertEq(currency0Token.balanceOf(recipient), 28, "recipient receives combined in currency0");
290
+ assertEq(currency1Token.balanceOf(recipient), 0, "recipient should NOT receive currency1");
291
+ assertEq(poolManager.swapCalls(), 1, "swap should consolidate currencies");
285
292
  }
286
293
 
287
294
  function test_burnAndPayoutWithoutReferralRoutesAllProceeds() public {
@@ -340,7 +347,7 @@ contract LimitOrderLiquidityPayoutsTest is Test {
340
347
  uint256 makerCurrency0Before = currency0Token.balanceOf(maker);
341
348
  uint256 makerCurrency1Before = currency1Token.balanceOf(maker);
342
349
 
343
- (, uint128 makerAmount, ) = harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(currency0Token), versionLookup);
350
+ harness.burnAndPayout(poolManager, poolKey, ORDER_ID, address(0), address(currency0Token), versionLookup);
344
351
 
345
352
  // With the fix: only currency1 is paid out (NOT currency0)
346
353
  assertEq(currency0Token.balanceOf(maker), makerCurrency0Before, "maker should NOT receive currency0");
@@ -503,14 +510,7 @@ contract LimitOrderLiquidityPayoutsTest is Test {
503
510
  uint256 ref0Before = currency0Token.balanceOf(referral);
504
511
  uint256 ref1Before = currency1Token.balanceOf(referral);
505
512
 
506
- (Currency makerCoinOut, uint128 makerAmount, uint128 referralAmount) = harness.burnAndPayout(
507
- poolManager,
508
- poolKey,
509
- ORDER_ID,
510
- referral,
511
- address(0),
512
- versionLookup
513
- );
513
+ (Currency makerCoinOut, , ) = harness.burnAndPayout(poolManager, poolKey, ORDER_ID, referral, address(0), versionLookup);
514
514
 
515
515
  // Verify maker receives only currency1
516
516
  assertEq(Currency.unwrap(makerCoinOut), address(currency1Token), "maker payout should be currency1");
@@ -306,11 +306,15 @@ contract SwapWithLimitOrdersTestNonForked is SwapWithLimitOrdersTestBase {
306
306
  // TODO: Requires controlling tick movement during swap
307
307
  }
308
308
 
309
+ /// forge-config: default.isolate = true
309
310
  function test_orderFilling_invertedDirection() public {
310
311
  // This test verifies the fix for audit issue #16
311
312
  // https://github.com/kadenzipfel/zora-autosell-audit/issues/16
312
313
  // The router should pass isCoinCurrency0 (not !isCoinCurrency0) to _fillOrders
313
314
 
315
+ // Skip past launch fee period to test normal swap behavior
316
+ vm.warp(block.timestamp + 1 days);
317
+
314
318
  PoolKey memory key = creatorCoin.getPoolKey();
315
319
 
316
320
  // 1. Create first buyer's orders using swapWithLimitOrders