@zoralabs/coins 2.5.0 → 2.6.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 (48) hide show
  1. package/.turbo/turbo-build$colon$js.log +136 -130
  2. package/CHANGELOG.md +28 -17
  3. package/abis/BaseCoin.json +5 -0
  4. package/abis/ContentCoin.json +5 -0
  5. package/abis/ICoin.json +5 -0
  6. package/abis/ICoinV3.json +5 -0
  7. package/abis/ITrendCoin.json +140 -0
  8. package/abis/ITrendCoinErrors.json +33 -0
  9. package/abis/IUniversalRouter.json +61 -0
  10. package/abis/IZoraFactory.json +237 -0
  11. package/abis/TrendCoin.json +2053 -0
  12. package/abis/ZoraFactoryImpl.json +242 -0
  13. package/dist/index.cjs +955 -138
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.js +953 -138
  16. package/dist/index.js.map +1 -1
  17. package/dist/wagmiGenerated.d.ts +1388 -149
  18. package/dist/wagmiGenerated.d.ts.map +1 -1
  19. package/foundry.toml +1 -0
  20. package/package/wagmiGenerated.ts +962 -139
  21. package/package.json +2 -2
  22. package/src/BaseCoin.sol +12 -12
  23. package/src/ContentCoin.sol +20 -1
  24. package/src/CreatorCoin.sol +3 -0
  25. package/src/TrendCoin.sol +117 -0
  26. package/src/ZoraFactoryImpl.sol +142 -1
  27. package/src/hooks/ZoraV4CoinHook.sol +17 -7
  28. package/src/interfaces/ICoin.sol +5 -1
  29. package/src/interfaces/ICreatorCoin.sol +0 -3
  30. package/src/interfaces/IPoolManager.sol +13 -0
  31. package/src/interfaces/ITrendCoin.sol +26 -0
  32. package/src/interfaces/ITrendCoinErrors.sol +24 -0
  33. package/src/interfaces/IZoraFactory.sol +60 -1
  34. package/src/libs/CoinConstants.sol +13 -1
  35. package/src/libs/CoinRewardsV4.sol +82 -21
  36. package/src/libs/TickerUtils.sol +66 -0
  37. package/src/libs/UniV4SwapToCurrency.sol +2 -1
  38. package/src/version/ContractVersionBase.sol +1 -1
  39. package/test/CoinRewardsV4.t.sol +48 -0
  40. package/test/CreatorCoin.t.sol +2 -1
  41. package/test/Factory.t.sol +31 -5
  42. package/test/LaunchFee.t.sol +0 -2
  43. package/test/LiquidityMigration.t.sol +0 -2
  44. package/test/TrendCoin.t.sol +1128 -0
  45. package/test/Upgrades.t.sol +16 -3
  46. package/test/utils/FeeEstimatorHook.sol +36 -10
  47. package/test/utils/V4TestSetup.sol +36 -4
  48. package/wagmi.config.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/coins",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -34,7 +34,7 @@
34
34
  "tsup": "^7.2.0",
35
35
  "tsx": "^3.13.0",
36
36
  "typescript": "^5.2.2",
37
- "viem": "^2.21.18",
37
+ "viem": "2.53.1",
38
38
  "@zoralabs/shared-contracts": "^0.0.5",
39
39
  "@zoralabs/shared-scripts": "^0.0.0",
40
40
  "@zoralabs/tsconfig": "^0.0.1"
package/src/BaseCoin.sol CHANGED
@@ -76,8 +76,8 @@ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC2
76
76
  string public tokenURI;
77
77
  /// @notice The address of the coin creator
78
78
  address public payoutRecipient;
79
- /// @notice The address of the platform referrer
80
- address public platformReferrer;
79
+ /// @notice The address of the platform referrer (internal storage, use platformReferrer() getter)
80
+ address internal _platformReferrer;
81
81
  /// @notice The address of the currency
82
82
  address public currency;
83
83
 
