@zoralabs/coins 2.2.1 → 2.3.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 (88) hide show
  1. package/.turbo/turbo-build$colon$js.log +125 -106
  2. package/CHANGELOG.md +50 -5
  3. package/README.md +5 -0
  4. package/abis/AddressConstants.json +7 -0
  5. package/abis/BaseCoin.json +0 -5
  6. package/abis/BaseTest.json +62 -0
  7. package/abis/BuySupplyWithV4SwapHook.json +429 -0
  8. package/abis/ContentCoin.json +0 -5
  9. package/abis/CreatorCoin.json +0 -5
  10. package/abis/FeeEstimatorHook.json +94 -1
  11. package/abis/IUniswapV4Router04.json +484 -0
  12. package/abis/IUpgradeableDestinationV4HookWithUpdateableFee.json +95 -0
  13. package/abis/IZoraFactory.json +69 -0
  14. package/abis/MockAirlock.json +39 -0
  15. package/abis/SimpleERC20.json +326 -0
  16. package/abis/ZoraFactoryImpl.json +69 -0
  17. package/abis/ZoraV4CoinHook.json +94 -1
  18. package/addresses/8453.json +8 -10
  19. package/audits/report-cantinacode-zora-0827.pdf +3498 -4
  20. package/audits/report-cantinacode-zora-1021.pdf +0 -0
  21. package/dist/index.cjs +161 -22
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +160 -21
  24. package/dist/index.js.map +1 -1
  25. package/dist/wagmiGenerated.d.ts +259 -40
  26. package/dist/wagmiGenerated.d.ts.map +1 -1
  27. package/foundry.toml +3 -3
  28. package/package/wagmiGenerated.ts +160 -21
  29. package/package.json +1 -1
  30. package/script/DeployPostDeploymentHooks.s.sol +1 -3
  31. package/script/TestBackingCoinSwap.s.sol +0 -2
  32. package/script/TestV4Swap.s.sol +0 -2
  33. package/src/BaseCoin.sol +4 -12
  34. package/src/ContentCoin.sol +3 -4
  35. package/src/CreatorCoin.sol +8 -10
  36. package/src/ZoraFactoryImpl.sol +115 -83
  37. package/src/deployment/CoinsDeployerBase.sol +9 -8
  38. package/src/hook-registry/ZoraHookRegistry.sol +4 -0
  39. package/src/hooks/ZoraV4CoinHook.sol +66 -9
  40. package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +310 -0
  41. package/src/interfaces/IUpgradeableV4Hook.sol +18 -0
  42. package/src/interfaces/IZoraFactory.sol +21 -2
  43. package/src/libs/CoinConstants.sol +51 -8
  44. package/src/libs/CoinDopplerMultiCurve.sol +11 -11
  45. package/src/libs/CoinRewardsV4.sol +26 -33
  46. package/src/libs/CoinSetup.sol +2 -9
  47. package/src/libs/DopplerMath.sol +2 -2
  48. package/src/libs/V4Liquidity.sol +79 -15
  49. package/src/utils/AutoSwapper.sol +1 -1
  50. package/src/version/ContractVersionBase.sol +1 -1
  51. package/test/BuySupplyWithV4SwapHook.t.sol +509 -0
  52. package/test/Coin.t.sol +26 -14
  53. package/test/CoinRewardsV4.t.sol +33 -0
  54. package/test/CoinUniV4.t.sol +3 -5
  55. package/test/ContentCoinRewards.t.sol +44 -3
  56. package/test/CreatorCoin.t.sol +54 -33
  57. package/test/CreatorCoinRewards.t.sol +1 -3
  58. package/test/DeploymentHooks.t.sol +54 -2
  59. package/test/Factory.t.sol +3 -3
  60. package/test/LiquidityMigration.t.sol +145 -7
  61. package/test/MultiOwnable.t.sol +4 -4
  62. package/test/Upgrades.t.sol +26 -17
  63. package/test/V4Liquidity.t.sol +178 -0
  64. package/test/ZoraHookRegistry.t.sol +19 -9
  65. package/test/mocks/MockAirlock.sol +22 -0
  66. package/test/mocks/SimpleERC20.sol +8 -0
  67. package/test/utils/BaseTest.sol +155 -3
  68. package/test/utils/RewardTestHelpers.sol +4 -4
  69. package/test/utils/hookmate/README.md +50 -0
  70. package/test/utils/hookmate/artifacts/DeployHelper.sol +20 -0
  71. package/test/utils/hookmate/artifacts/Permit2.sol +16 -0
  72. package/test/utils/hookmate/artifacts/UniversalRouter.sol +29 -0
  73. package/test/utils/hookmate/artifacts/V4PoolManager.sol +17 -0
  74. package/test/utils/hookmate/artifacts/V4PositionManager.sol +23 -0
  75. package/test/utils/hookmate/artifacts/V4Quoter.sol +17 -0
  76. package/test/utils/hookmate/artifacts/V4Router.sol +18 -0
  77. package/test/utils/hookmate/constants/AddressConstants.sol +193 -0
  78. package/test/utils/hookmate/interfaces/router/IUniswapV4Router04.sol +173 -0
  79. package/test/utils/hookmate/interfaces/router/PathKey.sol +34 -0
  80. package/test/utils/hookmate/test/utils/SwapFeeEventAsserter.sol +24 -0
  81. package/wagmi.config.ts +1 -1
  82. package/abis/CoinConstants.json +0 -54
  83. package/abis/CoinRewardsV4.json +0 -67
  84. package/src/libs/CreatorCoinConstants.sol +0 -15
  85. package/src/libs/MarketConstants.sol +0 -23
  86. package/src/utils/uniswap/BytesLib.sol +0 -35
  87. package/src/utils/uniswap/Path.sol +0 -31
  88. /package/abis/{VmContractHelper227.json → VmContractHelper239.json} +0 -0
