@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
@@ -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,17 @@ 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 The LP fee for trend coins (after launch fee period)
57
+ /// @dev 100 pips = 1 basis point = 0.01%
58
+ uint24 internal constant TREND_LP_FEE_V4 = 100;
59
+
56
60
  /// @notice Flag to enable dynamic fees for the pool
57
61
  /// @dev When set in pool key fee, enables hook to override fee per-swap
58
62
  uint24 internal constant DYNAMIC_FEE_FLAG = 0x800000;
@@ -93,4 +97,12 @@ library CoinConstants {
93
97
  int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
94
98
  uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
95
99
  uint256 internal constant DEFAULT_DISCOVERY_SUPPLY_SHARE = 0.495e18; // half of the 990m total pool supply
100
+
101
+ /// @notice The default pool configuration for TrendCoins
102
+ /// @dev Pre-encoded bytes for version 4 with 3 curves and ZORA currency
103
+ /// Curve 1: ticks [-89200, -75200], 11 positions, 5% max supply
104
+ /// Curve 2: ticks [-77200, -68200], 11 positions, 12.5% max supply
105
+ /// Curve 3: ticks [-71200, -68200], 11 positions, 20% max supply
106
+ bytes internal constant TREND_COIN_DEFAULT_POOL_CONFIG =
107
+ hex"00000000000000000000000000000000000000000000000000000000000000040000000000000000000000001111111111166b7fe7bd91427724b487980afc6900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffea390fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed270fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee9e00000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeda40fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef598fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef5980000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000002c68af0bb140000";
96
108
  }
