@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
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ /// @title IHasCreationInfo
5
+ /// @notice Interface for coins that support launch fee functionality
6
+ /// @dev Legacy coins that don't implement this interface will use the normal LP fee
7
+ interface IHasCreationInfo {
8
+ /// @notice Returns creation info for the coin used by the launch fee calculation
9
+ /// @return creationTimestamp The block.timestamp when the coin was initialized
10
+ /// @return isDeploying True if the coin is being deployed (transient), false otherwise
11
+ function creationInfo() external view returns (uint256 creationTimestamp, bool isDeploying);
12
+ }
@@ -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,18 @@
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
+ /// @dev Allowed characters: space (0x20), dash (0x2D), 0-9, A-Z, a-z
10
+ error InvalidTickerCharacters();
11
+
12
+ /// @notice Thrown when attempting to deploy a trend coin with a ticker that already exists
13
+ /// @param symbol The ticker symbol that was already used
14
+ error TickerAlreadyUsed(string symbol);
15
+
16
+ /// @notice Thrown when attempting to use the legacy initialize function for a trend coin
17
+ error UseSpecificTrendCoinInitialize();
18
+ }
@@ -4,8 +4,9 @@ pragma solidity ^0.8.23;
4
4
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
5
5
  import {PoolKeyStruct} from "./ICoin.sol";
6
6
  import {IDeployedCoinVersionLookup} from "./IDeployedCoinVersionLookup.sol";
7
+ import {ITrendCoinErrors} from "./ITrendCoinErrors.sol";
7
8
 