@@ -34,7 +34,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
34
34
  import {CoinSetup} from "./libs/CoinSetup.sol";
35
35
  import {CoinDopplerMultiCurve} from "./libs/CoinDopplerMultiCurve.sol";
36
36
  import {ICreatorCoin} from "./interfaces/ICreatorCoin.sol";
37
- import {MarketConstants} from "./libs/MarketConstants.sol";
38
37
  import {DeployedCoinVersionLookup} from "./utils/DeployedCoinVersionLookup.sol";
39
38
  import {IZoraHookRegistry} from "./interfaces/IZoraHookRegistry.sol";
40
39
 
@@ -67,15 +66,7 @@ contract ZoraFactoryImpl is
67
66
  zoraHookRegistry = zoraHookRegistry_;
68
67
  }
69
68
 
70
- /// @notice Creates a new creator coin contract
71
- /// @param payoutRecipient The recipient of creator reward payouts; this can be updated by an owner
72
- /// @param owners The list of addresses that will be able to manage the coin's payout address and metadata uri
73
- /// @param uri The coin metadata uri
74
- /// @param name The name of the coin
75
- /// @param symbol The symbol of the coin
76
- /// @param poolConfig The config parameters for the coin's pool
77
- /// @param platformReferrer The address of the platform referrer
78
- /// @param coinSalt The salt used to deploy the coin
69
+ /// @inheritdoc IZoraFactory
79
70
  function deployCreatorCoin(
80
71
  address payoutRecipient,
81
72
  address[] memory owners,
@@ -87,39 +78,24 @@ contract ZoraFactoryImpl is
87
78
  bytes32 coinSalt
88
79
  ) public nonReentrant returns (address) {
89
80
  bytes32 salt = _buildSalt(msg.sender, name, symbol, poolConfig, platformReferrer, coinSalt);
81
+ return address(_createAndInitializeCreatorCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, salt));
82
+ }
90
83
 