@@ -38,30 +38,64 @@ library CoinRewardsV4 {
38
38
  using SafeERC20 for IERC20;
39
39
 
40
40
  function getTradeReferral(bytes calldata hookData) internal pure returns (address) {
41
- return hookData.length >= 20 ? abi.decode(hookData, (address)) : address(0);
41
+ return hookData.length >= 32 ? 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
 
@@ -101,8 +135,15 @@ library CoinRewardsV4 {
101
135
  IPoolManager poolManager,
102
136
  PoolKey calldata key,
103
137
  int128 fees0,
104
- int128 fees1
138
+ int128 fees1,
139
+ IHasCoinType.CoinType coinType
105
140
  ) internal returns (uint128 marketRewardsAmount0, uint128 marketRewardsAmount1) {
141
+ if (coinType == IHasCoinType.CoinType.Trend) {
142
+ marketRewardsAmount0 = fees0 > 0 ? uint128(fees0) : 0;
143
+ marketRewardsAmount1 = fees1 > 0 ? uint128(fees1) : 0;
144
+ return (marketRewardsAmount0, marketRewardsAmount1);
145
+ }
146
+
106
147
  if (fees0 > 0) {
107
148
  uint128 lpRewardAmount0 = computeLpReward(uint128(fees0));
108
149
  if (lpRewardAmount0 > 0) {
@@ -188,7 +229,8 @@ library CoinRewardsV4 {
188
229
  platformReferrer,
189
230
  protocolRewardRecipient,
190
231
  doppler,
191
- tradeReferrer
232
+ tradeReferrer,
233
+ coinType
192
234
  );
193
235
 
194
236
  IZoraV4CoinHook.MarketRewardsV4 memory marketRewards = IZoraV4CoinHook.MarketRewardsV4({
@@ -242,9 +284,16 @@ library CoinRewardsV4 {
242
284
  address platformReferrer,
243
285
  address protocolRewardRecipient,
244
286
  address doppler,
245
- address tradeReferral
287
+ address tradeReferral,
288
+ IHasCoinType.CoinType coinType
246
289
  ) internal returns (MarketRewards memory rewards) {
247
- rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0));
290
+ if (coinType == IHasCoinType.CoinType.Trend) {
291
+ rewards.protocolAmount = fee;
292
+ _transferCurrency(currency, fee, protocolRewardRecipient, address(0));
293
+ return rewards;
294
+ }
295
+
296
+ rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0), coinType);
248
297
 
249
298
  // Notes on ETH transfer fallback behavior:
250
299
  // - 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 +325,24 @@ library CoinRewardsV4 {
276
325
  }
277
326
  }
278
327
 
279
- function _computeMarketRewards(uint128 fee, bool hasTradeReferral, bool hasCreateReferral) internal pure returns (MarketRewards memory rewards) {
328
+ function _computeMarketRewards(
329
+ uint128 fee,
330
+ bool hasTradeReferral,
331
+ bool hasCreateReferral,
332
+ IHasCoinType.CoinType coinType
333
+ ) internal pure returns (MarketRewards memory rewards) {
280
334
  if (fee == 0) {
281
335
  return rewards;
282
336
  }
283
337
 
284
338
  uint256 totalAmount = uint256(fee);
339
+
340
+ // TrendCoins: 100% of market rewards go to protocol (80% of total fees, with 20% already going to LPs)
341
+ if (coinType == IHasCoinType.CoinType.Trend) {
342
+ rewards.protocolAmount = totalAmount;
343
+ return rewards;
344
+ }
345
+
285
346
  rewards.platformReferrerAmount = hasCreateReferral ? calculateReward(totalAmount, CoinConstants.CREATE_REFERRAL_REWARD_BPS) : 0;
286
347
  rewards.tradeReferrerAmount = hasTradeReferral ? calculateReward(totalAmount, CoinConstants.TRADE_REFERRAL_REWARD_BPS) : 0;
287
348
  rewards.creatorAmount = calculateReward(totalAmount, CoinConstants.CREATOR_REWARD_BPS);
@@ -0,0 +1,66 @@
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
+ import {ITrendCoinErrors} from "../interfaces/ITrendCoinErrors.sol";
11
+
12
+ /// @title TickerUtils
13
+ /// @notice Library for ASCII case-folding ticker symbols for uniqueness checking
14
+ library TickerUtils {
15
+ /// @notice Converts a ticker string to lowercase (ASCII case-folding)
16
+ /// @param ticker The ticker symbol to fold
17
+ /// @return The lowercase ticker bytes
18
+ function lowercaseTicker(string memory ticker) internal pure returns (bytes memory) {
19
+ bytes memory tickerBytes = bytes(ticker);
20
+ bytes memory result = new bytes(tickerBytes.length);
21
+
22
+ for (uint256 i = 0; i < tickerBytes.length; i++) {
23
+ bytes1 char = tickerBytes[i];
24
+ // If uppercase A-Z (0x41-0x5A), convert to lowercase (add 0x20)
25
+ if (char >= 0x41 && char <= 0x5A) {
26
+ result[i] = bytes1(uint8(char) + 32);
27
+ } else {
28
+ result[i] = char;
29
+ }
30
+ }
31
+
32
+ return result;
33
+ }
34
+
35
+ /// @notice Computes a hash of the case-folded ticker for uniqueness checking
36
+ /// @param ticker The ticker symbol to hash
37
+ /// @return The keccak256 hash of the lowercase ticker
38
+ function tickerHash(string memory ticker) internal pure returns (bytes32) {
39
+ return keccak256(lowercaseTicker(ticker));
40
+ }
41
+
42
+ /// @notice Validates that a ticker symbol contains only allowed characters and has valid length
43
+ /// @dev Allowed characters: 0-9 (0x30-0x39), A-Z (0x41-0x5A), a-z (0x61-0x7A). Length must be 2-32.
44
+ /// Reverts if parameters are invalid
45
+ /// @param ticker The ticker symbol to validate
46
+ function requireValidateTickerCharacters(string memory ticker) internal pure {
47
+ bytes memory tickerBytes = bytes(ticker);
48
+ // Ticker is between 2 and 32 chars in length, only ascii A-Z 0-9 a-z
49
+ if (tickerBytes.length < 2) {
50
+ revert ITrendCoinErrors.TickerTooShort();
51
+ }
52
+ if (tickerBytes.length > 32) {
53
+ revert ITrendCoinErrors.TickerTooLong();
54
+ }
55
+ for (uint256 i = 0; i < tickerBytes.length; i++) {
56
+ bytes1 char = tickerBytes[i];
57
+ bool isValid = (char >= 0x30 && char <= 0x39) || // 0-9
58
+ (char >= 0x41 && char <= 0x5A) || // A-Z
59
+ (char >= 0x61 && char <= 0x7A); // a-z
60
+
61
+ if (!isValid) {
62
+ revert ITrendCoinErrors.TickerInvalidCharacters();
63
+ }
64
+ }
65
+ }
66
+ }
@@ -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
 
@@ -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.5.0";
12
+ return "2.6.1";
13
13
  }
14
14
  }
@@ -5,7 +5,20 @@ import "forge-std/Test.sol";
5
5
  import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
6
6
  import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
7
7
 
8
+ contract CoinRewardsV4Harness {
9
+ function getTradeReferral(bytes calldata hookData) external pure returns (address) {
10
+ return CoinRewardsV4.getTradeReferral(hookData);
11
+ }
12
+ }
13
+
8
14
  contract CoinRewardsV4Test is Test {
15
+ CoinRewardsV4Harness internal harness;
16
+ address internal constant TRADE_REFERRER = 0x1234567890123456789012345678901234567890;
17
+
18
+ function setUp() public {
19
+ harness = new CoinRewardsV4Harness();
20
+ }
21
+
9
22
  function test_convertDeltaToPositiveUint128_success_with_valid_positive_values() public pure {
10
23
  // Test with small positive value
11
24
  int256 smallDelta = 1000;
@@ -30,4 +43,39 @@ contract CoinRewardsV4Test is Test {
30
43
  }
31
44
  CoinRewardsV4.convertDeltaToPositiveUint128(difference);
32
45
  }
46
+
47
+ function test_getTradeReferral_returnsZeroAddress_forHookDataUnderTwentyBytes(uint8 length) public view {
48
+ vm.assume(length < 20);
49
+
50
+ assertEq(harness.getTradeReferral(_bytesOfLength(length)), address(0));
51
+ }
52
+
53
+ function test_getTradeReferral_returnsZeroAddress_forTwentyByteHookData() public view {
54
+ assertEq(harness.getTradeReferral(_bytesOfLength(20)), address(0));
55
+ }
56
+
57
+ function test_getTradeReferral_returnsZeroAddress_forTwentyOneThroughThirtyOneByteHookData(uint8 length) public view {
58
+ length = uint8(bound(length, 21, 31));
59
+
60
+ assertEq(harness.getTradeReferral(_bytesOfLength(length)), address(0));
61
+ }
62
+
63
+ function test_getTradeReferral_decodesAbiEncodedAddress() public view {
64
+ assertEq(harness.getTradeReferral(abi.encode(TRADE_REFERRER)), TRADE_REFERRER);
65
+ }
66
+
67
+ function test_getTradeReferral_decodesAbiEncodedAddressWithTrailingData() public view {
68
+ bytes memory hookData = abi.encodePacked(abi.encode(TRADE_REFERRER), bytes12(uint96(0xabcdef)));
69
+
70
+ assertGt(hookData.length, 32);
71
+ assertEq(harness.getTradeReferral(hookData), TRADE_REFERRER);
72
+ }
73
+
74
+ function _bytesOfLength(uint256 length) private pure returns (bytes memory data) {
75
+ data = new bytes(length);
76
+
77
+ for (uint256 i; i < length; i++) {
78
+ data[i] = bytes1(uint8(i + 1));
79
+ }
80
+ }
33
81
  }
@@ -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
 
@@ -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), "");
@@ -204,8 +204,6 @@ contract LaunchFeeTest is BaseTest {
204
204
  // The initial supply purchase during deployment should bypass launch fee
205
205
  // This is verified by checking the creator receives coins during deployment
206
206
 
207
- uint256 creatorBalanceBefore = 0; // Creator has no coins before deployment
208
-
209
207
  _deployCoin();
210
208
 
211
209
  uint256 creatorBalanceAfter = coin.balanceOf(users.creator);
@@ -435,8 +435,6 @@ contract LiquidityMigrationTest is BaseTest {
435
435
 
436
436
  BaseCoin coin = BaseCoin(contentCoin);
437
437
 
438
- uint24 oldFee = coin.getPoolKey().fee;
439
-
440
438
  // Register upgrade path
441
439
  address[] memory baseImpls = new address[](1);
442
440
  baseImpls[0] = oldHook;