@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.
- package/.abi-stability +70 -0
- package/.turbo/turbo-build$colon$js.log +40 -40
- package/CHANGELOG.md +23 -0
- package/cache/solidity-files-cache.json +1 -1
- package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
- package/out/CoinConstants.sol/CoinConstants.json +1 -1
- package/out/DopplerMath.sol/DopplerMath.json +1 -1
- package/out/ISetLimitOrderConfig.sol/ISetLimitOrderConfig.json +1 -1
- package/out/LimitOrderBitmap.sol/LimitOrderBitmap.json +1 -1
- package/out/LimitOrderCommon.sol/LimitOrderCommon.json +1 -1
- package/out/LimitOrderCreate.sol/LimitOrderCreate.json +1 -1
- package/out/LimitOrderFill.sol/LimitOrderFill.json +1 -1
- package/out/LimitOrderLiquidity.sol/LimitOrderLiquidity.json +1 -1
- package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
- package/out/PermittedCallers.sol/PermittedCallers.json +1 -1
- package/out/SwapLimitOrders.sol/SwapLimitOrders.json +1 -1
- package/out/SwapWithLimitOrders.sol/SwapWithLimitOrders.json +1 -1
- package/out/UniV4SwapToCurrency.sol/UniV4SwapToCurrency.json +1 -1
- package/out/V3ToV4SwapLib.sol/V3ToV4SwapLib.json +1 -1
- package/out/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
- package/out/build-info/{37e0124d88d60569.json → 68b2e124c4a02a45.json} +1 -1
- package/package.json +4 -2
- package/src/access/PermittedCallers.sol +5 -1
- package/src/libs/LimitOrderBitmap.sol +0 -1
- package/src/libs/LimitOrderCommon.sol +14 -0
- package/src/libs/LimitOrderCreate.sol +6 -0
- package/src/libs/LimitOrderFill.sol +3 -20
- package/src/libs/LimitOrderLiquidity.sol +61 -38
- package/src/libs/LimitOrderWithdraw.sol +2 -6
- package/src/libs/SwapLimitOrders.sol +13 -7
- package/src/router/SwapWithLimitOrders.sol +1 -0
- package/test/LimitOrderLiquidityPayouts.t.sol +14 -14
- package/test/SwapWithLimitOrdersRouter.t.sol +4 -0
- package/test/unit/LimitOrderCreateUnit.t.sol +17 -17
- package/test/unit/SwapLimitOrdersUnit.t.sol +47 -86
- package/test/unit/SwapLimitOrdersValidation.t.sol +3 -11
- package/test/utils/BaseTest.sol +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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(
|
|
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
|
|
85
|
-
(,
|
|
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
|
|
108
|
-
// For currency0 (buy orders): cannot place if
|
|
109
|
-
// For currency1 (sell orders): cannot place if
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
282
|
-
assertEq(
|
|
283
|
-
assertEq(
|
|
284
|
-
assertEq(
|
|
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
|
-
|
|
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,
|
|
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
|