@zoralabs/coins 2.3.1 → 2.4.1

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 (126) hide show
  1. package/.turbo/turbo-build$colon$js.log +119 -128
  2. package/CHANGELOG.md +30 -0
  3. package/abis/Address.json +0 -16
  4. package/abis/BuySupplyWithSwapRouterHook.json +0 -27
  5. package/abis/BuySupplyWithV4SwapHook.json +0 -32
  6. package/abis/Clones.json +1 -1
  7. package/abis/CoinDopplerMultiCurve.json +109 -0
  8. package/abis/Create2.json +0 -21
  9. package/abis/ERC1967Proxy.json +1 -1
  10. package/abis/ERC1967Utils.json +0 -45
  11. package/abis/{UpgradeCoinImpl.json → Errors.json} +14 -10
  12. package/abis/{MockERC20.json → IERC1363.json} +134 -104
  13. package/abis/IERC1967.json +47 -0
  14. package/abis/IERC20.json +0 -36
  15. package/abis/IProtocolRewards.json +0 -258
  16. package/abis/{Script.json → ISupportsLimitOrderFill.json} +2 -2
  17. package/abis/ITrustedMsgSenderProviderLookup.json +21 -0
  18. package/abis/IZoraLimitOrderBookCoinsInterface.json +67 -0
  19. package/abis/IZoraV4CoinHook.json +15 -0
  20. package/abis/ProxyShim.json +15 -16
  21. package/abis/SafeCast.json +51 -0
  22. package/abis/{AddressConstants.json → SafeCast160.json} +1 -1
  23. package/abis/Strings.json +10 -0
  24. package/abis/TrustedMsgSenderProviderLookup.json +215 -0
  25. package/abis/UUPSUpgradeable.json +1 -1
  26. package/abis/V3ToV4SwapLib.json +28 -0
  27. package/abis/ZoraFactory.json +1 -1
  28. package/abis/ZoraFactoryImpl.json +22 -6
  29. package/abis/ZoraV4CoinHook.json +41 -51
  30. package/dist/index.cjs +950 -43
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +948 -41
  33. package/dist/index.js.map +1 -1
  34. package/dist/wagmiGenerated.d.ts +1459 -76
  35. package/dist/wagmiGenerated.d.ts.map +1 -1
  36. package/foundry.toml +5 -1
  37. package/package/wagmiGenerated.ts +951 -44
  38. package/package.json +7 -7
  39. package/remappings.txt +2 -1
  40. package/src/ZoraFactoryImpl.sol +8 -0
  41. package/src/deployment/ForkedCoinsAddresses.sol +54 -0
  42. package/src/hooks/ZoraV4CoinHook.sol +92 -74
  43. package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +20 -142
  44. package/src/interfaces/ISupportsLimitOrderFill.sol +11 -0
  45. package/src/interfaces/ITrustedMsgSenderProviderLookup.sol +18 -0
  46. package/src/interfaces/IZoraLimitOrderBookCoinsInterface.sol +21 -0
  47. package/src/interfaces/IZoraV4CoinHook.sol +9 -0
  48. package/src/libs/CoinConstants.sol +6 -0
  49. package/src/libs/CoinDopplerMultiCurve.sol +1 -1
  50. package/src/libs/CoinRewardsV4.sol +0 -1
  51. package/src/libs/HooksDeployment.sol +25 -12
  52. package/src/libs/UniV4SwapHelper.sol +35 -0
  53. package/src/libs/V3ToV4SwapLib.sol +261 -0
  54. package/src/libs/V4Liquidity.sol +50 -6
  55. package/src/utils/TrustedMsgSenderProviderLookup.sol +73 -0
  56. package/src/version/ContractVersionBase.sol +1 -1
  57. package/test/BuySupplyWithV4SwapHook.t.sol +4 -3
  58. package/test/Coin.t.sol +7 -1
  59. package/test/CoinUniV4.t.sol +2 -1
  60. package/test/ContentCoinRewards.t.sol +5 -1
  61. package/test/CreatorCoin.t.sol +3 -1
  62. package/test/CreatorCoinRewards.t.sol +7 -1
  63. package/test/Factory.t.sol +20 -7
  64. package/test/HooksDeployment.t.sol +73 -8
  65. package/test/LiquidityMigration.t.sol +55 -43
  66. package/test/MultiOwnable.t.sol +2 -1
  67. package/test/TrustedMsgSenderProviderLookup.t.sol +112 -0
  68. package/test/Upgrades.t.sol +112 -78
  69. package/test/V4Liquidity.t.sol +1 -1
  70. package/test/mocks/MockSwapRouter.sol +33 -0
  71. package/test/mocks/MockZoraLimitOrderBook.sol +14 -0
  72. package/test/utils/BaseTest.sol +17 -425
  73. package/test/utils/FeeEstimatorHook.sol +8 -2
  74. package/test/utils/TrustedSenderTestHelper.sol +18 -0
  75. package/test/utils/V4TestSetup.sol +595 -0
  76. package/wagmi.config.ts +1 -1
  77. package/abis/BaseTest.json +0 -718
  78. package/abis/DeterministicDeployerAndCaller.json +0 -315
  79. package/abis/DeterministicUUPSProxyDeployer.json +0 -167
  80. package/abis/EIP712.json +0 -67
  81. package/abis/ERC20.json +0 -310
  82. package/abis/FeeEstimatorHook.json +0 -1915
  83. package/abis/IERC721.json +0 -287
  84. package/abis/IERC721Enumerable.json +0 -343
  85. package/abis/IERC721Metadata.json +0 -332
  86. package/abis/IERC721TokenReceiver.json +0 -36
  87. package/abis/IImmutableCreate2Factory.json +0 -93
  88. package/abis/IMulticall3.json +0 -440
  89. package/abis/ISafe.json +0 -15
  90. package/abis/ISymbol.json +0 -15
  91. package/abis/IUniswapV4Router04.json +0 -484
  92. package/abis/IUniversalRouter.json +0 -61
  93. package/abis/IV4Quoter.json +0 -310
  94. package/abis/ImmutableCreate2FactoryUtils.json +0 -15
  95. package/abis/LibString.json +0 -7
  96. package/abis/Math.json +0 -7
  97. package/abis/MockAirlock.json +0 -39
  98. package/abis/MockERC721.json +0 -350
  99. package/abis/ProtocolRewards.json +0 -494
  100. package/abis/ShortStrings.json +0 -18
  101. package/abis/SimpleERC20.json +0 -326
  102. package/abis/StdAssertions.json +0 -379
  103. package/abis/StdInvariant.json +0 -180
  104. package/abis/Test.json +0 -570
  105. package/abis/VmContractHelper239.json +0 -233
  106. package/abis/stdError.json +0 -119
  107. package/abis/stdStorageSafe.json +0 -52
  108. package/addresses/8453.json +0 -13
  109. package/addresses/84532.json +0 -10
  110. package/deterministicConfig/deployerAndCaller.json +0 -5
  111. package/deterministicConfig/zoraFactory.json +0 -8
  112. package/script/Deploy.s.sol +0 -23
  113. package/script/DeployAutoSwapper.s.sol +0 -30
  114. package/script/DeployDevFactory.s.sol +0 -21
  115. package/script/DeployPostDeploymentHooks.s.sol +0 -20
  116. package/script/DeployUpgradeGate.s.sol +0 -21
  117. package/script/GenerateDeterministicParams.s.sol +0 -43
  118. package/script/PrintRegisterUpgradePath.s.sol +0 -28
  119. package/script/PrintUpgradeCommand.s.sol +0 -13
  120. package/script/TestBackingCoinSwap.s.sol +0 -144
  121. package/script/TestV4Swap.s.sol +0 -133
  122. package/script/UpgradeCoinImpl.sol +0 -23
  123. package/script/UpgradeFactoryImpl.s.sol +0 -28
  124. package/script/UpgradeHooks.s.sol +0 -23
  125. package/src/deployment/CoinsDeployerBase.sol +0 -276
  126. /package/{test → src}/utils/ProxyShim.sol +0 -0
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
2
+ // This software is licensed under the Zora Delayed Open Source License.
3
+ // Under this license, you may use, copy, modify, and distribute this software for
4
+ // non-commercial purposes only. Commercial use and competitive products are prohibited
5
+ // until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
6
+ // at which point this software automatically becomes available under the MIT License.
7
+ // Full license terms available at: https://docs.zora.co/coins/license
8
+ pragma solidity ^0.8.23;
9
+
10
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
11
+
12
+ interface IZoraLimitOrderBookCoinsInterface {
13
+ /// @notice Fills limit orders within a tick window.
14
+ /// @param key Pool key whose orders should be processed.
15
+ /// @param isCurrency0 Whether currency0 orders are targeted; otherwise currency1.
16
+ /// @param startTick Inclusive starting tick. Use `-type(int24).max` for the default lower bound.
17
+ /// @param endTick Inclusive ending tick. Use `type(int24).max` for the default upper bound.
18
+ /// @param maxFillCount Maximum orders to fill in this pass.
19
+ /// @param fillReferral Address to receive accrued LP fees; use address(0) to give fees to maker.
20
+ function fill(PoolKey calldata key, bool isCurrency0, int24 startTick, int24 endTick, uint256 maxFillCount, address fillReferral) external;
21
+ }
@@ -44,6 +44,9 @@ interface IZoraV4CoinHook is IUpgradeableV4Hook {
44
44
  /// @notice Upgrade gate cannot be the zero address.
45
45
  error UpgradeGateCannotBeZeroAddress();
46
46
 
47
+ /// @notice Trusted message sender lookup cannot be the zero address.
48
+ error TrustedMsgSenderLookupCannotBeZeroAddress();
49
+
47
50
  /// @notice Thrown when a pool is not initialized for the hook.
48
51
  /// @param key The pool key struct to identify the pool.
49
52
  error NoCoinForHook(PoolKey key);
@@ -54,6 +57,12 @@ interface IZoraV4CoinHook is IUpgradeableV4Hook {
54
57
  /// @notice Thrown when a non-coin is used to access the functionality of a coin.
55
58
  error OnlyCoin(address caller, address expectedCoin);
56
59
 
60
+ /// @notice Thrown when the Zora limit order book is the zero address.
61
+ error ZoraLimitOrderBookCannotBeZeroAddress();
62
+
63
+ /// @notice Thrown when the Zora hook registry is the zero address.
64
+ error ZoraHookRegistryCannotBeZeroAddress();
65
+
57
66
  /// @notice The pool coin struct. Lists all the contract-created positions for the coin.
58
67
  struct PoolCoin {
59
68
  /// @notice The address of the coin.
@@ -8,6 +8,12 @@
8
8
  pragma solidity ^0.8.23;
9
9
 
10
10
  library CoinConstants {
11
+ /// @dev Slot for transiently storing the tick before a swap.
12
+ bytes32 internal constant _BEFORE_SWAP_TICK_SLOT = keccak256("ZoraV4CoinHook.beforeSwap.tick");
13
+
14
+ /// @dev Constant used to indicate the max fill count for limit orders
15
+ uint256 internal constant SENTINEL_DEFAULT_LIMIT_ORDER_FILL_COUNT = 0;
16
+
11
17
  /// @dev Constant used to increase precision during calculations
12
18
  uint256 internal constant WAD = 1e18;
13
19
 
@@ -105,7 +105,7 @@ library CoinDopplerMultiCurve {
105
105
  bool isCoinToken0,
106
106
  PoolConfiguration memory poolConfiguration,
107
107
  uint256 totalSupply
108
- ) internal pure returns (LpPosition[] memory positions) {
108
+ ) external pure returns (LpPosition[] memory positions) {
109
109
  positions = new LpPosition[](poolConfiguration.numPositions);
110
110
 
111
111
  uint256 discoverySupply;
@@ -27,7 +27,6 @@ import {IHasRewardsRecipients} from "../interfaces/IHasRewardsRecipients.sol";
27
27
  import {ICoin} from "../interfaces/ICoin.sol";
28
28
  import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
29
29
  import {IHasSwapPath} from "../interfaces/ICoin.sol";
30
- import {V4Liquidity} from "./V4Liquidity.sol";
31
30
  import {UniV4SwapToCurrency} from "./UniV4SwapToCurrency.sol";
32
31
  import {ICreatorCoinHook} from "../interfaces/ICreatorCoinHook.sol";
33
32
  import {IHasCoinType} from "../interfaces/ICoin.sol";
@@ -14,6 +14,7 @@ import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol";
14
14
  import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
15
15
  import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
16
16
  import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
17
+ import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
17
18
 
18
19
  Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))));
19
20
 
@@ -62,7 +63,7 @@ library HooksDeployment {
62
63
  bytes32 constant VALID_CREATOR_COIN_SALT = 0x00000000000000000000000000000000000000000000000000000000000031af;
63
64
 
64
65
  function mineForSalt(address deployer, bytes memory hookCreationCode) internal view returns (address hookAddress, bytes32 salt) {
65
- uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG) ^ (0x4444 << 144);
66
+ uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG) ^ (0x4444 << 144);
66
67
  return HookMinerWithCreationCodeArgs.find(deployer, flags, hookCreationCode);
67
68
  }