@@ -164,11 +164,6 @@ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC2
164
164
  string memory symbol_,
165
165
  address platformReferrer_
166
166
  ) internal {
167
- // Validate the creation parameters
168
- if (payoutRecipient_ == address(0)) {
169
- revert AddressZero();
170
- }
171
-
172
167
  _setNameAndSymbol(name_, symbol_);
173
168
 
174
169
  // Set base contract state, leave name and symbol empty to save space.
@@ -179,12 +174,12 @@ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC2
179
174
 
180
175
  __MultiOwnable_init(owners_);
181
176
 
182
- // Set mutable state
183
- _setPayoutRecipient(payoutRecipient_);
177
+ // Set mutable state (no validation here - subclasses validate if needed)
178
+ payoutRecipient = payoutRecipient_;
184
179
  _setContractURI(tokenURI_);
185
180
 
186
181
  // Store the referrer or use the protocol reward recipient if not set
187
- platformReferrer = platformReferrer_ == address(0) ? protocolRewardRecipient : platformReferrer_;
182
+ _platformReferrer = platformReferrer_ == address(0) ? protocolRewardRecipient : platformReferrer_;
188
183
 
189
184
  // Distribute the initial supply
190
185
  _handleInitialDistribution();
@@ -216,7 +211,7 @@ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC2
216
211
 
217
212
  /// @notice Set the contract URI
218
213
  /// @param newURI The new URI
219
- function setContractURI(string memory newURI) external onlyOwner {
214
+ function setContractURI(string memory newURI) external virtual onlyOwner {
220
215
  _setContractURI(newURI);
221
216
  }
222
217
 
@@ -232,7 +227,7 @@ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC2
232
227
  return _name;
233
228
  }
234
229
 
235
- function setNameAndSymbol(string memory newName, string memory newSymbol) external onlyOwner {
230
+ function setNameAndSymbol(string memory newName, string memory newSymbol) external virtual onlyOwner {
236
231
  _setNameAndSymbol(newName, newSymbol);
237
232
  }
238
233
 
@@ -334,6 +329,11 @@ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC2
334
329
  return poolKey.hooks;
335
330
  }
336
331
 
332
+ /// @inheritdoc IHasRewardsRecipients
333
+ function platformReferrer() external view virtual returns (address) {
334
+ return _platformReferrer;
335
+ }
336
+
337
337
  /// @notice Migrate liquidity from current hook to a new hook implementation
338
338
  /// @param newHook Address of the new hook implementation
339
339
  /// @param additionalData Additional data to pass to the new hook during initialization
@@ -10,7 +10,7 @@ pragma solidity ^0.8.23;
10
10
  import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
11
  import {BaseCoin} from "./BaseCoin.sol";
12
12
  import {CoinConstants} from "./libs/CoinConstants.sol";
13
- import {IHasCoinType} from "./interfaces/ICoin.sol";
13
+ import {IHasCoinType, ICoin, PoolKey, PoolConfiguration} from "./interfaces/ICoin.sol";
14
14
 
15
15
  /**
16
16
  * @title ContentCoin
@@ -31,6 +31,25 @@ contract ContentCoin is BaseCoin {
31
31
  address airlock_
32
32
  ) BaseCoin(protocolRewardRecipient_, protocolRewards_, poolManager_, airlock_) {}
33
33
 
34
+ /// @inheritdoc ICoin
35
+ function initialize(
36
+ address payoutRecipient_,
37
+ address[] memory owners_,
38
+ string memory tokenURI_,
39
+ string memory name_,
40
+ string memory symbol_,
41
+ address platformReferrer_,
42
+ address currency_,
43
+ PoolKey memory poolKey_,
44
+ uint160 sqrtPriceX96,
45
+ PoolConfiguration memory poolConfiguration_
46
+ ) public override {
47
+ if (payoutRecipient_ == address(0)) {
48
+ revert AddressZero();
49
+ }
50
+ super.initialize(payoutRecipient_, owners_, tokenURI_, name_, symbol_, platformReferrer_, currency_, poolKey_, sqrtPriceX96, poolConfiguration_);
51
+ }
52
+
34
53
  /// @dev The initial mint and distribution of the coin supply.
35
54
  /// Implements content coin specific distribution: 990M to liquidity pool, 10M to creator.
36
55
  function _handleInitialDistribution() internal virtual override {
@@ -46,6 +46,9 @@ contract CreatorCoin is ICreatorCoin, BaseCoin {
46
46
  uint160 sqrtPriceX96,
47
47
  PoolConfiguration memory poolConfiguration_
48
48
  ) public override(BaseCoin, ICoin) {
49
+ if (payoutRecipient_ == address(0)) {
50
+ revert AddressZero();
51
+ }
49
52
  require(currency_ == CoinConstants.CREATOR_COIN_CURRENCY, InvalidCurrency());
50
53
 
51
54
  super.initialize({
@@ -0,0 +1,117 @@
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.28;
9
+
10
+ import {CoinConstants} from "./libs/CoinConstants.sol";
11
+ import {TickerUtils} from "./libs/TickerUtils.sol";
12
+ import {IHooks, PoolConfiguration, PoolKey, ICoin} from "./interfaces/ICoin.sol";
13
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
14
+ import {BaseCoin} from "./BaseCoin.sol";
15
+ import {IHasCoinType} from "./interfaces/ICoin.sol";
16
+ import {ITrendCoin} from "./interfaces/ITrendCoin.sol";
17
+
18
+ /// @title TrendCoin
19
+ /// @notice Trend coin implementation with no creator payout recipient
20
+ /// @dev TrendCoins have 100% of supply in the liquidity pool with no creator allocation.
21
+ /// Unlike ContentCoin and CreatorCoin, TrendCoins do not have a payoutRecipient or platformReferrer.
22
+ contract TrendCoin is BaseCoin, ITrendCoin {
23
+ /// @notice Base URI for trend coin metadata
24
+ string internal constant TREND_COIN_BASE_URI = "https://trends.theme.wtf/trend/";
25
+
26
+ address internal immutable metadataManager;
27
+
28
+ constructor(
29
+ address protocolRewardRecipient_,
30
+ address protocolRewards_,
31
+ IPoolManager poolManager_,
32
+ address airlock_,
33
+ address metadataManager_
34
+ ) BaseCoin(protocolRewardRecipient_, protocolRewards_, poolManager_, airlock_) initializer {
35
+ // Zero address is valid when metadata is intended to be non-updatable
36
+ metadataManager = metadataManager_;
37
+ }
38
+
39
+ function totalSupplyForPositions() external pure override returns (uint256) {
40
+ return CoinConstants.TOTAL_SUPPLY;
41
+ }
42
+
43
+ function coinType() external pure override returns (IHasCoinType.CoinType) {
44
+ return IHasCoinType.CoinType.Trend;
45
+ }
46
+
47
+ function setContractURI(string memory newURI) external override {
48
+ require(msg.sender == metadataManager, OnlyMetadataManager());
49
+ _setContractURI(newURI);
50
+ }
51
+
52
+ function setNameAndSymbol(string memory newName, string memory newSymbol) external override {
53
+ require(msg.sender == metadataManager, OnlyMetadataManager());
54
+ _setNameAndSymbol(newName, newSymbol);
55
+ }
56
+
57
+ /// @inheritdoc ITrendCoin
58
+ function initializeTrendCoin(
59
+ address[] memory owners_,
60
+ string memory symbol_,
61
+ PoolKey memory poolKey_,
62
+ uint160 sqrtPriceX96,
63
+ PoolConfiguration memory poolConfiguration_
64
+ ) external {
65
+ // Validate ticker characters
66
+ TickerUtils.requireValidateTickerCharacters(symbol_);
67
+
68
+ // Generate URI from base URI + encoded symbol
69
+ string memory uri = string.concat(TREND_COIN_BASE_URI, symbol_);
70
+
71
+ // Call parent initialize with derived values
72
+ // name = symbol for trend coins
73
+ // The initializer modifier is on BaseCoin.initialize, not here
74
+ BaseCoin.initialize({
75
+ payoutRecipient_: address(0),
76
+ owners_: owners_,
77
+ tokenURI_: uri,
78
+ name_: symbol_,
79
+ symbol_: symbol_,
80
+ platformReferrer_: address(0),
81
+ currency_: CoinConstants.CREATOR_COIN_CURRENCY,
82
+ poolKey_: poolKey_,
83
+ sqrtPriceX96: sqrtPriceX96,
84
+ poolConfiguration_: poolConfiguration_
85
+ });
86
+ }
87
+
88
+ /// @dev Legacy initialize function for ICoin compatibility
89
+ /// @notice Prefer using initializeTrendCoin for new deployments
90
+ function initialize(
91
+ address /* payoutRecipient_ */,
92
+ address[] memory /* owners_ */,
93
+ string memory /* tokenURI_ */,
94
+ string memory /* name_ */,
95
+ string memory /* symbol_ */,
96
+ address /* platformReferrer_ */,
97
+ address /* currency_ */,
98
+ PoolKey memory /* poolKey_ */,
99
+ uint160 /* sqrtPriceX96 */,
100
+ PoolConfiguration memory /* poolConfiguration_ */
101
+ ) public pure override {
102
+ revert UseSpecificTrendCoinInitialize();
103
+ }
104
+
105
+ /// @dev The initial mint and distribution of the coin supply.
106
+ /// TrendCoins have 100% of supply in the liquidity pool.
107
+ function _handleInitialDistribution() internal override {
108
+ _mint(address(this), CoinConstants.TOTAL_SUPPLY);
109
+ _transfer(address(this), address(poolKey.hooks), CoinConstants.TOTAL_SUPPLY);
110
+ }
111
+
112
+ /// @notice TrendCoins have no platform referrer - always returns address(0)
113
+ /// @dev Overrides BaseCoin's platformReferrer which defaults to protocolRewardRecipient when not set
114
+ function platformReferrer() external pure override returns (address) {
115
+ return address(0);
116
+ }
117
+ }
@@ -16,6 +16,8 @@ import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.s
16
16
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
17
17
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
18
18
  import {CoinConfigurationVersions} from "./libs/CoinConfigurationVersions.sol";
