@zoralabs/coins 2.5.0 → 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 (46) hide show
  1. package/.turbo/turbo-build$colon$js.log +143 -131
  2. package/CHANGELOG.md +20 -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 +130 -0
  8. package/abis/ITrendCoinErrors.json +23 -0
  9. package/abis/IUniversalRouter.json +61 -0
  10. package/abis/IZoraFactory.json +227 -0
  11. package/abis/TrendCoin.json +2043 -0
  12. package/abis/ZoraFactoryImpl.json +232 -0
  13. package/dist/index.cjs +953 -138
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.js +951 -138
  16. package/dist/index.js.map +1 -1
  17. package/dist/wagmiGenerated.d.ts +1380 -149
  18. package/dist/wagmiGenerated.d.ts.map +1 -1
  19. package/package/wagmiGenerated.ts +960 -139
  20. package/package.json +2 -2
  21. package/src/BaseCoin.sol +12 -12
  22. package/src/ContentCoin.sol +20 -1
  23. package/src/CreatorCoin.sol +3 -0
  24. package/src/TrendCoin.sol +117 -0
  25. package/src/ZoraFactoryImpl.sol +142 -1
  26. package/src/hooks/ZoraV4CoinHook.sol +14 -6
  27. package/src/interfaces/ICoin.sol +5 -1
  28. package/src/interfaces/ICreatorCoin.sol +0 -3
  29. package/src/interfaces/IPoolManager.sol +13 -0
  30. package/src/interfaces/ITrendCoin.sol +26 -0
  31. package/src/interfaces/ITrendCoinErrors.sol +18 -0
  32. package/src/interfaces/IZoraFactory.sol +60 -1
  33. package/src/libs/CoinConstants.sol +9 -1
  34. package/src/libs/CoinRewardsV4.sol +67 -19
  35. package/src/libs/TickerUtils.sol +84 -0
  36. package/src/libs/UniV4SwapToCurrency.sol +2 -1
  37. package/src/version/ContractVersionBase.sol +1 -1
  38. package/test/CreatorCoin.t.sol +2 -1
  39. package/test/Factory.t.sol +31 -5
  40. package/test/LaunchFee.t.sol +0 -2
  41. package/test/LiquidityMigration.t.sol +0 -2
  42. package/test/TrendCoin.t.sol +1077 -0
  43. package/test/Upgrades.t.sol +16 -3
  44. package/test/utils/FeeEstimatorHook.sol +33 -8
  45. package/test/utils/V4TestSetup.sol +36 -4
  46. package/wagmi.config.ts +2 -0
@@ -46,7 +46,7 @@ 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
@@ -93,4 +93,12 @@ library CoinConstants {
93
93
  int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
94
94
  uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
95
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";
96
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);
@@ -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
 
@@ -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.0";
13
13
  }
14
14
  }
@@ -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;