68
69
 
@@ -86,10 +87,12 @@ library HooksDeployment {
86
87
  address deployer,
87
88
  address poolManager,
88
89
  address coinVersionLookup,
89
- address[] memory trustedMessageSenders,
90
- address upgradeGate
90
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
91
+ address upgradeGate,
92
+ address orderFiller,
93
+ address hookRegistry
91
94
  ) internal returns (address hookAddress, bytes32 salt) {
92
- bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
95
+ bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry);
93
96
  (salt, ) = mineAndCacheSalt(deployer, hookCreationCode);
94
97
  hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, salt, hookCreationCode);
95
98
  }
@@ -131,19 +134,27 @@ library HooksDeployment {
131
134
  function hookConstructorArgs(
132
135
  address poolManager,
133
136
  address coinVersionLookup,
134
- address[] memory trustedMessageSenders,
135
- address upgradeGate
137
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
138
+ address upgradeGate,
139
+ address orderFiller,
140
+ address hookRegistry
136
141
  ) internal pure returns (bytes memory) {
137
- return abi.encode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
142
+ return abi.encode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry);
138
143
  }
139
144
 
140
145
  function makeHookCreationCode(
141
146
  address poolManager,
142
147
  address coinVersionLookup,
143
- address[] memory trustedMessageSenders,
144
- address upgradeGate
148
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
149
+ address upgradeGate,
150
+ address orderFiller,
151
+ address hookRegistry
145
152
  ) internal pure returns (bytes memory) {
146
- return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate));
153
+ return
154
+ abi.encodePacked(
155
+ type(ZoraV4CoinHook).creationCode,
156
+ hookConstructorArgs(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry)
157
+ );
147
158
  }
