@zoralabs/coins 2.4.1 → 2.6.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 (55) hide show
  1. package/.abi-stability +923 -0
  2. package/.turbo/turbo-build$colon$js.log +143 -129
  3. package/CHANGELOG.md +38 -16
  4. package/abis/BaseCoin.json +23 -0
  5. package/abis/ContentCoin.json +23 -0
  6. package/abis/CreatorCoin.json +18 -0
  7. package/abis/ICoin.json +5 -0
  8. package/abis/ICoinV3.json +5 -0
  9. package/abis/IHasCreationInfo.json +20 -0
  10. package/abis/ITrendCoin.json +130 -0
  11. package/abis/ITrendCoinErrors.json +23 -0
  12. package/abis/IUniversalRouter.json +61 -0
  13. package/abis/IZoraFactory.json +227 -0
  14. package/abis/TrendCoin.json +2043 -0
  15. package/abis/ZoraFactoryImpl.json +232 -0
  16. package/dist/index.cjs +962 -117
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +960 -117
  19. package/dist/index.js.map +1 -1
  20. package/dist/wagmiGenerated.d.ts +1404 -131
  21. package/dist/wagmiGenerated.d.ts.map +1 -1
  22. package/package/wagmiGenerated.ts +970 -119
  23. package/package.json +4 -2
  24. package/src/BaseCoin.sol +44 -14
  25. package/src/ContentCoin.sol +20 -1
  26. package/src/CreatorCoin.sol +3 -0
  27. package/src/TrendCoin.sol +117 -0
  28. package/src/ZoraFactoryImpl.sol +142 -1
  29. package/src/hooks/ZoraV4CoinHook.sol +73 -8
  30. package/src/interfaces/ICoin.sol +5 -1
  31. package/src/interfaces/ICreatorCoin.sol +0 -3
  32. package/src/interfaces/IHasCreationInfo.sol +12 -0
  33. package/src/interfaces/IPoolManager.sol +13 -0
  34. package/src/interfaces/ITrendCoin.sol +26 -0
  35. package/src/interfaces/ITrendCoinErrors.sol +18 -0
  36. package/src/interfaces/IZoraFactory.sol +60 -1
  37. package/src/libs/CoinConstants.sol +25 -1
  38. package/src/libs/CoinRewardsV4.sol +67 -19
  39. package/src/libs/CoinSetup.sol +7 -1
  40. package/src/libs/TickerUtils.sol +84 -0
  41. package/src/libs/UniV4SwapToCurrency.sol +2 -1
  42. package/src/libs/V3ToV4SwapLib.sol +7 -3
  43. package/src/version/ContractVersionBase.sol +1 -1
  44. package/test/CoinUniV4.t.sol +4 -0
  45. package/test/ContentCoinRewards.t.sol +1 -0
  46. package/test/CreatorCoin.t.sol +2 -1
  47. package/test/CreatorCoinRewards.t.sol +1 -0
  48. package/test/Factory.t.sol +31 -5
  49. package/test/LaunchFee.t.sol +284 -0
  50. package/test/LiquidityMigration.t.sol +0 -2
  51. package/test/TrendCoin.t.sol +1077 -0
  52. package/test/Upgrades.t.sol +16 -3
  53. package/test/utils/FeeEstimatorHook.sol +33 -8
  54. package/test/utils/V4TestSetup.sol +36 -4
  55. package/wagmi.config.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/coins",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
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.22.12",
38
38
  "@zoralabs/shared-contracts": "^0.0.5",
39
39
  "@zoralabs/shared-scripts": "^0.0.0",
40
40
  "@zoralabs/tsconfig": "^0.0.1"
@@ -49,6 +49,8 @@
49
49
  "prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol'",
50
50
  "prettier:write": "prettier --write 'src/**/*.sol' 'test/**/*.sol'",
51
51
  "test": "forge test -vv",
52
+ "abi-check:check": "../../scripts/abi-check.sh check",
53
+ "abi-check:generate": "../../scripts/abi-check.sh generate",
52
54
  "test-gas": "forge test --gas-report",
53
55
  "update-contract-version": "pnpm exec update-contract-version",
54
56
  "wagmi:generate": "pnpm run build:contracts:minimal && wagmi generate && pnpm exec rename-generated-abi-casing ./package/wagmiGenerated.ts"
package/src/BaseCoin.sol CHANGED
@@ -11,6 +11,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
11
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
12
12
  import {ICoin, IHasTotalSupplyForPositions, IHasCoinType} from "./interfaces/ICoin.sol";
