@zoralabs/coins 2.3.0 → 2.4.0

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 (72) hide show
  1. package/.turbo/turbo-build$colon$js.log +119 -101
  2. package/CHANGELOG.md +31 -1
  3. package/README.md +1 -0
  4. package/abis/AddressConstants.json +7 -0
  5. package/abis/BaseTest.json +65 -3
  6. package/abis/BuySupplyWithV4SwapHook.json +429 -0
  7. package/abis/FeeEstimatorHook.json +23 -0
  8. package/abis/ITrustedMsgSenderProviderLookup.json +21 -0
  9. package/abis/IUniswapV4Router04.json +484 -0
  10. package/abis/IZoraV4CoinHook.json +5 -0
  11. package/abis/MockAirlock.json +39 -0
  12. package/abis/SimpleERC20.json +326 -0
  13. package/abis/TrustedMsgSenderProviderLookup.json +215 -0
  14. package/abis/VmContractHelper242.json +233 -0
  15. package/abis/ZoraV4CoinHook.json +21 -3
  16. package/addresses/8453.json +7 -9
  17. package/audits/report-cantinacode-zora-1021.pdf +0 -0
  18. package/dist/index.cjs +140 -19
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.js +139 -18
  21. package/dist/index.js.map +1 -1
  22. package/dist/wagmiGenerated.d.ts +205 -28
  23. package/dist/wagmiGenerated.d.ts.map +1 -1
  24. package/foundry.toml +5 -1
  25. package/package/wagmiGenerated.ts +139 -18
  26. package/package.json +3 -3
  27. package/script/DeployPostDeploymentHooks.s.sol +1 -3
  28. package/script/DeployTrustedMsgSenderLookup.s.sol +20 -0
  29. package/src/deployment/CoinsDeployerBase.sol +31 -9
  30. package/src/hooks/ZoraV4CoinHook.sol +19 -55
  31. package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +310 -0
  32. package/src/interfaces/ITrustedMsgSenderProviderLookup.sol +18 -0
  33. package/src/interfaces/IZoraV4CoinHook.sol +3 -0
  34. package/src/libs/HooksDeployment.sol +9 -8
  35. package/src/libs/V4Liquidity.sol +50 -6
  36. package/src/utils/AutoSwapper.sol +1 -1
  37. package/src/utils/TrustedMsgSenderProviderLookup.sol +73 -0
  38. package/src/version/ContractVersionBase.sol +1 -1
  39. package/test/BuySupplyWithV4SwapHook.t.sol +509 -0
  40. package/test/Coin.t.sol +21 -9
  41. package/test/CoinUniV4.t.sol +1 -2
  42. package/test/ContentCoinRewards.t.sol +1 -3
  43. package/test/CreatorCoin.t.sol +1 -4
  44. package/test/CreatorCoinRewards.t.sol +5 -3
  45. package/test/Factory.t.sol +3 -3
  46. package/test/HooksDeployment.t.sol +58 -6
  47. package/test/LiquidityMigration.t.sol +6 -2
  48. package/test/MultiOwnable.t.sol +4 -4
  49. package/test/TrustedMsgSenderProviderLookup.t.sol +112 -0
  50. package/test/Upgrades.t.sol +41 -27
  51. package/test/ZoraHookRegistry.t.sol +19 -9
  52. package/test/mocks/MockAirlock.sol +22 -0
  53. package/test/mocks/SimpleERC20.sol +8 -0
  54. package/test/utils/BaseTest.sol +185 -6
  55. package/test/utils/FeeEstimatorHook.sol +3 -1
  56. package/test/utils/TrustedSenderTestHelper.sol +18 -0
  57. package/test/utils/hookmate/README.md +50 -0
  58. package/test/utils/hookmate/artifacts/DeployHelper.sol +20 -0
  59. package/test/utils/hookmate/artifacts/Permit2.sol +16 -0
  60. package/test/utils/hookmate/artifacts/UniversalRouter.sol +29 -0
  61. package/test/utils/hookmate/artifacts/V4PoolManager.sol +17 -0
  62. package/test/utils/hookmate/artifacts/V4PositionManager.sol +23 -0
  63. package/test/utils/hookmate/artifacts/V4Quoter.sol +17 -0
  64. package/test/utils/hookmate/artifacts/V4Router.sol +18 -0
  65. package/test/utils/hookmate/constants/AddressConstants.sol +193 -0
  66. package/test/utils/hookmate/interfaces/router/IUniswapV4Router04.sol +173 -0
  67. package/test/utils/hookmate/interfaces/router/PathKey.sol +34 -0
  68. package/test/utils/hookmate/test/utils/SwapFeeEventAsserter.sol +24 -0
  69. package/wagmi.config.ts +1 -1
  70. package/src/utils/uniswap/BytesLib.sol +0 -35
  71. package/src/utils/uniswap/Path.sol +0 -31
  72. /package/abis/{VmContractHelper226.json → VmContractHelper235.json} +0 -0