91
- uint8 version = CoinConfigurationVersions.getVersion(poolConfig);
92
-
93
- require(version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, InvalidConfig());
94
-
95
- address creatorCoin = Clones.cloneDeterministic(creatorCoinImpl, salt);
96
-
97
- _setVersionForDeployedCoin(address(creatorCoin), version);
98
-
99
- (, address currency, uint160 sqrtPriceX96, bool isCoinToken0, PoolConfiguration memory poolConfiguration) = CoinSetup.generatePoolConfig(
100
- address(creatorCoin),
101
- poolConfig
102
- );
103
-
104
- PoolKey memory poolKey = CoinSetup.buildPoolKey(address(creatorCoin), currency, isCoinToken0, IHooks(hook));
105
-
106
- ICreatorCoin(creatorCoin).initialize(payoutRecipient, owners, uri, name, symbol, platformReferrer, currency, poolKey, sqrtPriceX96, poolConfiguration);
107
-
108
- emit CreatorCoinCreated(
109
- msg.sender,
110
- payoutRecipient,
111
- platformReferrer,
112
- currency,
113
- uri,
114
- name,
115
- symbol,
116
- address(creatorCoin),
117
- poolKey,
118
- CoinCommon.hashPoolKey(poolKey),
119
- IVersionedContract(address(creatorCoin)).contractVersion()
120
- );
121
-
122
- return creatorCoin;
84
+ /// @inheritdoc IZoraFactory
85
+ function deployCreatorCoin(
86
+ address payoutRecipient,
87
+ address[] memory owners,
88
+ string memory uri,
89
+ string memory name,
90
+ string memory symbol,
91
+ bytes memory poolConfig,
92
+ address platformReferrer,
93
+ address postDeployHook,
94
+ bytes calldata postDeployHookData,
95
+ bytes32 coinSalt
96
+ ) external payable nonReentrant returns (address coin, bytes memory postDeployHookDataOut) {
97
+ bytes32 salt = _buildSalt(msg.sender, name, symbol, poolConfig, platformReferrer, coinSalt);
98
+ return _deployCreatorCoinWithHook(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, postDeployHook, postDeployHookData, salt);
123
99
  }
124
100
 
125
101
  /// @inheritdoc IZoraFactory
@@ -152,6 +128,18 @@ contract ZoraFactoryImpl is
152
128
  return Clones.predictDeterministicAddress(getCoinImpl(CoinConfigurationVersions.getVersion(poolConfig)), salt, address(this));
153
129
  }
154
130
 
131
+ function _executePostDeployHook(address coin, address deployHook, bytes calldata hookData) internal returns (bytes memory hookDataOut) {
132
+ if (deployHook != address(0)) {
133
+ if (!IERC165(deployHook).supportsInterface(type(IHasAfterCoinDeploy).interfaceId)) {
134
+ revert InvalidHook();
135
+ }
136
+ hookDataOut = IHasAfterCoinDeploy(deployHook).afterCoinDeploy{value: msg.value}(msg.sender, ICoin(coin), hookData);
137
+ } else if (msg.value > 0) {
138
+ // cannot send eth without a hook
139
+ revert EthTransferInvalid();
140
+ }
141
+ }
142
+
155
143
  /// @dev Internal function to deploy a coin with a hook
156
144
  function _deployWithHook(
157
145
  address payoutRecipient,
@@ -166,16 +154,24 @@ contract ZoraFactoryImpl is
166
154
  bytes32 salt
167
155
  ) internal returns (address coin, bytes memory hookDataOut) {
168
156
  coin = address(_createAndInitializeCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, salt));
157
+ hookDataOut = _executePostDeployHook(coin, deployHook, hookData);
158
+ }
169
159
 
170
- if (deployHook != address(0)) {
171
- if (!IERC165(deployHook).supportsInterface(type(IHasAfterCoinDeploy).interfaceId)) {
172
- revert InvalidHook();
173
- }
174
- hookDataOut = IHasAfterCoinDeploy(deployHook).afterCoinDeploy{value: msg.value}(msg.sender, ICoin(coin), hookData);
175
- } else if (msg.value > 0) {
176
- // cannot send eth without a hook
177
- revert EthTransferInvalid();
178
- }
160
+ /// @dev Internal function to deploy a creator coin with a hook
161
+ function _deployCreatorCoinWithHook(
162
+ address payoutRecipient,
163
+ address[] memory owners,
164
+ string memory uri,
165
+ string memory name,
166
+ string memory symbol,
167
+ bytes memory poolConfig,
168
+ address platformReferrer,
169
+ address deployHook,
170
+ bytes calldata hookData,
171
+ bytes32 salt
172
+ ) internal returns (address coin, bytes memory hookDataOut) {
173
+ coin = address(_createAndInitializeCreatorCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer, salt));
174
+ hookDataOut = _executePostDeployHook(coin, deployHook, hookData);
179
175
  }
