@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.
- package/.abi-stability +0 -21
- package/.turbo/turbo-build$colon$js.log +52 -50
- package/CHANGELOG.md +21 -6
- package/abis/ContractVersionBase.json +15 -0
- package/abis/ICoin.json +5 -0
- package/abis/IVersionedContract.json +15 -0
- package/abis/SwapWithLimitOrders.json +13 -0
- package/abis/ZoraLimitOrderBook.json +13 -0
- package/cache/solidity-files-cache.json +1 -1
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +20 -0
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/out/BytesLib.sol/BytesLib.json +1 -1
- package/out/CoinConfigurationVersions.sol/CoinConfigurationVersions.json +1 -1
- package/out/CoinConstants.sol/CoinConstants.json +1 -1
- package/out/ContractVersionBase.sol/ContractVersionBase.json +1 -0
- package/out/DopplerMath.sol/DopplerMath.json +1 -1
- package/out/ICoin.sol/ICoin.json +1 -1
- package/out/ICoin.sol/IHasCoinType.json +1 -1
- package/out/ICoin.sol/IHasPoolKey.json +1 -1
- package/out/ICoin.sol/IHasSwapPath.json +1 -1
- package/out/ICoin.sol/IHasTotalSupplyForPositions.json +1 -1
- package/out/ISwapRouter.sol/ISwapRouter.json +1 -1
- package/out/IUniswapV3SwapCallback.sol/IUniswapV3SwapCallback.json +1 -1
- package/out/IVersionedContract.sol/IVersionedContract.json +1 -0
- package/out/IZoraLimitOrderBook.sol/IZoraLimitOrderBook.json +1 -1
- package/out/IZoraV4CoinHook.sol/IZoraV4CoinHook.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/LimitOrderQueues.sol/LimitOrderQueues.json +1 -1
- package/out/LimitOrderStorage.sol/LimitOrderStorage.json +1 -1
- package/out/LimitOrderTypes.sol/LimitOrderTypes.json +1 -1
- package/out/LimitOrderViews.sol/LimitOrderViews.json +1 -1
- package/out/LimitOrderWithdraw.sol/LimitOrderWithdraw.json +1 -1
- package/out/Path.sol/Path.json +1 -1
- package/out/Permit2Payments.sol/Permit2Payments.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/ZoraLimitOrderBook.sol/ZoraLimitOrderBook.json +1 -1
- package/out/build-info/{c9f7ee5726bfbd48.json → fcf3d601c2943dea.json} +1 -1
- package/package/wagmiGenerated.ts +14 -0
- package/package.json +3 -3
- package/src/ZoraLimitOrderBook.sol +4 -1
- package/src/libs/LimitOrderFill.sol +4 -5
- package/src/router/SwapWithLimitOrders.sol +2 -1
- package/src/version/ContractVersionBase.sol +14 -0
- package/test/DebugSwapWithLimitOrders.t.sol +196 -0
- package/test/LimitOrderAccessControl.t.sol +8 -2
- package/test/LimitOrderFill.t.sol +118 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
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.
|
|
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": "
|
|
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.
|
|
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
|
|
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
|
|
188
|
-
// This ensures payout uses the
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|