19
+ import {CoinConstants} from "./libs/CoinConstants.sol";
20
+ import {TickerUtils} from "./libs/TickerUtils.sol";
19
21
  import {ISwapRouter} from "./interfaces/ISwapRouter.sol";
20
22
  import {IWETH} from "./interfaces/IWETH.sol";
21
23
  import {IZoraFactory} from "./interfaces/IZoraFactory.sol";
@@ -34,6 +36,7 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
34
36
  import {CoinSetup} from "./libs/CoinSetup.sol";
35
37
  import {CoinDopplerMultiCurve} from "./libs/CoinDopplerMultiCurve.sol";
36
38
  import {ICreatorCoin} from "./interfaces/ICreatorCoin.sol";
39
+ import {ITrendCoin} from "./interfaces/ITrendCoin.sol";
37
40
  import {DeployedCoinVersionLookup} from "./utils/DeployedCoinVersionLookup.sol";
38
41
  import {IZoraHookRegistry} from "./interfaces/IZoraHookRegistry.sol";
39
42
 
@@ -52,16 +55,55 @@ contract ZoraFactoryImpl is
52
55
  address public immutable coinV4Impl;
53
56
  /// @notice The creator coin contract implementation address
54
57
  address public immutable creatorCoinImpl;
58
+ /// @notice The trend coin contract implementation address
59
+ address public immutable trendCoinImpl;
55
60
  /// @notice The uniswap v4 coin hook address