180
176
 
181
177
  /**
@@ -252,29 +248,55 @@ contract ZoraFactoryImpl is
252
248
  revert ICoin.InvalidPoolVersion();
253
249
  }
254
250
 
255
- function _createCoin(uint8 version, bytes32 salt) internal returns (address payable) {
256
- return payable(Clones.cloneDeterministic(getCoinImpl(version), salt));
251
+ function _createCoinWithPoolConfig(
252
+ address _implementation,
253
+ bytes memory poolConfig,
254
+ bytes32 coinSalt,
255
+ address payoutRecipient,
256
+ address[] memory owners,
257
+ string memory uri,
258
+ string memory name,
259
+ string memory symbol,
260
+ address platformReferrer
261
+ ) internal returns (address coin, uint8 version, PoolKey memory poolKey, address currency) {
262
+ version = CoinConfigurationVersions.getVersion(poolConfig);
263
+ coin = Clones.cloneDeterministic(_implementation, coinSalt);
264
+ _setVersionForDeployedCoin(coin, version);
265
+
266
+ uint160 sqrtPriceX96;
267
+ bool isCoinToken0;
268
+ PoolConfiguration memory poolConfiguration;
269
+ (, currency, sqrtPriceX96, isCoinToken0, poolConfiguration) = CoinSetup.generatePoolConfig(coin, poolConfig);
270
+
271
+ poolKey = CoinSetup.buildPoolKey(coin, currency, isCoinToken0, IHooks(hook));
272
+ ICoin(coin).initialize(payoutRecipient, owners, uri, name, symbol, platformReferrer, currency, poolKey, sqrtPriceX96, poolConfiguration);
257
273
  }
258
274
 
259
- function _setupV4Coin(
260
- ICoin coin,
261
- address currency,
262
- bool isCoinToken0,
263
- uint160 sqrtPriceX96,
264
- PoolConfiguration memory poolConfiguration,
275
+ function _createAndInitializeCreatorCoin(
265
276
  address payoutRecipient,
266
277
  address[] memory owners,
267
278
  string memory uri,
268
279
  string memory name,
269
280
  string memory symbol,
270
- address platformReferrer
271
- ) internal {
272
- PoolKey memory poolKey = CoinSetup.buildPoolKey(address(coin), currency, isCoinToken0, IHooks(hook));
281
+ bytes memory poolConfig,
282
+ address platformReferrer,
283
+ bytes32 coinSalt
284
+ ) internal returns (ICreatorCoin) {
285
+ (address creatorCoin, uint8 version, PoolKey memory poolKey, address currency) = _createCoinWithPoolConfig(
286
+ creatorCoinImpl,
287
+ poolConfig,
288
+ coinSalt,
289
+ payoutRecipient,
290
+ owners,
291
+ uri,
292
+ name,
293
+ symbol,
294
+ platformReferrer
295
+ );
273
296
 
274
- // Initialize coin with pre-configured pool
275
- coin.initialize(payoutRecipient, owners, uri, name, symbol, platformReferrer, currency, poolKey, sqrtPriceX96, poolConfiguration);
297
+ require(version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, InvalidConfig());
276
298
 
277
- emit CoinCreatedV4(
299
+ emit CreatorCoinCreated(
278
300
  msg.sender,
279
301
  payoutRecipient,
280
302
  platformReferrer,
@@ -282,11 +304,13 @@ contract ZoraFactoryImpl is
282
304
  uri,
283
305
  name,
284
306
  symbol,
285
- address(coin),
307
+ creatorCoin,
286
308
  poolKey,
287
309
  CoinCommon.hashPoolKey(poolKey),
288
- IVersionedContract(address(coin)).contractVersion()
310
+ IVersionedContract(creatorCoin).contractVersion()
289
311
  );
312
+
313
+ return ICreatorCoin(creatorCoin);
290
314
  }
291
315
 
292
316
  function _createAndInitializeCoin(
@@ -299,26 +323,34 @@ contract ZoraFactoryImpl is
299
323
  address platformReferrer,
300
324
  bytes32 coinSalt
301
325
  ) internal returns (ICoin) {
302
- uint8 version = CoinConfigurationVersions.getVersion(poolConfig);
303
-
304
- address payable coin = _createCoin(version, coinSalt);
326
+ (address coin, uint8 version, PoolKey memory poolKey, address currency) = _createCoinWithPoolConfig(
327
+ coinV4Impl,
328
+ poolConfig,
329
+ coinSalt,
330
+ payoutRecipient,
331
+ owners,
332
+ uri,
333
+ name,
334
+ symbol,
335
+ platformReferrer
336
+ );
305
337
 
306
- _setVersionForDeployedCoin(address(coin), version);
338
+ require(version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, ICoin.InvalidPoolVersion());
307
339
 
308
- (, address currency, uint160 sqrtPriceX96, bool isCoinToken0, PoolConfiguration memory poolConfiguration) = CoinSetup.generatePoolConfig(
309
- address(coin),
310
- poolConfig
340
+ emit CoinCreatedV4(
341
+ msg.sender,
342
+ payoutRecipient,
343
+ platformReferrer,
344
+ currency,
345
+ uri,
346
+ name,
347
+ symbol,
348
+ coin,
349
+ poolKey,
350
+ CoinCommon.hashPoolKey(poolKey),
351
+ IVersionedContract(coin).contractVersion()
311
352
  );
312
353
 
313
- if (CoinConfigurationVersions.isV3(version)) {
314
- // V3 is no longer supported
315
- revert ICoin.InvalidPoolVersion();
316
- } else if (CoinConfigurationVersions.isV4(version)) {
317
- _setupV4Coin(ICoin(coin), currency, isCoinToken0, sqrtPriceX96, poolConfiguration, payoutRecipient, owners, uri, name, symbol, platformReferrer);
318
- } else {
319
- revert ICoin.InvalidPoolVersion();
320
- }
321
-
322
354
  return ICoin(coin);
323
355
  }
324
356
 
@@ -18,6 +18,7 @@ import {ProxyShim} from "../../test/utils/ProxyShim.sol";
18
18
  import {CreatorCoin} from "../CreatorCoin.sol";
19
19
  import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
20
20
  import {HookUpgradeGate} from "../hooks/HookUpgradeGate.sol";
21
+ import {BuySupplyWithV4SwapHook} from "../hooks/deployment/BuySupplyWithV4SwapHook.sol";
21
22
 
22
23
  contract CoinsDeployerBase is ProxyDeployerScript {
23
24
  address internal constant PROTOCOL_REWARDS = 0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B;
@@ -123,14 +124,14 @@ contract CoinsDeployerBase is ProxyDeployerScript {
123
124
  return new ZoraFactoryImpl({coinV4Impl_: coinV4Impl_, creatorCoinImpl_: creatorCoinImpl_, hook_: hook_, zoraHookRegistry_: zoraHookRegistry_});
124
125
  }
125
126
 
126
- // function deployBuySupplyWithSwapRouterHook(CoinsDeployment memory deployment) internal returns (BuySupplyWithSwapRouterHook) {
127
- // return
128
- // new BuySupplyWithSwapRouterHook({
129
- // _factory: IZoraFactory(deployment.zoraFactory),
130
- // _swapRouter: getUniswapSwapRouter(),
131
- // _poolManager: getUniswapV4PoolManager()
132
- // });
133
- // }
127
+ function deployBuySupplyWithV4SwapHook(CoinsDeployment memory deployment) internal returns (BuySupplyWithV4SwapHook) {
128
+ return
129
+ new BuySupplyWithV4SwapHook({
130
+ _factory: IZoraFactory(deployment.zoraFactory),
131
+ _swapRouter: getUniswapSwapRouter(),
132
+ _poolManager: getUniswapV4PoolManager()
133
+ });
134
+ }
134
135
 
135
136
  function deployUpgradeGate(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
136
137
  deployment.hookUpgradeGate = address(new HookUpgradeGate(getProxyAdmin()));
@@ -23,6 +23,10 @@ contract ZoraHookRegistry is IZoraHookRegistry, MultiOwnable {
23
23
  /// @dev The tag for each hook
24
24
  mapping(address hook => string tag) internal hookTags;
25
25
 
26
+ /// @notice Constructor for deployment pathway
27
+ /// @dev This contract needs to be atomically initialized, otherwise it is unsafe to deploy.
28
+ /// @dev Initial owners need to be automatically set by the deployer contract.
29
+ /// @dev The initial owners is not a part of the constructor() to allow for multichain deterministic addresses.
26
30
  constructor() {}
27
31
 
28
32
  /// @notice Initializes the registry with initial owners
@@ -31,13 +31,14 @@ import {IUpgradeableV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
31
31
  import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
32
32
  import {MultiOwnable} from "../utils/MultiOwnable.sol";
33
33
  import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
34
- import {IUpgradeableDestinationV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
34
+ import {IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee} from "../interfaces/IUpgradeableV4Hook.sol";
35
35
  import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
36
36
  import {BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
37
37
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
38
38
  import {TickMath} from "../utils/uniswap/TickMath.sol";
39
39
  import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
40
40
  import {IHasCoinType} from "../interfaces/ICoin.sol";
41
+ import {CoinConstants} from "../libs/CoinConstants.sol";
41
42
 
42
43
  /// @title ZoraV4CoinHook
43
44
  /// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
@@ -49,7 +50,14 @@ import {IHasCoinType} from "../interfaces/ICoin.sol";
49
50
  /// 2. Swaps collected fees to the backing currency through multi-hop paths
50
51
  /// 3. Distributes converted fees as rewards
51
52
  /// @author oveddan
52
- contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC165, IUpgradeableDestinationV4Hook {
53
+ contract ZoraV4CoinHook is
54
+ BaseHook,
55
+ ContractVersionBase,
56
+ IZoraV4CoinHook,
57
+ ERC165,
58
+ IUpgradeableDestinationV4Hook,
59
+ IUpgradeableDestinationV4HookWithUpdateableFee
60
+ {
53
61
  using BalanceDeltaLibrary for BalanceDelta;
54
62
 
55
63
  /// @notice Mapping of trusted message senders - these are addresses that are trusted to provide a
@@ -130,6 +138,7 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
130
138
  return
131
139
  super.supportsInterface(interfaceId) ||
132
140
  interfaceId == type(IUpgradeableDestinationV4Hook).interfaceId ||
141
+ interfaceId == type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId ||
133
142
  interfaceId == type(IVersionedContract).interfaceId;
134
143
  }
135
144
 
@@ -137,10 +146,18 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
137
146
  /// @param coin The coin address.
138
147
  /// @param key The pool key for the coin.
139
148
  /// @return positions The contract-created liquidity positions the positions for the coin's pool.
140
- function _generatePositions(ICoin coin, PoolKey memory key) internal view returns (LpPosition[] memory positions) {
149
+ function _generatePositions(ICoin coin, PoolKey memory key) internal view returns (LpPosition[] memory) {
141
150
  bool isCoinToken0 = Currency.unwrap(key.currency0) == address(coin);
142
151
 
143
- positions = CoinDopplerMultiCurve.calculatePositions(isCoinToken0, coin.getPoolConfiguration(), coin.totalSupplyForPositions());
152
+ LpPosition[] memory calculatedPositions = CoinDopplerMultiCurve.calculatePositions(
153
+ isCoinToken0,
154
+ coin.getPoolConfiguration(),
155
+ coin.totalSupplyForPositions()
156
+ );
157
+
158
+ // sometimes the calculated positions have liquidity added in duplicated positions. So here we dedupe them
159
+ // to save on gas in future swaps.
160
+ return V4Liquidity.dedupePositions(calculatedPositions);
144
161
  }
145
162
 
146
163
  /// @notice Internal fn called when a pool is initialized.
@@ -168,17 +185,50 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
168
185
  }
169
186
 
170
187
  /// @inheritdoc IUpgradeableDestinationV4Hook
188
+ /// @dev left for backward compatibility for migrating from hooks that dont support
189
+ /// updating the fee
171
190
  function initializeFromMigration(
172
191
  PoolKey calldata poolKey,
173
192
  address coin,
174
193
  uint160 sqrtPriceX96,
175
194
  BurnedPosition[] calldata migratedLiquidity,
176
- bytes calldata
195
+ bytes calldata additionalData
177
196
  ) external {
197
+ // keep the existing fee and tick spacing.
198
+ uint24 fee = poolKey.fee;
199
+ int24 tickSpacing = poolKey.tickSpacing;
200
+
201
+ _initializeFromMigration(poolKey, coin, sqrtPriceX96, migratedLiquidity, additionalData, fee, tickSpacing);
202
+ }
203
+
204
+ /// @inheritdoc IUpgradeableDestinationV4HookWithUpdateableFee
205
+ function initializeFromMigrationWithUpdateableFee(
206
+ PoolKey calldata poolKey,
207
+ address coin,
208
+ uint160 sqrtPriceX96,
209
+ BurnedPosition[] calldata migratedLiquidity,
210
+ bytes calldata additionalData
211
+ ) external returns (uint24 fee, int24 tickSpacing) {
212
+ // update the fee to the current one.
213
+ fee = CoinConstants.LP_FEE_V4;
214
+ tickSpacing = CoinConstants.TICK_SPACING;
215
+
216
+ _initializeFromMigration(poolKey, coin, sqrtPriceX96, migratedLiquidity, additionalData, fee, tickSpacing);
217
+ }
218
+
219
+ function _initializeFromMigration(
220
+ PoolKey calldata poolKey,
221
+ address coin,
222
+ uint160 sqrtPriceX96,
223
+ BurnedPosition[] calldata migratedLiquidity,
224
+ bytes calldata,
225
+ uint24 fee,
226
+ int24 tickSpacing
227
+ ) internal {
178
228
  address oldHook = msg.sender;
179
229
  address newHook = address(this);
180
230
 
181
- // Verify that the caller (new hook) is authorized to perform this migration
231
+ // Verify that the caller (old hook) is authorized to perform this migration
182
232
  // Only registered upgrade paths in the upgrade gate are allowed to migrate liquidity
183
233
  if (!upgradeGate.isRegisteredUpgradePath(oldHook, newHook)) {
184
234
  revert IUpgradeableV4Hook.UpgradePathNotRegistered(oldHook, newHook);
@@ -189,8 +239,8 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
189
239
  PoolKey memory newKey = PoolKey({
190
240
  currency0: poolKey.currency0,
191
241
  currency1: poolKey.currency1,
192
- fee: poolKey.fee,
193
- tickSpacing: poolKey.tickSpacing,
242
+ fee: fee,
243
+ tickSpacing: tickSpacing,
194
244
  hooks: IHooks(newHook)
195
245
  });
196
246
 
@@ -343,7 +393,7 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
343
393
  return CoinRewardsV4.getCoinType(coin);
344
394
  }
345
395
 
346
- /// @notice Internal fn called when the PoolManager is unlocked. Used to mint initial liquidity positions.
396
+ /// @notice Internal fn called when the PoolManager is unlocked. Used to mint initial liquidity positions and burn positions during migration.
347
397
  function unlockCallback(bytes calldata data) external onlyPoolManager returns (bytes memory) {
348
398
  return V4Liquidity.handleCallback(poolManager, data);
349
399
  }
@@ -376,7 +426,14 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
376
426
  }
377
427
 
378
428
  newPoolKey = V4Liquidity.lockAndMigrate(poolManager, poolKey, poolCoin.positions, poolCoin.coin, newHook, additionalData);
429
+
430
+ // Delete the old pool key mapping to prevent future operations on the migrated pool
431
+ delete poolCoins[poolKeyHash];
379
432
  }
380
433
 
434
+ /// @notice Receives ETH from the pool manager for ETH-backed coins during fee collection.
435
+ /// @dev Only required for coins using ETH as backing currency (currency = address(0)).
436
+ /// Restricted to onlyPoolManager to prevent ETH from getting stuck in the contract.
437
+ /// Unused for ERC20-backed coins.
381
438
  receive() external payable onlyPoolManager {}
382
439
  }