148
159
 
149
160
  /// @notice Deploys or returns existing ContentCoinHook using deterministic deployment. Ensures that if a hooks is already
@@ -151,11 +162,13 @@ library HooksDeployment {
151
162
  function deployZoraV4CoinHook(
152
163
  address poolManager,
153
164
  address coinVersionLookup,
154
- address[] memory trustedMessageSenders,
165
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
155
166
  address upgradeGate,
167
+ address orderFiller,
168
+ address hookRegistry,
156
169
  bytes32 salt
157
170
  ) internal returns (IHooks hook) {
158
- bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
171
+ bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry);
159
172
  return deployHookWithSalt(creationCode, salt);
160
173
  }
161
174
 
@@ -22,6 +22,41 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
22
22
  import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
23
23
 
24
24
  library UniV4SwapHelper {
25
+ function buildExactInputMultiSwapCommand(
26
+ address currencyIn,
27
+ uint128 amountIn,
28
+ PoolKey[] memory keys,
29
+ uint128 minAmountOut,
30
+ bytes[] memory hopHookData
31
+ ) internal pure returns (bytes memory commands, bytes[] memory inputs) {
32
+ require(keys.length > 0 && hopHookData.length == keys.length, "invalid lengths");
33
+
34
+ PathKey[] memory path = new PathKey[](keys.length);
35
+
36
+ Currency currency = Currency.wrap(currencyIn);
37
+ Currency finalCurrencyOut;
38
+
39
+ for (uint256 i; i < keys.length; ++i) {
40
+ Currency out = currency == keys[i].currency0 ? keys[i].currency1 : keys[i].currency0;
41
+ path[i] = PathKey({intermediateCurrency: out, fee: keys[i].fee, tickSpacing: keys[i].tickSpacing, hooks: keys[i].hooks, hookData: hopHookData[i]});
42
+ currency = out;
43
+ finalCurrencyOut = out;
44
+ }
45
+
46
+ bytes memory actions = abi.encodePacked(uint8(Actions.SWAP_EXACT_IN), uint8(Actions.SETTLE), uint8(Actions.TAKE_ALL));
47
+ bytes[] memory params = new bytes[](3);
48
+ params[0] = abi.encode( // 1) SWAP_EXACT_IN({ currencyIn, path, amountIn, amountOutMinimum })
49
+ IV4Router.ExactInputParams({currencyIn: Currency.wrap(currencyIn), path: path, amountIn: amountIn, amountOutMinimum: minAmountOut})
50
+ );
51
+ params[1] = abi.encode(currencyIn, amountIn, true); // 2) SETTLE(tokenIn, amountIn, payerIsUser=true) — pulls from user via Permit2
52
+ params[2] = abi.encode(Currency.unwrap(finalCurrencyOut), minAmountOut); // 3) TAKE_ALL(finalCurrencyOut, minOut)
53
+
54
+ commands = abi.encodePacked(uint8(Commands.V4_SWAP));
55
+
56
+ inputs = new bytes[](1);
57
+ inputs[0] = abi.encode(actions, params);
58
+ }
59
+
25
60
  function buildExactInputSingleSwapCommand(
26
61
  address currencyIn,
27
62
  uint128 amountIn,
@@ -0,0 +1,261 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
7
+ import {IPoolManager, SwapParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
8
+ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
9
+ import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
10
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
11
+ import {ISwapRouter} from "../interfaces/ISwapRouter.sol";
12
+ import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
13
+ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
14
+ import {SafeCast160} from "permit2/src/libraries/SafeCast160.sol";
15
+
16
+ /// @title V3ToV4SwapLib
17
+ /// @notice Shared library for executing V3-to-V4 swap routing
18
+ /// @dev Provides common functionality for:
19
+ /// - V3 route validation and connection to V4 routes
20
+ /// - Input currency validation and transfer (ETH vs ERC20)
21
+ /// - V3 swap execution via ISwapRouter.exactInput()
22
+ /// - V4 multi-hop swap execution
23
+ /// - Delta settlement with poolManager
24
+ /// - V3 route parsing utilities
25
+ library V3ToV4SwapLib {
26
+ using SafeERC20 for IERC20;
27
+ using BalanceDeltaLibrary for BalanceDelta;
28
+ using CurrencyLibrary for Currency;
29
+ using Path for bytes;
30
+ using SafeCast160 for uint256;
31
+
32
+ // ============ ERRORS ============
33
+
34
+ error InsufficientInputCurrency(uint256 inputAmount, uint256 availableAmount);
35
+ error V3RouteCannotStartWithInputCurrency();
36
+ error V3RouteDoesNotConnectToV4RouteStart();
37
+
38
+ // ============ STRUCTS ============
39
+
40
+ /// @notice Parameters for V3 swap execution
41
+ struct V3SwapParams {
42
+ bytes v3Route; // V3 route path
43
+ address inputCurrency; // Input currency (address(0) for ETH)
44
+ uint256 inputAmount; // Amount of input currency
45
+ address recipient; // Recipient of swap output
46
+ }
47
+
48
+ /// @notice Parameters for V4 multi-hop swap execution
49
+ struct V4SwapParams {
50
+ PoolKey[] v4Route; // Array of pool keys to swap through
51
+ uint256 amountIn; // Starting amount
52
+ Currency startingCurrency; // Starting currency
53
+ }
54
+
55
+ /// @notice Result from V4 multi-hop swap
56
+ struct V4SwapResult {
57
+ uint128 outputAmount; // Final output amount
58
+ Currency outputCurrency; // Final output currency
59
+ }
60
+
61
+ // ============ VALIDATION ============
62
+
63
+ /// @notice Validates that V3 route output connects to V4 route start
64
+ /// @param v3Route The V3 route path (empty if no V3 swap)
65
+ /// @param inputCurrency The input currency for the swap
66
+ /// @param v4Route The V4 route (first pool must accept V3 output or input currency)
67
+ function validateRoutes(bytes memory v3Route, address inputCurrency, PoolKey[] memory v4Route) internal pure {
68
+ if (v4Route.length == 0) {
69
+ return; // No V4 route to validate
70
+ }
71
+
72
+ // Determine what currency should be the input to the V4 route
73
+ address v4InputCurrency;
74
+ if (v3Route.length == 0) {
75
+ // No V3 swap - input currency should directly match V4 route start
76
+ v4InputCurrency = inputCurrency;
77
+ } else {
78
+ // V3 swap exists - V3 output should match V4 route start
79
+ v4InputCurrency = getV3RouteOutputCurrency(v3Route);
80
+ }
81
+
82
+ PoolKey memory firstPool = v4Route[0];
83
+
84
+ require(
85
+ v4InputCurrency == Currency.unwrap(firstPool.currency0) || v4InputCurrency == Currency.unwrap(firstPool.currency1),
86
+ V3RouteDoesNotConnectToV4RouteStart()
87
+ );
88
+ }
89
+
90
+ /// @notice Validates and transfers input currency from sender to contract
91
+ /// @param inputCurrency The input currency address (address(0) for ETH)
92
+ /// @param inputAmount The amount to transfer
93
+ /// @param from The address to transfer from
94
+ /// @param msgValue The msg.value sent with the transaction
95
+ function validateAndTransferInputCurrency(address inputCurrency, uint256 inputAmount, address from, uint256 msgValue) internal {
96
+ if (inputCurrency == address(0)) {
97
+ // ETH payment
98
+ require(msgValue == inputAmount, InsufficientInputCurrency(inputAmount, msgValue));
99
+ } else {
100
+ // ERC20 payment
101
+ uint256 allowanceAmount = IERC20(inputCurrency).allowance(from, address(this));
102
+ require(allowanceAmount >= inputAmount, InsufficientInputCurrency(inputAmount, allowanceAmount));
103
+
104
+ uint256 balanceAmount = IERC20(inputCurrency).balanceOf(from);
105
+ require(balanceAmount >= inputAmount, InsufficientInputCurrency(inputAmount, balanceAmount));
106
+
107
+ IERC20(inputCurrency).safeTransferFrom(from, address(this), inputAmount);
108
+ }
109
+ }
110
+
111
+ /// @notice Validates and transfers input currency from sender using Permit2
112
+ /// @param permit2 The Permit2 contract
113
+ /// @param inputCurrency The input currency address (address(0) for ETH)
114
+ /// @param inputAmount The amount to transfer
115
+ /// @param from The address to transfer from
116
+ /// @param to The address to transfer to (recipient)
117
+ /// @param msgValue The msg.value sent with the transaction
118
+ function permit2TransferFrom(IAllowanceTransfer permit2, address inputCurrency, uint256 inputAmount, address from, address to, uint256 msgValue) internal {
119
+ if (inputCurrency == address(0)) {
120
+ // ETH payment - no Permit2 needed
121
+ require(msgValue == inputAmount, InsufficientInputCurrency(inputAmount, msgValue));
122
+ } else {
123
+ // ERC20 payment via Permit2
124
+ require(msgValue == 0, InsufficientInputCurrency(0, msgValue));
125
+ permit2.transferFrom(from, to, inputAmount.toUint160(), inputCurrency);
126
+ }
127
+ }
128
+
129
+ // ============ V3 SWAP LOGIC ============
130
+
131
+ /// @notice Executes a V3 swap if v3Route is provided, otherwise returns input
132
+ /// @param swapRouter The Uniswap V3 swap router
133
+ /// @param params The V3 swap parameters
134
+ /// @return amountCurrency The amount received from V3 swap (or input if no swap)
135
+ /// @return currencyReceived The currency received (output of V3 or input if no swap)
136
+ function executeV3Swap(ISwapRouter swapRouter, V3SwapParams memory params) internal returns (uint256 amountCurrency, address currencyReceived) {
137
+ if (params.v3Route.length == 0) {
138
+ // No V3 swap needed - return input directly
139
+ return (params.inputAmount, params.inputCurrency);
140
+ }
141
+
142
+ // Handle ERC20 input - approve swapRouter to spend tokens
143
+ if (params.inputCurrency != address(0)) {
144
+ IERC20(params.inputCurrency).safeIncreaseAllowance(address(swapRouter), params.inputAmount);
145
+ }
146
+
147
+ // Build swap router call for exactInput
148
+ ISwapRouter.ExactInputParams memory swapParams = ISwapRouter.ExactInputParams({
149
+ path: params.v3Route,
150
+ recipient: params.recipient,
151
+ amountIn: params.inputAmount,
152
+ amountOutMinimum: 0 // Slippage protection should be handled at higher level
153
+ });
154
+
155
+ // Conditional value passing - ETH if inputCurrency is address(0), otherwise 0
156
+ uint256 value = params.inputCurrency == address(0) ? params.inputAmount : 0;
157
+ amountCurrency = swapRouter.exactInput{value: value}(swapParams);
158
+ currencyReceived = getV3RouteOutputCurrency(params.v3Route);
159
+ }
160
+
161
+ // ============ V4 SWAP LOGIC ============
162
+
163
+ /// @notice Executes a multi-hop V4 swap through multiple pools
164
+ /// @param poolManager The Uniswap V4 pool manager
165
+ /// @param params The V4 swap parameters
166
+ /// @return result The swap result containing output amount and currency
167
+ function executeV4MultiHopSwap(IPoolManager poolManager, V4SwapParams memory params) internal returns (V4SwapResult memory result) {
168
+ Currency currentCurrency = params.startingCurrency;
169
+ uint128 currentAmount = uint128(params.amountIn);
170
+
171
+ // Execute swaps through the route
172
+ for (uint256 i = 0; i < params.v4Route.length; i++) {
173
+ PoolKey memory poolKey = params.v4Route[i];
174
+
175
+ // Determine swap direction based on current currency
176
+ bool zeroForOne = currentCurrency == poolKey.currency0;
177
+
178
+ BalanceDelta delta = poolManager.swap(
179
+ poolKey,
180
+ SwapParams(zeroForOne, -(int128(currentAmount)), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
181
+ ""
182
+ );
183
+
184
+ // Extract output amount from delta
185
+ uint128 outputAmount = zeroForOne ? uint128(delta.amount1()) : uint128(delta.amount0());
186
+
187
+ // Update for next iteration
188
+ currentAmount = outputAmount;
189
+ currentCurrency = zeroForOne ? poolKey.currency1 : poolKey.currency0;
190
+ }
191
+
192
+ result.outputAmount = currentAmount;
193
+ result.outputCurrency = currentCurrency;
194
+ }
195
+
196
+ // ============ DELTA SETTLEMENT ============
197
+
198
+ /// @notice Settles currency deltas with the pool manager
199
+ /// @param poolManager The Uniswap V4 pool manager
200
+ /// @param inputCurrency The input currency to settle
201
+ /// @param outputCurrency The output currency to take
202
+ /// @param to The recipient of the output currency
203
+ /// @param inputAmount The amount of input currency to settle
204
+ /// @param outputAmount The amount of output currency to take
205
+ function settleDeltas(
206
+ IPoolManager poolManager,
207
+ Currency inputCurrency,
208
+ Currency outputCurrency,
209
+ address to,
210
+ uint256 inputAmount,
211
+ uint128 outputAmount
212
+ ) internal {
213
+ // Pay the input amount
214
+ if (inputCurrency.isAddressZero()) {
215
+ // For ETH, settle with msg.value
216
+ poolManager.settle{value: inputAmount}();
217
+ } else {
218
+ // For ERC20, sync and transfer
219
+ poolManager.sync(inputCurrency);
220
+ inputCurrency.transfer(address(poolManager), inputAmount);
221
+ poolManager.settle();
222
+ }
223
+
224
+ // Transfer the output amount to the recipient
225
+ poolManager.take(outputCurrency, to, outputAmount);
226
+ }
227
+
228
+ // ============ UTILITIES ============
229
+
230
+ /// @notice Gets the output currency from a V3 route path
231
+ /// @param path The V3 route path
232
+ /// @return tokenOut The output token address
233
+ function getV3RouteOutputCurrency(bytes memory path) internal pure returns (address tokenOut) {
234
+ if (path.length == 0) {
235
+ return address(0);
236
+ }
237
+
238
+ // Traverse to the end of the path to find the final token
239
+ bytes memory currentPath = path;
240
+
241
+ // Keep skipping tokens until we reach the final pool
242
+ while (currentPath.hasMultiplePools()) {
243
+ currentPath = currentPath.skipToken();
244
+ }
245
+
246
+ // The final segment contains the last pool, decode to get the output token
247
+ (, tokenOut, ) = currentPath.decodeFirstPool();
248
+ }
249
+
250
+ /// @notice Gets the input currency from a V3 route path
251
+ /// @param path The V3 route path
252
+ /// @return tokenIn The input token address
253
+ function getV3RouteInputCurrency(bytes memory path) internal pure returns (address tokenIn) {
254
+ if (path.length == 0) {
255
+ return address(0);
256
+ }
257
+
258
+ // Use Path library to get the input token (first token in the path)
259
+ (tokenIn, , ) = path.decodeFirstPool();
260
+ }
261
+ }
@@ -269,13 +269,14 @@ library V4Liquidity {
269
269
  salt: 0
270
270
  });
271
271
 
272
- (BalanceDelta liquidityDelta, BalanceDelta feesAccrued) = poolManager.modifyLiquidity(poolKey, params, "");
272
+ // callerDelta already includes fees, feesAccrued is informational only
273
+ (BalanceDelta callerDelta, ) = poolManager.modifyLiquidity(poolKey, params, "");
273
274
 
274
275
  burnedPositions[i] = BurnedPosition({
275
276
  tickLower: positions[i].tickLower,
276
277
  tickUpper: positions[i].tickUpper,
277
- amount0Received: uint128(liquidityDelta.amount0() + feesAccrued.amount0()),
278
- amount1Received: uint128(liquidityDelta.amount1() + feesAccrued.amount1())
278
+ amount0Received: uint128(callerDelta.amount0()),
279
+ amount1Received: uint128(callerDelta.amount1())
279
280
  });
280
281
  }
281
282
  }
@@ -315,20 +316,63 @@ library V4Liquidity {
315
316
  feeGrowthInside1DeltaX128 = feeGrowthInside1X128 - feeGrowthInside1LastX128;
316
317
  }
317
318
 
319
+ /// @notice Mints liquidity positions into the pool
320
+ /// @dev Uses a defensive balance check to prevent ERC20InsufficientBalance errors during migration.
321
+ /// When burning positions from an old hook, the amounts received may not exactly match what's needed
322
+ /// to mint the same liquidity in the new hook due to:
323
+ /// 1. Rounding in getLiquidityForAmounts() when converting between liquidity and token amounts
324
+ /// 2. Price movements between burn and mint operations
325
+ /// 3. Any accumulated dust from previous operations
326
+ /// By capping each position's liquidity at what's actually mintable with remaining balances,
327
+ /// we ensure the migration never reverts due to insufficient tokens.
318
328
  function mintPositions(IPoolManager poolManager, PoolKey memory poolKey, LpPosition[] memory positions) internal returns (int128 amount0, int128 amount1) {
319
- ModifyLiquidityParams memory params;
320
329
  uint256 numPositions = positions.length;
321
330
 
331
+ // Track remaining token balances throughout minting.
332
+ // These balances decrease as each position consumes tokens.
333
+ uint256 balance0 = poolKey.currency0.balanceOf(address(this));
334
+ uint256 balance1 = poolKey.currency1.balanceOf(address(this));
335
+
336
+ // Cache sqrt price once for all liquidity calculations
337
+ (uint160 sqrtPriceX96, , , ) = StateLibrary.getSlot0(poolManager, poolKey.toId());
338
+
322
339
  for (uint256 i; i < numPositions; i++) {
323
- params = ModifyLiquidityParams({
340
+ if (positions[i].liquidity == 0) {
341
+ continue;
342
+ }
343
+
344
+ // Calculate the maximum liquidity we can mint given our remaining token balances.
345
+ // This is the key defensive check: even if the requested liquidity would require
346
+ // more tokens than we have (due to rounding), we cap it at what's actually possible.
347
+ uint128 maxLiquidity = LiquidityAmounts.getLiquidityForAmounts(
348
+ sqrtPriceX96,
349
+ TickMath.getSqrtPriceAtTick(positions[i].tickLower),
350
+ TickMath.getSqrtPriceAtTick(positions[i].tickUpper),
351
+ balance0,
352
+ balance1
353
+ );
354
+
355
+ // Use the lesser of requested liquidity and what we can actually afford
356
+ uint128 liquidityToMint = positions[i].liquidity < maxLiquidity ? positions[i].liquidity : maxLiquidity;
357
+
358
+ if (liquidityToMint == 0) {
359
+ continue;
360
+ }
361
+
362
+ ModifyLiquidityParams memory params = ModifyLiquidityParams({
324
363
  tickLower: positions[i].tickLower,
325
364
  tickUpper: positions[i].tickUpper,
326
- liquidityDelta: SafeCast.toInt256(positions[i].liquidity),
365
+ liquidityDelta: SafeCast.toInt256(liquidityToMint),
327
366
  salt: 0
328
367
  });
329
368
 
330
369
  (BalanceDelta delta, ) = poolManager.modifyLiquidity(poolKey, params, "");
331
370
 
371
+ // Update remaining balances for next iteration.
372
+ // delta.amount0/1 are negative when minting (tokens flow out), so adding them decreases our balance.
373
+ balance0 = uint256(int256(balance0) + int256(delta.amount0()));
374
+ balance1 = uint256(int256(balance1) + int256(delta.amount1()));
375
+
332
376
  amount0 += delta.amount0();
333
377
  amount1 += delta.amount1();
334
378
  }
@@ -0,0 +1,73 @@
1
+ // SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
2
+ // This software is licensed under the Zora Delayed Open Source License.
3
+ // Under this license, you may use, copy, modify, and distribute this software for
4
+ // non-commercial purposes only. Commercial use and competitive products are prohibited
5
+ // until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
6
+ // at which point this software automatically becomes available under the MIT License.
7
+ // Full license terms available at: https://docs.zora.co/coins/license
8
+ pragma solidity ^0.8.23;
9
+
10
+ import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
11
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
12
+ import {ContractVersionBase} from "../version/ContractVersionBase.sol";
13
+ import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
14
+
15
+ /// @title TrustedMsgSenderProviderLookup
16
+ /// @notice Contract for ITrustedMsgSenderProviderLookup that manages trusted message senders
17
+ /// @dev This contract allows the owner to add/remove trusted senders and provides lookup functionality
18
+ contract TrustedMsgSenderProviderLookup is ITrustedMsgSenderProviderLookup, ContractVersionBase, Ownable2Step {
19
+ /// @notice Emitted when a trusted sender is added
20
+ /// @param sender The address that was added as trusted
21
+ event TrustedSenderAdded(address indexed sender);
22
+
23
+ /// @notice Emitted when a trusted sender is removed
24
+ /// @param sender The address that was removed from trusted
25
+ event TrustedSenderRemoved(address indexed sender);
26
+
27
+ /// @notice Mapping of addresses to their trusted sender status
28
+ mapping(address => bool) private trustedSenders;
29
+
30
+ /// @notice Constructor that initializes the contract with trusted senders and sets the owner
31
+ /// @param trustedMessageSenders Array of addresses to mark as trusted senders initially
32
+ /// @param initialOwner The address that will own this contract
33
+ constructor(address[] memory trustedMessageSenders, address initialOwner) Ownable(initialOwner) {
34
+ for (uint256 i = 0; i < trustedMessageSenders.length; i++) {
35
+ trustedSenders[trustedMessageSenders[i]] = true;
36
+ emit TrustedSenderAdded(trustedMessageSenders[i]);
37
+ }
38
+ }
39
+
40
+ /// @notice Checks if an address is a trusted message sender provider
41
+ /// @param sender The address to check
42
+ /// @return true if the sender is trusted, false otherwise
43
+ function isTrustedMsgSenderProvider(address sender) external view override returns (bool) {
44
+ return trustedSenders[sender];
45
+ }
46
+
47
+ /// @notice Adds multiple trusted senders in a single transaction (only callable by owner)
48
+ /// @param senders Array of addresses to add as trusted
49
+ function addTrustedMsgSenderProviders(address[] calldata senders) external onlyOwner {
50
+ for (uint256 i = 0; i < senders.length; i++) {
51
+ address sender = senders[i];
52
+ require(sender != address(0), "Cannot add zero address as trusted sender");
53
+
54
+ if (!trustedSenders[sender]) {
55
+ trustedSenders[sender] = true;
56
+ emit TrustedSenderAdded(sender);
57
+ }
58
+ }
59
+ }
60
+
61
+ /// @notice Removes multiple trusted senders in a single transaction (only callable by owner)
62
+ /// @param senders Array of addresses to remove from trusted
63
+ function removeTrustedMsgSenderProviders(address[] calldata senders) external onlyOwner {
64
+ for (uint256 i = 0; i < senders.length; i++) {
65
+ address sender = senders[i];
66
+
67
+ if (trustedSenders[sender]) {
68
+ trustedSenders[sender] = false;
69
+ emit TrustedSenderRemoved(sender);
70
+ }
71
+ }
72
+ }
73
+ }
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
9
9
  contract ContractVersionBase is IVersionedContract {
10
10
  /// @notice The version of the contract
11
11
  function contractVersion() external pure override returns (string memory) {
12
- return "2.3.1";
12
+ return "2.4.1";
13
13
  }
14
14
  }