@@ -18,6 +18,9 @@ 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";
22
+ import {TrustedMsgSenderProviderLookup} from "../utils/TrustedMsgSenderProviderLookup.sol";
23
+ import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
21
24
 
22
25
  contract CoinsDeployerBase is ProxyDeployerScript {
23
26
  address internal constant PROTOCOL_REWARDS = 0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B;
@@ -38,6 +41,8 @@ contract CoinsDeployerBase is ProxyDeployerScript {
38
41
  address buySupplyWithSwapRouterHook;
39
42
  address zoraV4CoinHook;
40
43
  address hookUpgradeGate;
44
+ // trusted sender lookup
45
+ address trustedMsgSenderLookup;
41
46
  // Hook deployment salt (for deterministic deployment)
42
47
  bytes32 zoraV4CoinHookSalt;
43
48
  bool isDev;
@@ -69,6 +74,7 @@ contract CoinsDeployerBase is ProxyDeployerScript {
69
74
  vm.serializeAddress(objectKey, "CREATOR_COIN_IMPL", deployment.creatorCoinImpl);
70
75
  vm.serializeAddress(objectKey, "HOOK_UPGRADE_GATE", deployment.hookUpgradeGate);
71
76
  vm.serializeAddress(objectKey, "ZORA_HOOK_REGISTRY", deployment.zoraHookRegistry);
77
+ vm.serializeAddress(objectKey, "TRUSTED_MSG_SENDER_LOOKUP", deployment.trustedMsgSenderLookup);
72
78
  string memory result = vm.serializeAddress(objectKey, "COIN_V4_IMPL", deployment.coinV4Impl);
73
79
 
74
80
  vm.writeJson(result, addressesFile(deployment.isDev));
@@ -97,6 +103,7 @@ contract CoinsDeployerBase is ProxyDeployerScript {
97
103
  deployment.creatorCoinImpl = readAddressOrDefaultToZero(json, "CREATOR_COIN_IMPL");
98
104
  deployment.hookUpgradeGate = readAddressOrDefaultToZero(json, "HOOK_UPGRADE_GATE");
99
105
  deployment.zoraHookRegistry = readAddressOrDefaultToZero(json, "ZORA_HOOK_REGISTRY");
106
+ deployment.trustedMsgSenderLookup = readAddressOrDefaultToZero(json, "TRUSTED_MSG_SENDER_LOOKUP");
100
107
  }
101
108
 
102
109
  function deployCoinV4Impl() internal returns (ContentCoin) {
@@ -123,14 +130,14 @@ contract CoinsDeployerBase is ProxyDeployerScript {
123
130
  return new ZoraFactoryImpl({coinV4Impl_: coinV4Impl_, creatorCoinImpl_: creatorCoinImpl_, hook_: hook_, zoraHookRegistry_: zoraHookRegistry_});
124
131
  }
125
132
 
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
- // }
133
+ function deployBuySupplyWithV4SwapHook(CoinsDeployment memory deployment) internal returns (BuySupplyWithV4SwapHook) {
134
+ return
135
+ new BuySupplyWithV4SwapHook({
136
+ _factory: IZoraFactory(deployment.zoraFactory),
137
+ _swapRouter: getUniswapSwapRouter(),
138
+ _poolManager: getUniswapV4PoolManager()
139
+ });
140
+ }
134
141
 
135
142
  function deployUpgradeGate(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
136
143
  deployment.hookUpgradeGate = address(new HookUpgradeGate(getProxyAdmin()));
@@ -138,14 +145,23 @@ contract CoinsDeployerBase is ProxyDeployerScript {
138
145
  return deployment;
139
146
  }
140
147
 
148
+ function deployTrustedMsgSenderLookup(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
149
+ // Deploy the contract directly using constructor
150
+ deployment.trustedMsgSenderLookup = address(new TrustedMsgSenderProviderLookup(getDefaultTrustedMessageSenders(), getProxyAdmin()));
151
+
152
+ return deployment;
153
+ }
154
+
141
155
  function deployZoraV4CoinHook(CoinsDeployment memory deployment) internal returns (IHooks hook, bytes32 salt) {
156
+ require(deployment.trustedMsgSenderLookup != address(0), "Trusted message sender lookup not deployed");
157
+
142
158
  return
143
159
  HooksDeployment.deployHookWithExistingOrNewSalt(
144
160
  HooksDeployment.FOUNDRY_SCRIPT_ADDRESS,
145
161
  HooksDeployment.makeHookCreationCode(
146
162
  getUniswapV4PoolManager(),
147
163
  deployment.zoraFactory,
148
- getDefaultTrustedMessageSenders(),
164
+ ITrustedMsgSenderProviderLookup(deployment.trustedMsgSenderLookup),
149
165
  deployment.hookUpgradeGate
150
166
  ),
151
167
  deployment.zoraV4CoinHookSalt
@@ -174,6 +190,9 @@ contract CoinsDeployerBase is ProxyDeployerScript {
174
190
  function deployImpls(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
175
191
  // Deploy implementation contracts
176
192
 
193
+ // Deploy trusted message sender lookup first
194
+ deployment = deployTrustedMsgSenderLookup(deployment);
195
+
177
196
  // Deploy hook first, then use its address for coin v4 impl
178
197
  console.log("deploying content coin hook");
179
198
  (IHooks zoraV4CoinHook, bytes32 usedSalt) = deployZoraV4CoinHook(deployment);
@@ -190,6 +209,9 @@ contract CoinsDeployerBase is ProxyDeployerScript {
190
209
  }
191
210
 
192
211
  function deployHooks(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
212
+ // Deploy trusted message sender lookup first
213
+ deployment = deployTrustedMsgSenderLookup(deployment);
214
+
193
215
  // Deploy hook first, then use its address for coin v4 impl
194
216
  (IHooks zoraV4CoinHook, bytes32 usedSalt) = deployZoraV4CoinHook(deployment);
195
217
  deployment.zoraV4CoinHook = address(zoraV4CoinHook);
@@ -16,6 +16,7 @@ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
16
16
  import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
17
17
  import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
18
18
  import {IMsgSender} from "../interfaces/IMsgSender.sol";
19
+ import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
19
20
  import {IHasSwapPath} from "../interfaces/ICoin.sol";
20
21
  import {LpPosition} from "../types/LpPosition.sol";
21
22
  import {V4Liquidity} from "../libs/V4Liquidity.sol";
@@ -60,9 +61,10 @@ contract ZoraV4CoinHook is
60
61
  {
61
62
  using BalanceDeltaLibrary for BalanceDelta;
62
63
 
63
- /// @notice Mapping of trusted message senders - these are addresses that are trusted to provide a
64
- /// an original msg.sender
65
- mapping(address => bool) internal trustedMessageSender;
64
+ /// @dev DEPRECATED: This mapping is kept for storage compatibility. It doesn't matter that storage slots moved around
65
+ /// between versions since the contracts are immutable, but in some tests we do etching to test if a new hook fixes some bugs, so we want to maintain the storage slot order.
66
+ /// This slot previously held the mappings of trusted message senders.
67
+ mapping(address => bool) internal legacySlot0;
66
68
 
67
69
  /// @notice Mapping of pool keys to coins.
68
70
  mapping(bytes32 => IZoraV4CoinHook.PoolCoin) internal poolCoins;
@@ -73,27 +75,34 @@ contract ZoraV4CoinHook is
73
75
  /// @notice The upgrade gate contract - used to verify allowed upgrade paths
74
76
  IHooksUpgradeGate internal immutable upgradeGate;
75
77
 
78
+ /// @notice The trusted message sender lookup contract - used to determine if an address is trusted
79
+ ITrustedMsgSenderProviderLookup internal immutable trustedMsgSenderLookup;
80
+
76
81
  /// @notice The constructor for the ZoraV4CoinHook.
77
82
  /// @param poolManager_ The Uniswap V4 pool manager
78
83
  /// @param coinVersionLookup_ The coin version lookup contract - used to determine if an address is a coin and what version it is.
79
- /// @param trustedMessageSenders_ The addresses of the trusted message senders - these are addresses that are trusted to provide a
84
+ /// @param trustedMsgSenderLookup_ The trusted message sender lookup contract - used to determine if an address is trusted
80
85
  /// @param upgradeGate_ The upgrade gate contract for managing hook upgrades
81
86
  constructor(
82
87
  IPoolManager poolManager_,
83
88
  IDeployedCoinVersionLookup coinVersionLookup_,
84
- address[] memory trustedMessageSenders_,
89
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup_,
85
90
  IHooksUpgradeGate upgradeGate_
86
91
  ) BaseHook(poolManager_) {
87
92
  require(address(coinVersionLookup_) != address(0), CoinVersionLookupCannotBeZeroAddress());
88
93
 
89
94
  require(address(upgradeGate_) != address(0), UpgradeGateCannotBeZeroAddress());
90
95
 
96
+ require(address(trustedMsgSenderLookup_) != address(0), TrustedMsgSenderLookupCannotBeZeroAddress());
97
+
91
98
  coinVersionLookup = coinVersionLookup_;
92
99
  upgradeGate = upgradeGate_;
100
+ trustedMsgSenderLookup = trustedMsgSenderLookup_;
101
+ }
93
102
 
94
- for (uint256 i = 0; i < trustedMessageSenders_.length; i++) {
95
- trustedMessageSender[trustedMessageSenders_[i]] = true;
96
- }
103
+ /// @notice Returns the trusted message sender lookup contract
104
+ function getTrustedMsgSenderLookup() external view returns (ITrustedMsgSenderProviderLookup) {
105
+ return trustedMsgSenderLookup;
97
106
  }
98
107
 
99
108
  /// @notice Returns the uniswap v4 hook settings / permissions.
@@ -120,7 +129,7 @@ contract ZoraV4CoinHook is
120
129
 
121
130
  /// @inheritdoc IZoraV4CoinHook
122
131
  function isTrustedMessageSender(address sender) external view returns (bool) {
123
- return trustedMessageSender[sender];
132
+ return trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
124
133
  }
125
134
 
126
135
  /// @inheritdoc IZoraV4CoinHook
@@ -256,51 +265,6 @@ contract ZoraV4CoinHook is
256
265
 
257
266
  // Store the positions and mint the initial liquidity into the new pool
258
267
  _initializeForPositions(newKey, coin, positions);
259
-
260
- // Handle any remaining token balances by adding them to the last position
261
- // This ensures no tokens are left unminted during the migration process
262
- _mintExtraLiquidityAtLastPosition(sqrtPriceX96, newKey);
263
- }
264
-
265
- /// @notice Internal fn to add any remaining token balances to the last liquidity position.
266
- /// @param sqrtPriceX96 The sqrt price x96.
267
- /// @param poolKey The pool key.
268
- function _mintExtraLiquidityAtLastPosition(uint160 sqrtPriceX96, PoolKey memory poolKey) internal {
269
- // Check if there are any leftover token balances in the hook after migration
270
- // These could result from rounding or partial liquidity transfers
271
- uint256 currency0Balance = poolKey.currency0.balanceOfSelf();
272
- uint256 currency1Balance = poolKey.currency1.balanceOfSelf();
273
-
274
- // Get the stored positions for this pool to access the last position
275
- LpPosition[] storage positions = poolCoins[CoinCommon.hashPoolKey(poolKey)].positions;
276
-
277
- // Only proceed if there are actually leftover tokens to mint
278
- if (currency0Balance > 0 || currency1Balance > 0) {
279
- // Get reference to the last position where we'll add the extra liquidity
280
- LpPosition storage lastPosition = positions[positions.length - 1];
281
-
282
- // Calculate how much liquidity we can create with the remaining token balances
283
- // This uses the current pool price and the last position's tick range
284
- uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts(
285
- sqrtPriceX96,
286
- TickMath.getSqrtPriceAtTick(lastPosition.tickLower),
287
- TickMath.getSqrtPriceAtTick(lastPosition.tickUpper),
288
- currency0Balance,
289
- currency1Balance
290
- );
291
-
292
- // Create a temporary array with just the last position to mint the extra liquidity
293
- LpPosition[] memory newPositions = new LpPosition[](1);
294
- newPositions[0] = lastPosition;
295
- newPositions[0].liquidity = newLiquidity; // Set the calculated liquidity amount
296
-
297
- // Mint the extra liquidity into the pool using the V4 liquidity manager
298
- V4Liquidity.lockAndMint(poolManager, poolKey, newPositions);
299
-
300
- // Update our internal tracking of the last position's liquidity
301
- // This keeps our records in sync with the actual pool state
302
- positions[positions.length - 1].liquidity += newPositions[0].liquidity;
303
- }
304
268
  }
305
269
 
306
270
  /// @notice Saves the positions for the coin and mints them into the pool
@@ -403,7 +367,7 @@ contract ZoraV4CoinHook is
403
367
  /// @return swapper The original message sender.
404
368
  /// @return senderIsTrusted Whether the sender is a trusted message sender.
405
369
  function _getOriginalMsgSender(address sender) internal view returns (address swapper, bool senderIsTrusted) {
406
- senderIsTrusted = trustedMessageSender[sender];
370
+ senderIsTrusted = trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
407
371
 
408
372
  // If getter function reverts, we return a 0 address by default and continue execution.
409
373
  try IMsgSender(sender).msgSender() returns (address _swapper) {
@@ -0,0 +1,310 @@
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 {BaseCoinDeployHook} from "./BaseCoinDeployHook.sol";
12
+ import {IUniswapV3SwapCallback} from "../../interfaces/IUniswapV3SwapCallback.sol";
13
+ import {ICoin} from "../../interfaces/ICoin.sol";
14
+ import {ISwapRouter} from "../../interfaces/ISwapRouter.sol";
15
+ import {IZoraFactory} from "../../interfaces/IZoraFactory.sol";
16
+ import {ICoinV3} from "../../interfaces/ICoinV3.sol";
17
+ import {CoinConfigurationVersions} from "../../libs/CoinConfigurationVersions.sol";
18
+ import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
19
+ import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
20
+
21
+ /// @title BuySupplyWithV4SwapHook
22
+ /// @notice Hook for purchasing initial coin supply with flexible swap routing
23
+ /// @dev Capabilities:
24
+ /// - ETH → V3 swap → V4 swap → coin (e.g., ETH → ZORA → Creator Coin → Content Coin)
25
+ /// - ETH → V3 swap → coin (e.g., ETH → ZORA for ZORA-backed coin)
26
+ /// - ETH → V4 swap → coin (direct ETH-paired coins)
27
+ /// - ERC20 → V4 swap → coin (e.g., Creator Coins → Content Coin)
28
+ /// - Slippage protection with minAmountOut validation
29
+ ///
30
+ /// Limitations:
31
+ /// - V3 swaps only support ETH as input currency
32
+ /// - ERC20 input currencies require pre-approval
33
+ /// - V3 and V4 routes must connect properly (V3 output = V4 input)
34
+ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
35
+ using BalanceDeltaLibrary for BalanceDelta;
36
+ using SafeERC20 for IERC20;
37
+ using CurrencyLibrary for Currency;
38
+ using Path for bytes;
39
+
40
+ // ============ STATE VARIABLES ============
41
+
42
+ ISwapRouter public immutable swapRouter;
43
+ IPoolManager public immutable poolManager;
44
+
45
+ // ============ STRUCTS ============
46
+
47
+ struct InitialSupplyParams {
48
+ address buyRecipient; // Who gets the coins
49
+ bytes v3Route; // V3 route from ETH to backing currency
50
+ PoolKey[] v4Route; // V4 route from backing currency to coin
51
+ address inputCurrency; // Currency to use for the V3 swap
52
+ uint256 inputAmount; // Amount of input currency to use for the V3 swap
53
+ uint256 minAmountOut; // Minimum amount of coins to receive from final swap
54
+ }
55
+
56
+ event BuyInitialSupply(
57
+ address indexed coin,
58
+ address indexed recipient,
59
+ uint256 indexed coinsPurchased,
60
+ bytes v3Route,
61
+ PoolKey[] v4Route,
62
+ address inputCurrency,
63
+ uint256 inputAmount,
64
+ uint256 v4SwapInput
65
+ );
66
+
67
+ // ============ ERRORS ============
68
+
69
+ error OnlyPoolManager();
70
+ error InsufficientInputCurrency(uint256 inputAmount, uint256 availableAmount);
71
+ error V3RouteCannotStartWithInputCurrency();
72
+ error V3RouteDoesNotConnectToV4RouteStart();
73
+ error InsufficientOutputAmount();
74
+
75
+ // ============ CONSTRUCTOR ============
76
+
77
+ constructor(IZoraFactory _factory, address _swapRouter, address _poolManager) BaseCoinDeployHook(_factory) {
78
+ swapRouter = ISwapRouter(_swapRouter);
79
+ poolManager = IPoolManager(_poolManager);
80
+ }
81
+
82
+ // ============ MAIN HOOK FUNCTION ============
83
+
84
+ /// @notice Hook that buys supply for a coin using V3->V4 two-step swap routing
85
+ /// @dev Returns abi encoded (uint256 amountCurrency, uint256 coinsPurchased)
86
+ function _afterCoinDeploy(address, ICoin coin, bytes calldata hookData) internal override returns (bytes memory) {
87
+ // STEP 1: Decode parameters
88
+ InitialSupplyParams memory params = abi.decode(hookData, (InitialSupplyParams));
89
+
90
+ PoolKey[] memory v4Route = _buildV4RouteToCoin(coin, params.v4Route);
91
+
92
+ // STEP 2: Validate routes
93
+ _validateRoutes(params, v4Route);
94
+
95
+ _validateAndTransferInputCurrency(params);
96
+
97
+ // STEP 3: Execute V3 swap (inputCurrency -> backing currency)
98
+ (uint256 currencyAmount, address currencyReceived) = _executeV3Swap(params);
99
+
100
+ // STEP 4: Execute V4 swaps if needed, then buy coin
101
+ uint256 coinAmount = _executeV4Swap(v4Route, currencyAmount, currencyReceived, params.buyRecipient);
102
+
103
+ // Validate minimum amount of coins received from final swap
104
+ require(coinAmount >= params.minAmountOut, InsufficientOutputAmount());
105
+
106
+ emit BuyInitialSupply({
107
+ recipient: params.buyRecipient,
108
+ coin: address(coin),
109
+ v3Route: params.v3Route,
110
+ v4Route: v4Route,
111
+ inputCurrency: params.inputCurrency,
112
+ inputAmount: params.inputAmount,
113
+ v4SwapInput: currencyAmount,
114
+ coinsPurchased: coinAmount
115
+ });
116
+
117
+ // STEP 5: Return results
118
+ return abi.encode(currencyAmount, coinAmount);
119
+ }
120
+
121
+ // ============ VALIDATION ============
122
+
123
+ function _validateRoutes(InitialSupplyParams memory params, PoolKey[] memory v4Route) internal pure {
124
+ // Determine what currency should be the input to the V4 route
125
+ address v4InputCurrency;
126
+ if (params.v3Route.length == 0) {
127
+ // No V3 swap - input currency should directly match V4 route start
128
+ v4InputCurrency = params.inputCurrency;
129
+ } else {
130
+ // V3 swap exists - V3 output should match V4 route start
131
+ v4InputCurrency = _getV3RouteOutputCurrency(params.v3Route);
132
+ }
133
+
134
+ PoolKey memory firstPool = v4Route[0];
135
+
136
+ require(
137
+ v4InputCurrency == Currency.unwrap(firstPool.currency0) || v4InputCurrency == Currency.unwrap(firstPool.currency1),
138
+ V3RouteDoesNotConnectToV4RouteStart()
139
+ );
140
+ }
141
+
142
+ function _validateAndTransferInputCurrency(InitialSupplyParams memory params) internal {
143
+ if (params.inputCurrency == address(0)) {
144
+ uint256 providedAmount = msg.value;
145
+
146
+ require(providedAmount == params.inputAmount, InsufficientInputCurrency(params.inputAmount, providedAmount));
147
+ } else {
148
+ uint256 providedAmount = IERC20(params.inputCurrency).allowance(params.buyRecipient, address(this));
149
+
150
+ // must be enough allowance to transfer
151
+ require(providedAmount >= params.inputAmount, InsufficientInputCurrency(params.inputAmount, providedAmount));
152
+
153
+ // transfer from the buy recipient to this contract
154
+ IERC20(params.inputCurrency).safeTransferFrom(params.buyRecipient, address(this), params.inputAmount);
155
+ }
156
+ }
157
+
158
+ function _buildV4RouteToCoin(ICoin coin, PoolKey[] memory v4Route) internal view returns (PoolKey[] memory fullRoute) {
159
+ fullRoute = new PoolKey[](v4Route.length + 1);
160
+
161
+ for (uint256 i = 0; i < v4Route.length; i++) {
162
+ fullRoute[i] = v4Route[i];
163
+ }
164
+
165
+ fullRoute[v4Route.length] = coin.getPoolKey();
166
+ }
167
+
168
+ // ============ V3 SWAP LOGIC ============
169
+
170
+ function _executeV3Swap(InitialSupplyParams memory params) internal returns (uint256 amountCurrency, address currencyReceived) {
171
+ if (params.v3Route.length == 0) {
172
+ // No V3 swap needed - return inputAmount directly
173
+ return (params.inputAmount, params.inputCurrency);
174
+ }
175
+
176
+ // for v3 swap section, we dont support currently having an input currency other than eth
177
+ if (params.inputCurrency != address(0)) {
178
+ revert V3RouteCannotStartWithInputCurrency();
179
+ }
180
+
181
+ // Build swap router call for exactInput
182
+ ISwapRouter.ExactInputParams memory swapParams = ISwapRouter.ExactInputParams({
183
+ path: params.v3Route,
184
+ recipient: address(this),
185
+ amountIn: params.inputAmount,
186
+ amountOutMinimum: 0 // For testing - in production should have slippage protection
187
+ });
188
+
189
+ amountCurrency = swapRouter.exactInput{value: params.inputAmount}(swapParams);
190
+
191
+ currencyReceived = _getV3RouteOutputCurrency(params.v3Route);
192
+ }
193
+
194
+ function _executeV4Swap(PoolKey[] memory v4Route, uint256 amountIn, address currencyIn, address buyRecipient) internal returns (uint256 amountCoin) {
195
+ Currency startingCurrency = Currency.wrap(currencyIn);
196
+ bytes memory data = abi.encode(v4Route, amountIn, startingCurrency, buyRecipient);
197
+ bytes memory result = poolManager.unlock(data);
198
+ amountCoin = abi.decode(result, (uint256));
199
+ }
200
+
201
+ /// @notice Callback for V4 swaps through route or coin purchase
202
+ function unlockCallback(bytes calldata data) external returns (bytes memory) {
203
+ require(msg.sender == address(poolManager), OnlyPoolManager());
204
+
205
+ (PoolKey[] memory v4Route, uint256 amountIn, Currency startingCurrency, address buyRecipient) = abi.decode(
206
+ data,
207
+ (PoolKey[], uint256, Currency, address)
208
+ );
209
+
210
+ Currency lastReceivedCurrency = startingCurrency;
211
+ uint128 lastReceivedAmount = uint128(amountIn);
212
+ // Execute swaps through the route
213
+
214
+ uint128 outputAmount = 0;
215
+ for (uint256 i = 0; i < v4Route.length; i++) {
216
+ PoolKey memory poolKey = v4Route[i];
217
+
218
+ // Determine swap direction based on current currency
219
+ bool zeroForOne = lastReceivedCurrency == poolKey.currency0;
220
+
221
+ BalanceDelta delta = poolManager.swap(
222
+ poolKey,
223
+ SwapParams(zeroForOne, -(int128(lastReceivedAmount)), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
224
+ ""
225
+ );
226
+
227
+ // Extract output amount from delta
228
+ outputAmount = zeroForOne ? uint128(delta.amount1()) : uint128(delta.amount0());
229
+
230
+ // Update currentAmount for next iteration
231
+ lastReceivedAmount = uint128(outputAmount);
232
+
233
+ // Update current currency for next swap
234
+ lastReceivedCurrency = zeroForOne ? poolKey.currency1 : poolKey.currency0;
235
+ }
236
+
237
+ // Settle all currency deltas and get final amount
238
+ _settleDeltas(startingCurrency, lastReceivedCurrency, buyRecipient, amountIn, outputAmount);
239
+
240
+ return abi.encode(lastReceivedAmount);
241
+ }
242
+
243
+ /// @notice Helper to decode V4 route data (external for try/catch)
244
+ function decodeV4RouteData(bytes calldata data) external pure returns (PoolKey[] memory v4Route, uint256 startAmount) {
245
+ return abi.decode(data, (PoolKey[], uint256));
246
+ }
247
+
248
+ function encodeBuySupplyWithV4SwapHookData(InitialSupplyParams memory params) external pure returns (bytes memory) {
249
+ return abi.encode(params);
250
+ }
251
+
252
+ function _settleDeltas(Currency inputCurrency, Currency outputCurrency, address to, uint256 inputAmount, uint128 outputAmount) private {
253
+ // pay the input amount
254
+ if (inputCurrency.isAddressZero()) {
255
+ // For ETH, settle with msg.value
256
+ poolManager.settle{value: inputAmount}();
257
+ } else {
258
+ // For ERC20, sync and transfer
259
+ poolManager.sync(inputCurrency);
260
+ inputCurrency.transfer(address(poolManager), inputAmount);
261
+ poolManager.settle();
262
+ }
263
+
264
+ // transfer the output amount to the recipient
265
+ poolManager.take(outputCurrency, to, outputAmount);
266
+ }
267
+
268
+ // ============ UTILITIES ============
269
+
270
+ function _getCoinBackingCurrency(ICoin coin) internal view returns (Currency) {
271
+ PoolKey memory poolKey = coin.getPoolKey();
272
+
273
+ if (Currency.unwrap(poolKey.currency0) == address(coin)) {
274
+ return poolKey.currency1;
275
+ }
276
+ return poolKey.currency0;
277
+ }
278
+
279
+ function _getV3RouteOutputCurrency(bytes memory path) internal pure returns (address tokenOut) {
280
+ if (path.length == 0) {
281
+ // if no path, then output currency is eth
282
+ return address(0);
283
+ }
284
+
285
+ // For a path with multiple pools, we need to traverse to the end
286
+ // Path format: tokenA + fee + tokenB + fee + tokenC...
287
+ // We want the final token (tokenC in this example)
288
+
289
+ // Follow Uniswap's pattern: traverse the path to find the final token
290
+ bytes memory currentPath = path;
291
+
292
+ // Keep skipping tokens until we reach the final pool
293
+ while (currentPath.hasMultiplePools()) {
294
+ currentPath = currentPath.skipToken();
295
+ }
296
+
297
+ // The final segment contains the last pool, decode to get the output token
298
+ (, tokenOut, ) = currentPath.decodeFirstPool();
299
+ }
300
+
301
+ function _getV3RouteInputCurrency(bytes memory path) internal pure returns (address tokenIn) {
302
+ if (path.length == 0) {
303
+ // if no path, then input currency is eth
304
+ return address(0);
305
+ }
306
+
307
+ // Use Path library to get the input token (first token in the path)
308
+ (tokenIn, , ) = path.decodeFirstPool();
309
+ }
310
+ }
@@ -0,0 +1,18 @@
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
+ /// @title ITrustedMsgSenderProviderLookup
11
+ /// @notice Interface for contracts that can determine if an address is a trusted message sender
12
+ /// @dev This interface allows the hook to delegate the trusted sender check to an external contract
13
+ interface ITrustedMsgSenderProviderLookup {
14
+ /// @notice Checks if an address is a trusted message sender provider
15
+ /// @param sender The address to check
16
+ /// @return true if the sender is trusted, false otherwise
17
+ function isTrustedMsgSenderProvider(address sender) external view returns (bool);
18
+ }
@@ -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);
@@ -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
 
@@ -86,10 +87,10 @@ library HooksDeployment {
86
87
  address deployer,
87
88
  address poolManager,
88
89
  address coinVersionLookup,
89
- address[] memory trustedMessageSenders,
90
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
90
91
  address upgradeGate
91
92
  ) internal returns (address hookAddress, bytes32 salt) {
92
- bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
93
+ bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
93
94
  (salt, ) = mineAndCacheSalt(deployer, hookCreationCode);
94
95
  hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, salt, hookCreationCode);
95
96
  }
@@ -131,19 +132,19 @@ library HooksDeployment {
131
132
  function hookConstructorArgs(
132
133
  address poolManager,
133
134
  address coinVersionLookup,
134
- address[] memory trustedMessageSenders,
135
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
135
136
  address upgradeGate
136
137
  ) internal pure returns (bytes memory) {
137
- return abi.encode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
138
+ return abi.encode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
138
139
  }
139
140
 
140
141
  function makeHookCreationCode(
141
142
  address poolManager,
142
143
  address coinVersionLookup,
143
- address[] memory trustedMessageSenders,
144
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
144
145
  address upgradeGate
145
146
  ) internal pure returns (bytes memory) {
146
- return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate));
147
+ return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate));
147
148
  }
148
149
 
149
150
  /// @notice Deploys or returns existing ContentCoinHook using deterministic deployment. Ensures that if a hooks is already
@@ -151,11 +152,11 @@ library HooksDeployment {
151
152
  function deployZoraV4CoinHook(
152
153
  address poolManager,
153
154
  address coinVersionLookup,
154
- address[] memory trustedMessageSenders,
155
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
155
156
  address upgradeGate,
156
157
  bytes32 salt
157
158
  ) internal returns (IHooks hook) {
158
- bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
159
+ bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
159
160
  return deployHookWithSalt(creationCode, salt);
160
161
  }
161
162