8
- interface IZoraFactory is IDeployedCoinVersionLookup {
9
+ interface IZoraFactory is IDeployedCoinVersionLookup, ITrendCoinErrors {
9
10
  /// @notice Emitted when a coin is created
10
11
  /// @param caller The msg.sender address
11
12
  /// @param payoutRecipient The address of the creator payout recipient
@@ -80,6 +81,16 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
80
81
  string version
81
82
  );
82
83
 
84
+ /// @notice Emitted when a trend coin is created
85
+ /// @param caller The msg.sender address
86
+ /// @param symbol The symbol/ticker of the coin
87
+ /// @param coin The address of the coin
88
+ /// @param poolKey The uniswap v4 pool key
89
+ /// @param poolKeyHash The hash of the pool key
90
+ /// @param poolConfig The encoded pool configuration (curve config)
91
+ /// @param version The coin contract version
92
+ event TrendCoinCreated(address indexed caller, string symbol, address coin, PoolKey poolKey, bytes32 poolKeyHash, bytes poolConfig, string version);
93
+
83
94
  /// @notice Thrown when ETH is sent with a transaction but the currency is not WETH
84
95
  error EthTransferInvalid();
85
96
 
@@ -97,6 +108,13 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
97
108
  /// @notice Thrwon when an invalid config version is provided
98
109
  error InvalidConfig();
99
110
 
111
+ /// @notice Thrown when trying to deploy a trend coin before the pool config has been set
112
+ error TrendCoinPoolConfigNotSet();
113
+
114
+ /// @notice Emitted when the trend coin pool config is updated
115
+ /// @param poolConfig The new pool configuration
116
+ event TrendCoinPoolConfigUpdated(bytes poolConfig);
117
+
100
118
  /// @dev Deprecated: use `deployCreatorCoin` instead that has a salt and post-deploy hook specified
101
119
  function deployCreatorCoin(
102
120
  address payoutRecipient,
@@ -219,4 +237,45 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
219
237
 
220
238
  /// @notice The address of the Zora hook registry
221
239
  function zoraHookRegistry() external view returns (address);
240
+
241
+ /// @notice Creates a new trend coin with an optional hook that runs after the coin is deployed.
242
+ /// Enables buying initial supply by supporting ETH transfers to the post-deploy hook.
243
+ /// @dev TrendCoins have no payout recipient or platform referrer, and 100% of supply goes to the liquidity pool
244
+ /// @param symbol The ticker symbol for the trend coin (must be unique, case-insensitive)
245
+ /// @param postDeployHook The address of the hook to run after the coin is deployed
246
+ /// @param postDeployHookData The data to pass to the hook
247
+ /// @return coin The address of the deployed trend coin
248
+ /// @return postDeployHookDataOut The data returned by the hook
249
+ function deployTrendCoin(
250
+ string calldata symbol,
251
+ address postDeployHook,
252
+ bytes calldata postDeployHookData
253
+ ) external payable returns (address coin, bytes memory postDeployHookDataOut);
254
+
255
+ /// @notice Predicts the address of a trend coin that will be deployed with the given ticker
256
+ /// @param symbol The ticker symbol for the trend coin
257
+ /// @return The address of the trend coin contract
258
+ function trendCoinAddress(string calldata symbol) external view returns (address);
259
+
260
+ /// @notice The trend coin contract implementation address
261
+ function trendCoinImpl() external view returns (address);
262
+
263
+ /// @notice Sets the pool configuration for trend coins
264
+ /// @param currency The currency address for the pool (e.g., ZORA token)
265
+ /// @param tickLower Array of lower tick bounds for each curve
266
+ /// @param tickUpper Array of upper tick bounds for each curve
267
+ /// @param numDiscoveryPositions Array of number of discovery positions for each curve
268
+ /// @param maxDiscoverySupplyShare Array of max supply share (in WAD) for each curve
269
+ /// @dev Can only be called by the contract owner. Arrays must all be the same length.
270
+ function setTrendCoinPoolConfig(
271
+ address currency,
272
+ int24[] memory tickLower,
273
+ int24[] memory tickUpper,
274
+ uint16[] memory numDiscoveryPositions,
275
+ uint256[] memory maxDiscoverySupplyShare
276
+ ) external;
277
+
278
+ /// @notice Returns the current pool configuration for trend coins
279
+ /// @return The encoded pool configuration
280
+ function trendCoinPoolConfig() external view returns (bytes memory);
222
281
  }
@@ -46,13 +46,29 @@ library CoinConstants {
46
46
  uint256 internal constant CREATOR_VESTING_DURATION = (5 * 365.25 days);
47
47
 
48
48
  /// @notice The backing currency for creator coins
49
- /// @dev ETH backing currency address
49
+ /// @dev ZORA currency backing currency address
50
50
  address internal constant CREATOR_COIN_CURRENCY = 0x1111111111166b7FE7bd91427724B487980aFc69;
51
51
 
52
52
  /// @notice The LP fee
53
53
  /// @dev 10000 basis points = 1%
54
54
  uint24 internal constant LP_FEE_V4 = 10_000;
55
55
 
56
+ /// @notice Flag to enable dynamic fees for the pool
57
+ /// @dev When set in pool key fee, enables hook to override fee per-swap
58
+ uint24 internal constant DYNAMIC_FEE_FLAG = 0x800000;
59
+
60
+ /// @notice Flag to override the fee in beforeSwap return value
61
+ /// @dev Combined with fee value to signal V4 to use the returned fee
62
+ uint24 internal constant OVERRIDE_FEE_FLAG = 0x400000;
63
+
64
+ /// @notice Starting fee for launch fee (99%)
65
+ /// @dev 990,000 pips = 99% (1,000,000 pips = 100%)
66
+ uint24 internal constant LAUNCH_FEE_START = 990_000;
67
+
68
+ /// @notice Duration over which launch fee decays from start to end fee
69
+ /// @dev 10 seconds
70
+ uint256 internal constant LAUNCH_FEE_DURATION = 10 seconds;
71
+
56
72
  /// @notice The spacing for 1% pools
57
73
  /// @dev 200 ticks
58
74
  int24 internal constant TICK_SPACING = 200;
@@ -77,4 +93,12 @@ library CoinConstants {
77
93
  int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
78
94
  uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
79
95
  uint256 internal constant DEFAULT_DISCOVERY_SUPPLY_SHARE = 0.495e18; // half of the 990m total pool supply
96
+
97
+ /// @notice The default pool configuration for TrendCoins
98
+ /// @dev Pre-encoded bytes for version 4 with 3 curves and ZORA currency
99
+ /// Curve 1: ticks [-89200, -75200], 11 positions, 5% max supply
100
+ /// Curve 2: ticks [-77200, -68200], 11 positions, 12.5% max supply
101
+ /// Curve 3: ticks [-71200, -68200], 11 positions, 20% max supply
102
+ bytes internal constant TREND_COIN_DEFAULT_POOL_CONFIG =
103
+ hex"00000000000000000000000000000000000000000000000000000000000000040000000000000000000000001111111111166b7fe7bd91427724b487980afc6900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffea390fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed270fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee9e00000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeda40fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef598fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef5980000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000002c68af0bb140000";
80
104
  }
@@ -41,27 +41,61 @@ library CoinRewardsV4 {
41
41
  return hookData.length >= 20 ? abi.decode(hookData, (address)) : address(0);
42
42
  }
43
43
 
44
- /// @dev Converts collected fees from LP positions into target payout currency, and transfers to hook contract, so
45
- /// that they can later be distributed as rewards.
44
+ /// @dev Swaps collected fees through the payout path and distributes all deltas as rewards.
45
+ /// This handles partial swap execution where swaps may hit price limits and leave unsettled deltas.
46
+ /// After executing the swap path, all positive deltas (both intermediate and final) are taken and distributed.
46
47
  /// @param poolManager The pool manager instance
47
48
  /// @param fees0 The amount of fees collected in currency0
48
49
  /// @param fees1 The amount of fees collected in currency1
49
- /// @param payoutSwapPath The swap path to convert fees to target currency
50
- /// @return receivedCurrency The final currency after swapping
51
- /// @return receivedAmount The final amount after swapping
52
- function convertToPayoutCurrency(
50
+ /// @param payoutSwapPath The swap path for converting fees to the payout currency
51
+ /// @param coin The coin interface for getting reward recipients
52
+ /// @param tradeReferrer The trade referrer address
53
+ /// @param coinType The coin type for proper event emission
54
+ function swapFeesToPayoutAndDistribute(
53
55
  IPoolManager poolManager,
54
56
  uint128 fees0,
55
57
  uint128 fees1,
56
- IHasSwapPath.PayoutSwapPath memory payoutSwapPath
57
- ) internal returns (Currency receivedCurrency, uint128 receivedAmount) {
58
- // This handles multi-hop swaps if needed (e.g. coin -> backingCoin -> backingCoin's currency)
59
- (receivedCurrency, receivedAmount) = UniV4SwapToCurrency.swapToPath(poolManager, fees0, fees1, payoutSwapPath.currencyIn, payoutSwapPath.path);
60
-
61
- // Transfer the final converted currency amount to this contract for distribution
62
- // This makes the tokens available for the subsequent reward distribution
63
- if (receivedAmount > 0) {
64
- poolManager.take(receivedCurrency, address(this), receivedAmount);
58
+ IHasSwapPath.PayoutSwapPath memory payoutSwapPath,
59
+ IHasRewardsRecipients coin,
60
+ address tradeReferrer,
61
+ IHasCoinType.CoinType coinType
62
+ ) internal {
63
+ // Execute the swap path - may only partially execute if hitting price limits
64
+ UniV4SwapToCurrency.swapToPath(poolManager, fees0, fees1, payoutSwapPath.currencyIn, payoutSwapPath.path);
65
+
66
+ // After swap path execution, iterate through all currencies and take/distribute any positive deltas
67
+ // This handles cases where swaps only partially execute, leaving unsettled amounts
68
+
69
+ // Check currencyIn — partial swap remainder lives here
70
+ _takeAndDistributeIfPositiveDelta(poolManager, payoutSwapPath.currencyIn, coin, tradeReferrer, coinType);
71
+
72
+ // Check all intermediate currencies in the path (includes the other pool currency as path[0])
73
+ for (uint256 i = 0; i < payoutSwapPath.path.length; i++) {
74
+ Currency intermediateCurrency = payoutSwapPath.path[i].intermediateCurrency;
75
+ _takeAndDistributeIfPositiveDelta(poolManager, intermediateCurrency, coin, tradeReferrer, coinType);
76
+ }
77
+ }
78
+
79
+ /// @dev Checks if a currency has a positive delta, and if so, takes it and distributes as rewards
80
+ /// @param poolManager The pool manager instance
81
+ /// @param currency The currency to check
82
+ /// @param coin The coin interface for getting reward recipients
83
+ /// @param tradeReferrer The trade referrer address
84
+ /// @param coinType The coin type for proper event emission
85
+ function _takeAndDistributeIfPositiveDelta(
86
+ IPoolManager poolManager,
87
+ Currency currency,
88
+ IHasRewardsRecipients coin,
89
+ address tradeReferrer,
90
+ IHasCoinType.CoinType coinType
91
+ ) private {
92
+ int256 delta = TransientStateLibrary.currencyDelta(poolManager, address(this), currency);
93
+ // Only take if there's a positive delta
94
+ // Note: delta might be 0 if we already took from this currency in a previous call
95
+ if (delta > 0) {
96
+ uint128 amount = uint128(uint256(delta));
97
+ poolManager.take(currency, address(this), amount);
98
+ distributeMarketRewards(currency, amount, coin, tradeReferrer, coinType);
65
99
  }
66
100
  }
67
101
 
@@ -188,7 +222,8 @@ library CoinRewardsV4 {
188
222
  platformReferrer,
189
223
  protocolRewardRecipient,
190
224
  doppler,
191
- tradeReferrer
225
+ tradeReferrer,
226
+ coinType
192
227
  );
193
228
 
194
229
  IZoraV4CoinHook.MarketRewardsV4 memory marketRewards = IZoraV4CoinHook.MarketRewardsV4({
@@ -242,9 +277,10 @@ library CoinRewardsV4 {
242
277
  address platformReferrer,
243
278
  address protocolRewardRecipient,
244
279
  address doppler,
245
- address tradeReferral
280
+ address tradeReferral,
281
+ IHasCoinType.CoinType coinType
246
282
  ) internal returns (MarketRewards memory rewards) {
247
- rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0));
283
+ rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0), coinType);
248
284
 
249
285
  // Notes on ETH transfer fallback behavior:
250
286
  // - If the platform referrer is immutable; if it is set to an address that cannot receive ETH, it can brick swaps on the coin, as they would revert.
@@ -276,12 +312,24 @@ library CoinRewardsV4 {
276
312
  }
277
313
  }
278
314
 
279
- function _computeMarketRewards(uint128 fee, bool hasTradeReferral, bool hasCreateReferral) internal pure returns (MarketRewards memory rewards) {
315
+ function _computeMarketRewards(
316
+ uint128 fee,
317
+ bool hasTradeReferral,
318
+ bool hasCreateReferral,
319
+ IHasCoinType.CoinType coinType
320
+ ) internal pure returns (MarketRewards memory rewards) {
280
321
  if (fee == 0) {
281
322
  return rewards;
282
323
  }
283
324
 
284
325
  uint256 totalAmount = uint256(fee);
326
+
327
+ // TrendCoins: 100% of market rewards go to protocol (80% of total fees, with 20% already going to LPs)
328
+ if (coinType == IHasCoinType.CoinType.Trend) {
329
+ rewards.protocolAmount = totalAmount;
330
+ return rewards;
331
+ }
332
+
285
333
  rewards.platformReferrerAmount = hasCreateReferral ? calculateReward(totalAmount, CoinConstants.CREATE_REFERRAL_REWARD_BPS) : 0;
286
334
  rewards.tradeReferrerAmount = hasTradeReferral ? calculateReward(totalAmount, CoinConstants.TRADE_REFERRAL_REWARD_BPS) : 0;
287
335
  rewards.creatorAmount = calculateReward(totalAmount, CoinConstants.CREATOR_REWARD_BPS);
@@ -36,7 +36,13 @@ library CoinSetup {
36
36
  Currency currency0 = isCoinToken0 ? Currency.wrap(coin) : Currency.wrap(currency);
37
37
  Currency currency1 = isCoinToken0 ? Currency.wrap(currency) : Currency.wrap(coin);
38
38
 
39
- poolKey = PoolKey({currency0: currency0, currency1: currency1, fee: CoinConstants.LP_FEE_V4, tickSpacing: CoinConstants.TICK_SPACING, hooks: hooks});
39
+ poolKey = PoolKey({
40
+ currency0: currency0,
41
+ currency1: currency1,
42
+ fee: CoinConstants.DYNAMIC_FEE_FLAG,
43
+ tickSpacing: CoinConstants.TICK_SPACING,
44
+ hooks: hooks
45
+ });
40
46
  }
41
47
 
42
48
  function setupPoolWithVersion(
@@ -0,0 +1,84 @@
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 TickerUtils
11
+ /// @notice Library for ASCII case-folding ticker symbols for uniqueness checking
12
+ library TickerUtils {
13
+ /// @notice Converts a ticker string to lowercase (ASCII case-folding)
14
+ /// @param ticker The ticker symbol to fold
15
+ /// @return The lowercase ticker bytes
16
+ function foldTicker(string memory ticker) internal pure returns (bytes memory) {
17
+ bytes memory tickerBytes = bytes(ticker);
18
+ bytes memory result = new bytes(tickerBytes.length);
19
+
20
+ for (uint256 i = 0; i < tickerBytes.length; i++) {
21
+ bytes1 char = tickerBytes[i];
22
+ // If uppercase A-Z (0x41-0x5A), convert to lowercase (add 0x20)
23
+ if (char >= 0x41 && char <= 0x5A) {
24
+ result[i] = bytes1(uint8(char) + 32);
25
+ } else {
26
+ result[i] = char;
27
+ }
28
+ }
29
+
30
+ return result;
31
+ }
32
+
33
+ /// @notice Computes a hash of the case-folded ticker for uniqueness checking
34
+ /// @param ticker The ticker symbol to hash
35
+ /// @return The keccak256 hash of the lowercase ticker
36
+ function tickerHash(string memory ticker) internal pure returns (bytes32) {
37
+ return keccak256(foldTicker(ticker));
38
+ }
39
+
40
+ /// @notice Converts spaces in ticker to '+' for URI encoding
41
+ /// @param ticker The ticker symbol to encode
42
+ /// @return The ticker with spaces replaced by '+'
43
+ function tickerToUri(string memory ticker) internal pure returns (string memory) {
44
+ bytes memory tickerBytes = bytes(ticker);
45
+ bytes memory result = new bytes(tickerBytes.length);
46
+
47
+ for (uint256 i = 0; i < tickerBytes.length; i++) {
48
+ bytes1 char = tickerBytes[i];
49
+ // Replace space (0x20) with '+' (0x2B)
50
+ if (char == 0x20) {
51
+ result[i] = 0x2B;
52
+ } else {
53
+ result[i] = char;
54
+ }
55
+ }
56
+
57
+ return string(result);
58
+ }
59
+
60
+ /// @notice Validates that a ticker symbol contains only allowed characters
61
+ /// @dev Allowed characters: space (0x20), 0-9 (0x30-0x39), A-Z (0x41-0x5A), a-z (0x61-0x7A), dash (0x2D)
62
+ /// @param ticker The ticker symbol to validate
63
+ /// @return true if all characters are valid
64
+ function validateTickerCharacters(string memory ticker) internal pure returns (bool) {
65
+ bytes memory tickerBytes = bytes(ticker);
66
+ // Empty string is not allowed
67
+ if (tickerBytes.length == 0) {
68
+ return false;
69
+ }
70
+ for (uint256 i = 0; i < tickerBytes.length; i++) {
71
+ bytes1 char = tickerBytes[i];
72
+ bool isValid = char == 0x20 || // space
73
+ char == 0x2D || // dash (-)
74
+ (char >= 0x30 && char <= 0x39) || // 0-9
75
+ (char >= 0x41 && char <= 0x5A) || // A-Z
76
+ (char >= 0x61 && char <= 0x7A); // a-z
77
+
78
+ if (!isValid) {
79
+ return false;
80
+ }
81
+ }
82
+ return true;
83
+ }
84
+ }
@@ -63,7 +63,8 @@ library UniV4SwapToCurrency {
63
63
  if (inputAmount == 0) {
64
64
  outputAmount = initialAmountCurrency;
65
65
  } else {
66
- outputAmount = initialAmountCurrency + uint128(_swap(poolManager, poolKey, zeroForOne, -int128(inputAmount), bytes("")));
66
+ int128 swapResult = _swap(poolManager, poolKey, zeroForOne, -int128(inputAmount), bytes(""));
67
+ outputAmount = initialAmountCurrency + uint128(swapResult);
67
68
  }
68
69
  }
69
70
 
@@ -56,6 +56,7 @@ library V3ToV4SwapLib {
56
56
  struct V4SwapResult {
57
57
  uint128 outputAmount; // Final output amount
58
58
  Currency outputCurrency; // Final output currency
59
+ BalanceDelta targetPoolDelta; // Delta from final (target) pool swap
59
60
  }
60
61
 
61
62
  // ============ VALIDATION ============
@@ -167,6 +168,7 @@ library V3ToV4SwapLib {
167
168
  function executeV4MultiHopSwap(IPoolManager poolManager, V4SwapParams memory params) internal returns (V4SwapResult memory result) {
168
169
  Currency currentCurrency = params.startingCurrency;
169
170
  uint128 currentAmount = uint128(params.amountIn);
171
+ BalanceDelta lastDelta;
170
172
 
171
173
  // Execute swaps through the route
172
174
  for (uint256 i = 0; i < params.v4Route.length; i++) {
@@ -175,14 +177,14 @@ library V3ToV4SwapLib {
175
177
  // Determine swap direction based on current currency
176
178
  bool zeroForOne = currentCurrency == poolKey.currency0;
177
179
 
178
- BalanceDelta delta = poolManager.swap(
180
+ lastDelta = poolManager.swap(
179
181
  poolKey,
180
182
  SwapParams(zeroForOne, -(int128(currentAmount)), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
181
183
  ""
182
184
  );
183
185
 
184
186
  // Extract output amount from delta
185
- uint128 outputAmount = zeroForOne ? uint128(delta.amount1()) : uint128(delta.amount0());
187
+ uint128 outputAmount = zeroForOne ? uint128(lastDelta.amount1()) : uint128(lastDelta.amount0());
186
188
 
187
189
  // Update for next iteration
188
190
  currentAmount = outputAmount;
@@ -191,6 +193,7 @@ library V3ToV4SwapLib {
191
193
 
192
194
  result.outputAmount = currentAmount;
193
195
  result.outputCurrency = currentCurrency;
196
+ result.targetPoolDelta = lastDelta;
194
197
  }
195
198
 
196
199
  // ============ DELTA SETTLEMENT ============
@@ -212,7 +215,8 @@ library V3ToV4SwapLib {
212
215
  ) internal {
213
216
  // Pay the input amount
214
217
  if (inputCurrency.isAddressZero()) {
215
- // For ETH, settle with msg.value
218
+ // For ETH, sync and settle with msg.value
219
+ poolManager.sync(inputCurrency);
216
220
  poolManager.settle{value: inputAmount}();
217
221
  } else {
218
222
  // For ERC20, sync and transfer
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
9
9
  contract ContractVersionBase is IVersionedContract {
10
10
  /// @notice The version of the contract
11
11
  function contractVersion() external pure override returns (string memory) {
12
- return "2.4.1";
12
+ return "2.6.0";
13
13
  }
14
14
  }
@@ -34,6 +34,7 @@ import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
34
34
  import {ICoin, IHasSwapPath, PathKey} from "../src/interfaces/ICoin.sol";
35
35
  import {IDeployedCoinVersionLookup} from "../src/interfaces/IDeployedCoinVersionLookup.sol";
36
36
 
37
+ /// forge-config: default.isolate = true
37
38
  contract CoinUniV4Test is BaseTest {
38
39
  MockERC20 internal mockERC20A;
39
40
  MockERC20 internal mockERC20B;
@@ -113,6 +114,9 @@ contract CoinUniV4Test is BaseTest {
113
114
  address currency = address(mockERC20A);
114
115
  _deployV4Coin(currency);
115
116
 
117
+ // Skip past launch fee period to test normal LP fees
118
+ vm.warp(block.timestamp + 1 days);
119
+
116
120
  uint128 amountIn = uint128(0.00001 ether);
117
121
  uint128 minAmountOut = uint128(0);
118
122
 
@@ -17,6 +17,7 @@ import {CreatorCoin} from "../src/CreatorCoin.sol";
17
17
  import {ICoin} from "../src/interfaces/ICoin.sol";
18
18
  import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
19
19
 
20
+ /// forge-config: default.isolate = true
20
21
  contract ContentCoinRewardsTest is BaseTest {
21
22
  ContentCoin internal contentCoin;
22
23
  CreatorCoin internal backingCreatorCoin;
@@ -4,6 +4,7 @@ pragma solidity ^0.8.13;
4
4
  import {BaseTest} from "./utils/BaseTest.sol";
5
5
 
6
6
  import {ICreatorCoin} from "../src/interfaces/ICreatorCoin.sol";
7
+ import {ICoin} from "../src/interfaces/ICoin.sol";
7
8
  import {ICreatorCoinHook} from "../src/interfaces/ICreatorCoinHook.sol";
8
9
  import {CoinConstants} from "../src/libs/CoinConstants.sol";
9
10
  import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
@@ -92,7 +93,7 @@ contract CreatorCoinTest is BaseTest {
92
93
  );
93
94
 
94
95
  vm.prank(users.creator);
95
- vm.expectRevert(ICreatorCoin.InvalidCurrency.selector);
96
+ vm.expectRevert(ICoin.InvalidCurrency.selector);
96
97
  factory.deployCreatorCoin(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, address(0), bytes32(0));
97
98
  }
98
99
 
@@ -17,6 +17,7 @@ import {CreatorCoin} from "../src/CreatorCoin.sol";
17
17
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
18
18
  import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
19
19
 
20
+ /// forge-config: default.isolate = true
20
21
  contract CreatorCoinRewardsTest is BaseTest {
21
22
  CreatorCoin internal creatorCoin;
22
23
 
@@ -19,7 +19,13 @@ contract FactoryTest is BaseTest {
19
19
 
20
20
  function test_factory_constructor_and_proxy_setup() public {
21
21
  // Impl constructor test
22
- ZoraFactoryImpl impl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry));
22
+ ZoraFactoryImpl impl = new ZoraFactoryImpl(
23
+ address(coinV4Impl),
24
+ address(creatorCoinImpl),
25
+ address(trendCoinImpl),
26
+ address(hook),
27
+ address(zoraHookRegistry)
28
+ );
23
29
  assertEq(ZoraFactoryImpl(address(factory)).owner(), users.factoryOwner);
24
30
  assertEq(ZoraFactoryImpl(address(factory)).coinV4Impl(), address(coinV4Impl));
25
31
 
@@ -51,7 +57,9 @@ contract FactoryTest is BaseTest {
51
57
 
52
58
  assertEq(ZoraFactoryImpl(address(factory)).pendingOwner(), address(0));
53
59
 
54
- address newFactoryImpl = address(new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry)));
60
+ address newFactoryImpl = address(
61
+ new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(trendCoinImpl), address(hook), address(zoraHookRegistry))
62
+ );
55
63
 
56
64
  // Upgrade to current / new impl
57
65
  vm.prank(users.factoryOwner);
@@ -77,7 +85,13 @@ contract FactoryTest is BaseTest {
77
85
  }
78
86
 
79
87
  function test_upgrade() public {
80
- ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry));
88
+ ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
89
+ address(coinV4Impl),
90
+ address(creatorCoinImpl),
91
+ address(trendCoinImpl),
92
+ address(hook),
93
+ address(zoraHookRegistry)
94
+ );
81
95
 
82
96
  vm.prank(users.factoryOwner);
83
97
  ZoraFactoryImpl(address(factory)).upgradeToAndCall(address(newImpl), "");
@@ -98,7 +112,13 @@ contract FactoryTest is BaseTest {
98
112
  }
99
113
 
100
114
  function test_revert_invalid_owner() public {
101
- ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry));
115
+ ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
116
+ address(coinV4Impl),
117
+ address(creatorCoinImpl),
118
+ address(trendCoinImpl),
119
+ address(hook),
120
+ address(zoraHookRegistry)
121
+ );
102
122
 
103
123
  vm.prank(users.creator);
104
124
  vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.creator));
@@ -184,7 +204,13 @@ contract FactoryTest is BaseTest {
184
204
  _deployHooks(address(new MockZoraLimitOrderBook())); // Deploys new content and creator coin hook addresses
185
205
 
186
206
  // Deploy new factory impl with new content and creator coin hook addresses
187
- ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry));
207
+ ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
208
+ address(coinV4Impl),
209
+ address(creatorCoinImpl),
210
+ address(trendCoinImpl),
211
+ address(hook),
212
+ address(zoraHookRegistry)
213
+ );
188
214
 
189
215
  vm.prank(users.factoryOwner);
190
216
  ZoraFactoryImpl(address(factory)).upgradeToAndCall(address(newImpl), "");