13
13
  import {IHasRewardsRecipients} from "./interfaces/IHasRewardsRecipients.sol";
14
+ import {IHasCreationInfo} from "./interfaces/IHasCreationInfo.sol";
14
15
  import {ICoinComments} from "./interfaces/ICoinComments.sol";
15
16
  import {IERC7572} from "./interfaces/IERC7572.sol";
16
17
  import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
@@ -52,7 +53,7 @@ import {PoolState} from "./types/PoolState.sol";
52
53
  \$$$$$$ | $$$$$$ |$$$$$$\ $$ | \$$ |
53
54
  \______/ \______/ \______|\__| \__|
54
55
  */
55
- abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ERC165Upgradeable {
56
+ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ERC165Upgradeable {
56
57
  using SafeERC20 for IERC20;
57
58
 
58
59
  /// @notice The address of the protocol rewards contract
@@ -75,8 +76,8 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
75
76
  string public tokenURI;
76
77
  /// @notice The address of the coin creator
77
78
  address public payoutRecipient;
78
- /// @notice The address of the platform referrer
79
- address public platformReferrer;
79
+ /// @notice The address of the platform referrer (internal storage, use platformReferrer() getter)
80
+ address internal _platformReferrer;
80
81
  /// @notice The address of the currency
81
82
  address public currency;
82
83
 
@@ -84,6 +85,11 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
84
85
  string private _name;
85
86
  /// @notice The symbol of the token
86
87
  string private _symbol;
88
+ /// @notice The timestamp when the coin was created
89
+ uint256 private _creationTimestamp;
90
+
91
+ /// @dev Transient storage slot for tracking deployment state
92
+ bytes32 private constant _IS_DEPLOYING_SLOT = keccak256("BaseCoin.isDeploying");
87
93
 
88
94
  /**
89
95
  * @notice The constructor for the static Coin contract deployment shared across all Coins.
@@ -125,6 +131,12 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
125
131
  uint160 sqrtPriceX96,
126
132
  PoolConfiguration memory poolConfiguration_
127
133
  ) public virtual initializer {
134
+ // Set transient deploying flag for launch fee bypass
135
+ _setIsDeploying(true);
136
+
137
+ // Record creation timestamp for launch fee calculation
138
+ _creationTimestamp = block.timestamp;
139
+
128
140
  currency = currency_;
129
141
  // we need to set this before initialization, because
130
142
  // distributing currency relies on the poolkey being set since the hooks
@@ -152,11 +164,6 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
152
164
  string memory symbol_,
153
165
  address platformReferrer_
154
166
  ) internal {
155
- // Validate the creation parameters
156
- if (payoutRecipient_ == address(0)) {
157
- revert AddressZero();
158
- }
159
-
160
167
  _setNameAndSymbol(name_, symbol_);
161
168
 
162
169
  // Set base contract state, leave name and symbol empty to save space.
@@ -167,12 +174,12 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
167
174
 
168
175
  __MultiOwnable_init(owners_);
169
176
 
170
- // Set mutable state
171
- _setPayoutRecipient(payoutRecipient_);
177
+ // Set mutable state (no validation here - subclasses validate if needed)
178
+ payoutRecipient = payoutRecipient_;
172
179
  _setContractURI(tokenURI_);
173
180
 
174
181
  // Store the referrer or use the protocol reward recipient if not set
175
- platformReferrer = platformReferrer_ == address(0) ? protocolRewardRecipient : platformReferrer_;
182
+ _platformReferrer = platformReferrer_ == address(0) ? protocolRewardRecipient : platformReferrer_;
176
183
 
177
184
  // Distribute the initial supply
178
185
  _handleInitialDistribution();
@@ -204,7 +211,7 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
204
211
 
205
212
  /// @notice Set the contract URI
206
213
  /// @param newURI The new URI
207
- function setContractURI(string memory newURI) external onlyOwner {
214
+ function setContractURI(string memory newURI) external virtual onlyOwner {
208
215
  _setContractURI(newURI);
209
216
  }
210
217
 
@@ -220,7 +227,7 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
220
227
  return _name;
221
228
  }
222
229
 
223
- function setNameAndSymbol(string memory newName, string memory newSymbol) external onlyOwner {
230
+ function setNameAndSymbol(string memory newName, string memory newSymbol) external virtual onlyOwner {
224
231
  _setNameAndSymbol(newName, newSymbol);
225
232
  }
226
233
 
@@ -253,7 +260,25 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
253
260
  interfaceId == type(IHasPoolKey).interfaceId ||
254
261
  interfaceId == type(IHasCoinType).interfaceId ||
255
262
  interfaceId == type(IHasTotalSupplyForPositions).interfaceId ||
256
- interfaceId == type(IHasSwapPath).interfaceId;
263
+ interfaceId == type(IHasSwapPath).interfaceId ||
264
+ interfaceId == type(IHasCreationInfo).interfaceId;
265
+ }
266
+
267
+ /// @inheritdoc IHasCreationInfo
268
+ function creationInfo() external view returns (uint256 creationTimestamp, bool isDeploying) {
269
+ creationTimestamp = _creationTimestamp;
270
+ bytes32 slot = _IS_DEPLOYING_SLOT;
271
+ assembly {
272
+ isDeploying := tload(slot)
273
+ }
274
+ }
275
+
276
+ /// @dev Sets the transient deploying flag
277
+ function _setIsDeploying(bool value) internal {
278
+ bytes32 slot = _IS_DEPLOYING_SLOT;
279
+ assembly {
280
+ tstore(slot, value)
281
+ }
257
282
  }
258
283
 
259
284
  /// @dev Overrides ERC20's _update function to emit a superset `CoinTransfer` event
@@ -304,6 +329,11 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
304
329
  return poolKey.hooks;
305
330
  }
306
331
 
332
+ /// @inheritdoc IHasRewardsRecipients
333
+ function platformReferrer() external view virtual returns (address) {
334
+ return _platformReferrer;
335
+ }
336
+
307
337
  /// @notice Migrate liquidity from current hook to a new hook implementation
308
338
  /// @param newHook Address of the new hook implementation
309
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
+ require(TickerUtils.validateTickerCharacters(symbol_), InvalidTickerCharacters());
67
+
68
+ // Generate URI from base URI + encoded symbol
69
+ string memory uri = string.concat(TREND_COIN_BASE_URI, TickerUtils.tickerToUri(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 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
  }
@@ -24,6 +24,8 @@ import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
24
24
  import {IMsgSender} from "../interfaces/IMsgSender.sol";
25
25
  import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
26
26
  import {ICoin, IHasSwapPath, IHasRewardsRecipients, IHasCoinType} from "../interfaces/ICoin.sol";
27
+ import {IHasCreationInfo} from "../interfaces/IHasCreationInfo.sol";
28
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
27
29
  import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
28
30
  import {IUpgradeableV4Hook, IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee, BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
29
31
  import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
@@ -41,6 +43,7 @@ import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
41
43
  import {TickMath} from "../utils/uniswap/TickMath.sol";
42
44
  import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
43
45
  import {ISupportsLimitOrderFill} from "../interfaces/ISupportsLimitOrderFill.sol";
46
+ import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
44
47
 
45
48
  /// @title ZoraV4CoinHook
46
49
  /// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
@@ -295,8 +298,9 @@ contract ZoraV4CoinHook is
295
298
  V4Liquidity.lockAndMint(poolManager, key, positions);
296
299
  }
297
300
 
298
- /// @notice Transiently stores the tick before a swap.
301
+ /// @notice Transiently stores the tick before a swap and calculates the launch fee.
299
302
  /// @dev This is used in `_afterSwap` to determine the ticks crossed during the swap.
303
+ /// Also returns a dynamic fee that decays from 99% to 1% over 10 seconds after coin creation.
300
304
  function _beforeSwap(
301
305
  address sender,
302
306
  PoolKey calldata key,
@@ -313,7 +317,61 @@ contract ZoraV4CoinHook is
313
317
  TransientSlot.Int256Slot slot = TransientSlot.asInt256(CoinConstants._BEFORE_SWAP_TICK_SLOT);
314
318
  TransientSlot.tstore(slot, int256(currentTick));
315
319
 
316
- return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
320
+ // Calculate launch fee
321
+ bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
322
+ address coin = poolCoins[poolKeyHash].coin;
323
+ uint24 fee = _calculateLaunchFee(coin);
324
+
325
+ return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), fee);
326
+ }
327
+
328
+ /// @notice Calculates the launch fee based on coin creation time
329
+ /// @dev Returns fee with OVERRIDE_FEE_FLAG to signal V4 to use this fee.
330
+ /// Fee decays linearly from LAUNCH_FEE_START (99%) to LP_FEE_V4 (1%) over LAUNCH_FEE_DURATION.
331
+ /// Returns LP_FEE_V4 for legacy coins that don't support IHasCreationInfo.
332
+ /// Bypasses launch fee (returns LP_FEE_V4) during initial coin deployment.
333
+ /// @param coin The coin address
334
+ /// @return fee The calculated fee with OVERRIDE_FEE_FLAG
335
+ function _calculateLaunchFee(address coin) internal view returns (uint24 fee) {
336
+ // Check if coin supports creation info interface (legacy coins won't)
337
+ try IERC165(coin).supportsInterface(type(IHasCreationInfo).interfaceId) returns (bool supported) {
338
+ if (!supported) {
339
+ // Legacy coin - use normal LP fee
340
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
341
+ }
342
+ } catch {
343
+ // supportsInterface call failed - use normal LP fee
344
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
345
+ }
346
+
347
+ // Get creation info from coin
348
+ (uint256 creationTimestamp, bool isDeploying) = IHasCreationInfo(coin).creationInfo();
349
+
350
+ // Bypass launch fee during initial deployment
351
+ if (isDeploying) {
352
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
353
+ }
354
+
355
+ // Calculate elapsed time since creation
356
+ uint256 elapsed = block.timestamp - creationTimestamp;
357
+
358
+ // If launch fee duration has passed, use normal LP fee (0% for trend coins)
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;
363
+ }
364
+ } catch {}
365
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
366
+ }
367
+
368
+ // Linear decay: fee = startFee - (elapsed / duration) * (startFee - endFee)
369
+ uint256 startFee = CoinConstants.LAUNCH_FEE_START;
370
+ uint256 endFee = CoinConstants.LP_FEE_V4;
371
+ uint256 feeReduction = (elapsed * (startFee - endFee)) / CoinConstants.LAUNCH_FEE_DURATION;
372
+ fee = uint24(startFee - feeReduction);
373
+
374
+ return CoinConstants.OVERRIDE_FEE_FLAG | fee;
317
375
  }
318
376
 
319
377
  /// @notice Internal fn called when a swap is executed.
@@ -354,16 +412,18 @@ contract ZoraV4CoinHook is
354
412
 
355
413
  (uint128 marketRewardsAmount0, uint128 marketRewardsAmount1) = CoinRewardsV4.mintLpReward(poolManager, key, fees0, fees1);
356
414
 
357
- // convert remaining fees to payout currency for market rewards
358
- (Currency payoutCurrency, uint128 payoutAmount) = CoinRewardsV4.convertToPayoutCurrency(
415
+ // convert remaining fees to payout currency for market rewards, and distribute any partial swap remainders
416
+ address tradeReferrer = CoinRewardsV4.getTradeReferral(hookData);
417
+ CoinRewardsV4.swapFeesToPayoutAndDistribute(
359
418
  poolManager,
360
419
  marketRewardsAmount0,
361
420
  marketRewardsAmount1,
362
- payoutSwapPath
421
+ payoutSwapPath,
422
+ ICoin(coin),
423
+ tradeReferrer,
424
+ ICoin(coin).coinType()
363
425
  );
364
426
 
365
- _distributeMarketRewards(payoutCurrency, payoutAmount, ICoin(coin), CoinRewardsV4.getTradeReferral(hookData));
366
-
367
427
  {
368
428
  (address swapper, bool isTrustedSwapSenderAddress) = _getOriginalMsgSender(sender);
369
429
  bool isCoinBuy = params.zeroForOne ? Currency.unwrap(key.currency1) == address(coin) : Currency.unwrap(key.currency0) == address(coin);
@@ -383,7 +443,12 @@ contract ZoraV4CoinHook is
383
443
  }
384
444
 
385
445
  (int24 tickBeforeSwap, int24 tickAfterSwap) = _getSwapTickRange(key);
386
- zoraLimitOrderBook.fill(key, !params.zeroForOne, tickBeforeSwap, tickAfterSwap, CoinConstants.SENTINEL_DEFAULT_LIMIT_ORDER_FILL_COUNT, address(0));
446
+
447
+ // Derive fill direction from actual tick movement
448
+ if (tickAfterSwap != tickBeforeSwap) {
449
+ bool isCurrency0 = tickAfterSwap > tickBeforeSwap;
450
+ zoraLimitOrderBook.fill(key, isCurrency0, tickBeforeSwap, tickAfterSwap, CoinConstants.SENTINEL_DEFAULT_LIMIT_ORDER_FILL_COUNT, address(0));
451
+ }
387
452
 
388
453
  return (BaseHook.afterSwap.selector, 0);
389
454
  }
@@ -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);