56
61
  address public immutable hook;
57
62
  /// @notice The zora hook registry address
58
63
  address public immutable zoraHookRegistry;
59
64
 
60
- constructor(address coinV4Impl_, address creatorCoinImpl_, address hook_, address zoraHookRegistry_) {
65
+ /// @custom:storage-location erc7201:zora.coins.trendcointickers.storage
66
+ struct TrendCoinTickerStorage {
67
+ mapping(bytes32 => bool) usedTickerHashes;
68
+ }
69
+
70
+ // keccak256(abi.encode(uint256(keccak256("zora.coins.trendcointickers.storage")) - 1)) & ~bytes32(uint256(0xff))
71
+ bytes32 private constant TREND_COIN_TICKER_STORAGE_LOCATION = 0x57bdedf0ddfee9320a51cef29a2847cd7d7c32252cadecb7958561cc2d69ff00;
72
+
73
+ /// @custom:storage-location erc7201:zora.coins.trendcoinconfig.storage
74
+ struct TrendCoinConfigStorage {
75
+ bytes poolConfig;
76
+ }
77
+
78
+ // keccak256(abi.encode(uint256(keccak256("zora.coins.trendcoinconfig.storage")) - 1)) & ~bytes32(uint256(0xff))
79
+ bytes32 private constant TREND_COIN_CONFIG_STORAGE_LOCATION = 0xd1aa47a8d1a3f9b64aa4095f5f6c436e9b3a1eb90a61ab15f3a94d28bf1c0200;
80
+
81
+ /**
82
+ * @dev Returns the storage slot struct for trend coin ticker tracking
83
+ * @return $ Storage struct containing the usedTickerHashes mapping
84
+ */
85
+ function _getTrendCoinTickerStorage() private pure returns (TrendCoinTickerStorage storage $) {
86
+ assembly {
87
+ $.slot := TREND_COIN_TICKER_STORAGE_LOCATION
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @dev Returns the storage slot struct for trend coin pool configuration
93
+ * @return $ Storage struct containing the poolConfig bytes
94
+ */
95
+ function _getTrendCoinConfigStorage() private pure returns (TrendCoinConfigStorage storage $) {
96
+ assembly {
97
+ $.slot := TREND_COIN_CONFIG_STORAGE_LOCATION
98
+ }
99
+ }
100
+
101
+ constructor(address coinV4Impl_, address creatorCoinImpl_, address trendCoinImpl_, address hook_, address zoraHookRegistry_) {
61
102
  _disableInitializers();
62
103
 
63
104
  coinV4Impl = coinV4Impl_;
64
105
  creatorCoinImpl = creatorCoinImpl_;
106
+ trendCoinImpl = trendCoinImpl_;
65
107
  hook = hook_;
66
108
  zoraHookRegistry = zoraHookRegistry_;
67
109
  }
@@ -128,6 +170,73 @@ contract ZoraFactoryImpl is
128
170
  return Clones.predictDeterministicAddress(getCoinImpl(CoinConfigurationVersions.getVersion(poolConfig)), salt, address(this));
129
171
  }
130
172
 
173
+ /// @inheritdoc IZoraFactory
174
+ function deployTrendCoin(
175
+ string calldata symbol,
176
+ address postDeployHook,
177
+ bytes calldata postDeployHookData
178
+ ) external payable nonReentrant returns (address coin, bytes memory postDeployHookDataOut) {
179
+ bytes32 tickerHashValue = TickerUtils.tickerHash(symbol);
180
+
181
+ // Check ticker uniqueness
182
+ TrendCoinTickerStorage storage $ = _getTrendCoinTickerStorage();
183
+ if ($.usedTickerHashes[tickerHashValue]) {
184
+ revert TickerAlreadyUsed(symbol);
185
+ }
186
+ $.usedTickerHashes[tickerHashValue] = true;
187
+
188
+ // Use ticker hash as salt for deterministic address
189
+ bytes32 salt = tickerHashValue;
190
+
191
+ coin = _createAndInitializeTrendCoin(symbol, salt);
192
+ postDeployHookDataOut = _executePostDeployHook(coin, postDeployHook, postDeployHookData);
193
+ }
194
+
195
+ /// @inheritdoc IZoraFactory
196
+ function trendCoinAddress(string calldata symbol) external view returns (address) {
197
+ bytes32 tickerHashValue = TickerUtils.tickerHash(symbol);
198
+ return Clones.predictDeterministicAddress(trendCoinImpl, tickerHashValue, address(this));
199
+ }
200
+
201
+ /// @dev Internal function to create and initialize a trend coin
202
+ /// @param symbol The ticker symbol (validation happens in TrendCoin.initializeTrendCoin)
203
+ /// @param coinSalt The salt for deterministic address generation
204
+ function _createAndInitializeTrendCoin(string memory symbol, bytes32 coinSalt) internal returns (address) {
205
+ // Clone the TrendCoin implementation
206
+ address coin = Clones.cloneDeterministic(trendCoinImpl, coinSalt);
207
+
208
+ // Get pool configuration from storage
209
+ bytes memory poolConfig = _getTrendCoinConfigStorage().poolConfig;
210
+ if (poolConfig.length == 0) {
211
+ revert TrendCoinPoolConfigNotSet();
212
+ }
213
+
214
+ uint8 version = CoinConfigurationVersions.getVersion(poolConfig);
215
+ _setVersionForDeployedCoin(coin, version);
216
+
217
+ require(version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, InvalidConfig());
218
+
219
+ // Setup owners - factory owner is the owner of trend coins
220
+ address[] memory owners = new address[](1);
221
+ owners[0] = owner();
222
+
223
+ // Generate pool key and configuration
224
+ uint160 sqrtPriceX96;
225
+ bool isCoinToken0;
226
+ PoolConfiguration memory poolConfiguration;
227
+ address currency;
228
+ (, currency, sqrtPriceX96, isCoinToken0, poolConfiguration) = CoinSetup.generatePoolConfig(coin, poolConfig);
229
+ PoolKey memory poolKey = CoinSetup.buildPoolKey(coin, currency, isCoinToken0, IHooks(hook));
230
+
231
+ // Initialize using TrendCoin's simplified initialize
232
+ // Validation and URI generation happen inside TrendCoin
233
+ ITrendCoin(coin).initializeTrendCoin(owners, symbol, poolKey, sqrtPriceX96, poolConfiguration);
234
+
235
+ emit TrendCoinCreated(msg.sender, symbol, coin, poolKey, CoinCommon.hashPoolKey(poolKey), poolConfig, IVersionedContract(coin).contractVersion());
236
+
237
+ return coin;
238
+ }
239
+
131
240
  function _executePostDeployHook(address coin, address deployHook, bytes calldata hookData) internal returns (bytes memory hookDataOut) {
132
241
  if (deployHook != address(0)) {
133
242
  if (!IERC165(deployHook).supportsInterface(type(IHasAfterCoinDeploy).interfaceId)) {
@@ -442,4 +551,36 @@ contract ZoraFactoryImpl is
442
551
  function contentCoinHook() external view returns (address) {
443
552
  return hook;
444
553
  }
554
+
555
+ /// @inheritdoc IZoraFactory
556
+ function setTrendCoinPoolConfig(
557
+ address currency,
558
+ int24[] memory tickLower,
559
+ int24[] memory tickUpper,
560
+ uint16[] memory numDiscoveryPositions,
561
+ uint256[] memory maxDiscoverySupplyShare
562
+ ) external onlyOwner {
563
+ // Validate arrays have matching lengths
564
+ require(
565
+ tickLower.length == tickUpper.length && tickLower.length == numDiscoveryPositions.length && tickLower.length == maxDiscoverySupplyShare.length,
566
+ InvalidConfig()
567
+ );
568
+ require(tickLower.length > 0, InvalidConfig());
569
+ require(currency == CoinConstants.CREATOR_COIN_CURRENCY, InvalidConfig());
570
+
571
+ bytes memory poolConfig = CoinConfigurationVersions.encodeDopplerMultiCurveUniV4(
572
+ currency,
573
+ tickLower,
574
+ tickUpper,
575
+ numDiscoveryPositions,
576
+ maxDiscoverySupplyShare
577
+ );
578
+ _getTrendCoinConfigStorage().poolConfig = poolConfig;
579
+ emit TrendCoinPoolConfigUpdated(poolConfig);
580
+ }
581
+
582
+ /// @inheritdoc IZoraFactory
583
+ function trendCoinPoolConfig() external view returns (bytes memory) {
584
+ return _getTrendCoinConfigStorage().poolConfig;
585
+ }
445
586
  }
@@ -43,6 +43,7 @@ import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
43
43
  import {TickMath} from "../utils/uniswap/TickMath.sol";
44
44
  import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
45
45
  import {ISupportsLimitOrderFill} from "../interfaces/ISupportsLimitOrderFill.sol";
46
+ import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
46
47
 
47
48
  /// @title ZoraV4CoinHook
48
49
  /// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
@@ -354,8 +355,13 @@ contract ZoraV4CoinHook is
354
355
  // Calculate elapsed time since creation
355
356
  uint256 elapsed = block.timestamp - creationTimestamp;
356
357
 
357
- // If launch fee duration has passed, use normal LP fee
358
+ // If launch fee duration has passed, use normal LP fee (0.01% for trend coins, 1% for others)
358
359
  if (elapsed >= CoinConstants.LAUNCH_FEE_DURATION) {
360
+ try IHasCoinType(coin).coinType() returns (IHasCoinType.CoinType ct) {
361
+ if (ct == IHasCoinType.CoinType.Trend) {
362
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.TREND_LP_FEE_V4;
363
+ }
364
+ } catch {}
359
365
  return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
360
366
  }
361
367
 
@@ -404,18 +410,22 @@ contract ZoraV4CoinHook is
404
410
  // collect lp fees
405
411
  (int128 fees0, int128 fees1) = V4Liquidity.collectFees(poolManager, key, poolCoins[poolKeyHash].positions);
406
412
 
407
- (uint128 marketRewardsAmount0, uint128 marketRewardsAmount1) = CoinRewardsV4.mintLpReward(poolManager, key, fees0, fees1);
413
+ IHasCoinType.CoinType coinType = CoinRewardsV4.getCoinType(IHasRewardsRecipients(coin));
408
414
 
409
- // convert remaining fees to payout currency for market rewards
410
- (Currency payoutCurrency, uint128 payoutAmount) = CoinRewardsV4.convertToPayoutCurrency(
415
+ (uint128 marketRewardsAmount0, uint128 marketRewardsAmount1) = CoinRewardsV4.mintLpReward(poolManager, key, fees0, fees1, coinType);
416
+
417
+ // convert remaining fees to payout currency for market rewards, and distribute any partial swap remainders
418
+ address tradeReferrer = CoinRewardsV4.getTradeReferral(hookData);
419
+ CoinRewardsV4.swapFeesToPayoutAndDistribute(
411
420
  poolManager,
412
421
  marketRewardsAmount0,
413
422
  marketRewardsAmount1,
414
- payoutSwapPath
423
+ payoutSwapPath,
424
+ ICoin(coin),
425
+ tradeReferrer,
426
+ coinType
415
427
  );
416
428
 
417
- _distributeMarketRewards(payoutCurrency, payoutAmount, ICoin(coin), CoinRewardsV4.getTradeReferral(hookData));
418
-
419
429
  {
420
430
  (address swapper, bool isTrustedSwapSenderAddress) = _getOriginalMsgSender(sender);
421
431
  bool isCoinBuy = params.zeroForOne ? Currency.unwrap(key.currency1) == address(coin) : Currency.unwrap(key.currency0) == address(coin);
@@ -60,7 +60,8 @@ interface IHasCoinType {
60
60
  /// @notice The type of coin
61
61
  enum CoinType {
62
62
  Creator,
63
- Content
63
+ Content,
64
+ Trend
64
65
  }
65
66
 
66
67
  /// @notice Returns the type of coin
@@ -78,6 +79,9 @@ interface ICoin is IERC165, IERC7572, IDopplerErrors, IHasRewardsRecipients, IHa
78
79
  /// @notice Thrown when an invalid market type is specified
79
80
  error InvalidMarketType();
80
81
 
82
+ /// @notice Thrown when an invalid currency is used for coin operations
83
+ error InvalidCurrency();
84
+
81
85
  /// @notice Thrown when there are insufficient funds for an operation
82
86
  error InsufficientFunds();
83
87
 
@@ -12,9 +12,6 @@ interface ICreatorCoin is ICoin {
12
12
  /// @param vestingEndTime The timestamp when vesting ends
13
13
  event CreatorVestingClaimed(address indexed recipient, uint256 claimAmount, uint256 totalClaimed, uint256 vestingStartTime, uint256 vestingEndTime);
14
14
 
15
- /// @notice Thrown when an invalid currency is used for creator coin operations
16
- error InvalidCurrency();
17
-
18
15
  /// @notice Allows the creator payout recipient to claim vested tokens
19
16
  /// @return claimAmount The amount of tokens claimed
20
17
  function claimVesting() external returns (uint256);
@@ -0,0 +1,13 @@
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.28;
9
+
10
+ // these needed to be imported so that their abis can be included in the generated package output.
11
+
12
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
13
+ import {IUniversalRouter} from "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol";
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
5
+ import {PoolConfiguration} from "../types/PoolConfiguration.sol";
6
+ import {ITrendCoinErrors} from "./ITrendCoinErrors.sol";
7
+
8
+ interface ITrendCoin is ITrendCoinErrors {
9
+ /// @notice Thrown when an operation is attempted by an entity other than the metadata manager
10
+ error OnlyMetadataManager();
11
+
12
+ /// @notice Initializes a trend coin with simplified parameters
13
+ /// @dev Ticker validation, URI generation, and name derivation happen internally
14
+ /// @param owners_ Array of owner addresses for the coin
15
+ /// @param symbol_ The ticker symbol (also used as name)
16
+ /// @param poolKey_ The Uniswap V4 pool key
17
+ /// @param sqrtPriceX96 The initial sqrt price for the pool
18
+ /// @param poolConfiguration_ The pool configuration settings
19
+ function initializeTrendCoin(
20
+ address[] memory owners_,
21
+ string memory symbol_,
22
+ PoolKey memory poolKey_,
23
+ uint160 sqrtPriceX96,
24
+ PoolConfiguration memory poolConfiguration_
25
+ ) external;
26
+ }
@@ -0,0 +1,24 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ /// @title ITrendCoinErrors
5
+ /// @notice Shared error interface for TrendCoin-related errors
6
+ /// @dev Used by both TrendCoin and ZoraFactoryImpl for consistent error handling
7
+ interface ITrendCoinErrors {
8
+ /// @notice Thrown when ticker symbol contains invalid characters
9
+
10
+ /// @dev Allowed characters: 0-9, A-Z, a-z
11
+ error TickerInvalidCharacters();
12
+
13
+ /// @dev Ticker min length is 2
14
+ error TickerTooShort();
15
+ /// @dev Ticker max length is 32
16
+ error TickerTooLong();
17
+
18
+ /// @notice Thrown when attempting to deploy a trend coin with a ticker that already exists
19
+ /// @param symbol The ticker symbol that was already used
20
+ error TickerAlreadyUsed(string symbol);
21
+
22
+ /// @notice Thrown when attempting to use the legacy initialize function for a trend coin
23
+ error UseSpecificTrendCoinInitialize();
24
+ }