@zoralabs/coins 2.6.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.
@@ -164,33 +164,27 @@ contract TrendCoinTest is BaseTest {
164
164
  assertTrue(coin2 != address(0), "Coin with different numbers should deploy");
165
165
  }
166
166
 
167
- function test_deployTrendCoin_validSymbols_dashOnly() public {
168
- (address coin1, ) = factory.deployTrendCoin("-", address(0), "");
169
- assertTrue(coin1 != address(0), "Coin with single dash should deploy");
167
+ function test_deployTrendCoin_invalidSymbols_dashOnly() public {
168
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
169
+ factory.deployTrendCoin("--", address(0), "");
170
170
 
171
- (address coin2, ) = factory.deployTrendCoin("--", address(0), "");
172
- assertTrue(coin2 != address(0), "Coin with double dash should deploy");
173
-
174
- (address coin3, ) = factory.deployTrendCoin("---", address(0), "");
175
- assertTrue(coin3 != address(0), "Coin with triple dash should deploy");
171
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
172
+ factory.deployTrendCoin("---", address(0), "");
176
173
  }
177
174
 
178
- function test_deployTrendCoin_validSymbols_spaceOnly() public {
179
- (address coin1, ) = factory.deployTrendCoin(" ", address(0), "");
180
- assertTrue(coin1 != address(0), "Coin with single space should deploy");
181
-
182
- (address coin2, ) = factory.deployTrendCoin(" ", address(0), "");
183
- assertTrue(coin2 != address(0), "Coin with double space should deploy");
175
+ function test_deployTrendCoin_invalidSymbols_spaceOnly() public {
176
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
177
+ factory.deployTrendCoin(" ", address(0), "");
184
178
 
185
- (address coin3, ) = factory.deployTrendCoin(" ", address(0), "");
186
- assertTrue(coin3 != address(0), "Coin with triple space should deploy");
179
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
180
+ factory.deployTrendCoin(" ", address(0), "");
187
181
  }
188
182
 
189
183
  function test_deployTrendCoin_validSymbols_allAllowedCharacters() public {
190
- (address coin1, ) = factory.deployTrendCoin("ABC-123 xyz", address(0), "");
184
+ (address coin1, ) = factory.deployTrendCoin("ABC123xyz", address(0), "");
191
185
  assertTrue(coin1 != address(0), "Coin with all character types should deploy");
192
186
 
193
- (address coin2, ) = factory.deployTrendCoin("Test-123 Coin", address(0), "");
187
+ (address coin2, ) = factory.deployTrendCoin("Test123Coin", address(0), "");
194
188
  assertTrue(coin2 != address(0), "Coin with mixed valid characters should deploy");
195
189
  }
196
190
 
@@ -199,7 +193,7 @@ contract TrendCoinTest is BaseTest {
199
193
  }
200
194
 
201
195
  function fixtureInvalidSymbols() public pure returns (InvalidSymbolTestCase[] memory) {
202
- InvalidSymbolTestCase[] memory cases = new InvalidSymbolTestCase[](34);
196
+ InvalidSymbolTestCase[] memory cases = new InvalidSymbolTestCase[](35);
203
197
  cases[0] = InvalidSymbolTestCase("TEST!");
204
198
  cases[1] = InvalidSymbolTestCase("TEST@");
205
199
  cases[2] = InvalidSymbolTestCase("TEST#");
@@ -233,24 +227,25 @@ contract TrendCoinTest is BaseTest {
233
227
  cases[30] = InvalidSymbolTestCase("TEST_COIN");
234
228
  cases[31] = InvalidSymbolTestCase("TEST.COIN");
235
229
  cases[32] = InvalidSymbolTestCase("TEST!@#");
236
- cases[33] = InvalidSymbolTestCase("");
230
+ cases[33] = InvalidSymbolTestCase("TEST COIN"); // space not allowed
231
+ cases[34] = InvalidSymbolTestCase("TEST-COIN"); // dash not allowed
237
232
  return cases;
238
233
  }
239
234
 
240
235
  function tableInvalidSymbolsTest(InvalidSymbolTestCase memory invalidSymbols) public {
241
- vm.expectRevert(ITrendCoinErrors.InvalidTickerCharacters.selector);
236
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
242
237
  factory.deployTrendCoin(invalidSymbols.symbol, address(0), "");
243
238
  }
244
239
 
245
- function test_deployTrendCoin_validSymbols_withSpaces() public {
246
- (address coin1, ) = factory.deployTrendCoin("TEST COIN", address(0), "");
247
- assertTrue(coin1 != address(0), "Coin with space in middle should deploy");
240
+ function test_deployTrendCoin_invalidSymbols_withSpaces() public {
241
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
242
+ factory.deployTrendCoin("TEST COIN", address(0), "");
248
243
 
249
- (address coin2, ) = factory.deployTrendCoin(" TEST ", address(0), "");
250
- assertTrue(coin2 != address(0), "Coin with leading and trailing spaces should deploy");
244
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
245
+ factory.deployTrendCoin(" TEST ", address(0), "");
251
246
 
252
- (address coin3, ) = factory.deployTrendCoin("TEST COIN 123", address(0), "");
253
- assertTrue(coin3 != address(0), "Coin with multiple spaces should deploy");
247
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
248
+ factory.deployTrendCoin("TEST COIN 123", address(0), "");
254
249
  }
255
250
 
256
251
  function test_deployTrendCoin_validSymbols_lettersAndNumbers() public {
@@ -264,28 +259,51 @@ contract TrendCoinTest is BaseTest {
264
259
  assertTrue(coin3 != address(0), "Coin with alternating letters and numbers should deploy");
265
260
  }
266
261
 
267
- function test_deployTrendCoin_validSymbols_lettersAndDash() public {
268
- (address coin1, ) = factory.deployTrendCoin("TEST-COIN", address(0), "");
269
- assertTrue(coin1 != address(0), "Coin with dash in middle should deploy");
262
+ function test_deployTrendCoin_invalidSymbols_lettersAndDash() public {
263
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
264
+ factory.deployTrendCoin("TEST-COIN", address(0), "");
270
265
 
271
- (address coin2, ) = factory.deployTrendCoin("-TEST-", address(0), "");
272
- assertTrue(coin2 != address(0), "Coin with leading and trailing dashes should deploy");
266
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
267
+ factory.deployTrendCoin("-TEST-", address(0), "");
273
268
 
274
- (address coin3, ) = factory.deployTrendCoin("TEST--COIN", address(0), "");
275
- assertTrue(coin3 != address(0), "Coin with multiple dashes should deploy");
269
+ vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
270
+ factory.deployTrendCoin("TEST--COIN", address(0), "");
276
271
  }
277
272
 
278
- function test_deployTrendCoin_validSymbols_allTypes() public {
279
- (address coin1, ) = factory.deployTrendCoin("TEST-123 Coin", address(0), "");
280
- assertTrue(coin1 != address(0), "Coin with all character types should deploy");
273
+ function test_deployTrendCoin_validSymbols_alphanumericMixed() public {
274
+ (address coin1, ) = factory.deployTrendCoin("TEST123Coin", address(0), "");
275
+ assertTrue(coin1 != address(0), "Coin with all alphanumeric types should deploy");
281
276
 
282
- (address coin2, ) = factory.deployTrendCoin("ABC 123-XYZ", address(0), "");
277
+ (address coin2, ) = factory.deployTrendCoin("ABC123XYZ", address(0), "");
283
278
  assertTrue(coin2 != address(0), "Coin with mixed valid characters should deploy");
284
279
 
285
- (address coin3, ) = factory.deployTrendCoin("Test-456 Coin 789", address(0), "");
280
+ (address coin3, ) = factory.deployTrendCoin("Test456Coin789", address(0), "");
286
281
  assertTrue(coin3 != address(0), "Coin with complex valid pattern should deploy");
287
282
  }
288
283
 
284
+ function test_deployTrendCoin_validSymbols_lengthBoundaries() public {
285
+ // Minimum valid length (2 characters)
286
+ (address coin1, ) = factory.deployTrendCoin("AB", address(0), "");
287
+ assertTrue(coin1 != address(0), "Coin with 2 characters should deploy");
288
+
289
+ // Maximum valid length (32 characters)
290
+ (address coin2, ) = factory.deployTrendCoin("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456", address(0), "");
291
+ assertTrue(coin2 != address(0), "Coin with 32 characters should deploy");
292
+ }
293
+
294
+ function test_deployTrendCoin_invalidSymbols_tooShort() public {
295
+ vm.expectRevert(ITrendCoinErrors.TickerTooShort.selector);
296
+ factory.deployTrendCoin("A", address(0), "");
297
+
298
+ vm.expectRevert(ITrendCoinErrors.TickerTooShort.selector);
299
+ factory.deployTrendCoin("", address(0), "");
300
+ }
301
+
302
+ function test_deployTrendCoin_invalidSymbols_tooLong() public {
303
+ vm.expectRevert(ITrendCoinErrors.TickerTooLong.selector);
304
+ factory.deployTrendCoin("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567", address(0), "");
305
+ }
306
+
289
307
  // ============ Fee Distribution Tests ============
290
308
 
291
309
  function _deployTrendCoin() internal {
@@ -343,13 +361,15 @@ contract TrendCoinTest is BaseTest {
343
361
  vm.stopPrank();
344
362
  }
345
363
 
346
- /// @notice Test that TrendCoin fee distribution gives 80% to protocol (Zora) and 20% to LP
347
- /// For TrendCoins, 100% of market rewards (80% of total fees) should go to protocol
348
- function test_trendCoin_feeDistribution_80PercentToProtocol() public {
364
+ /// @notice Test that TrendCoin fee distribution sends 100% to protocol with no LP remint
365
+ /// For TrendCoins, all market rewards go to protocolRewardRecipient and LP remint is skipped.
366
+ /// Note: In Foundry tests, transient storage persists for the whole test (single tx), so
367
+ /// isDeploying stays true and _calculateLaunchFee always returns LP_FEE_V4 (1%).
368
+ /// In production (separate txs), trend coins use TREND_LP_FEE_V4 (0.01%) after launch.
369
+ function test_trendCoin_feeDistribution_100PercentToProtocol() public {
349
370
  _deployTrendCoin();
350
371
 
351
372
  // Roll forward past the launch fee period (10 seconds) to avoid high launch fees
352
- // Launch fee starts at 99% and decays to 1% over 10 seconds
353
373
  vm.warp(block.timestamp + 11 seconds);
354
374
 
355
375
  uint128 tradeAmount = 1000 ether; // 1000 ZORA tokens
@@ -365,26 +385,22 @@ contract TrendCoinTest is BaseTest {
365
385
  uint256 finalProtocolBalance = zoraToken.balanceOf(protocolRecipient);
366
386
  uint256 protocolReward = finalProtocolBalance - initialProtocolBalance;
367
387
 
368
- // Fee breakdown for TrendCoins after launch period:
369
- // - Total fee: 1% (LP_FEE_V4 = 10,000 pips = 1%)
370
- // - LP reward: 20% of fees (LP_REWARD_BPS = 2000)
371
- // - Market rewards: 80% of fees
372
- // - For TrendCoins: protocol gets 100% of market rewards
388
+ // Fee breakdown for TrendCoins in test (isDeploying=true due to transient storage):
389
+ // - Total fee: 1% (LP_FEE_V4, because isDeploying bypass is active)
390
+ // - No LP remint (skipped for trend coins)
391
+ // - Protocol gets 100% of fees
373
392
  //
374
393
  // Expected calculation:
375
394
  // - Total fees = tradeAmount * 1% = 10 ZORA
376
- // - LP gets = 10 ZORA * 20% = 2 ZORA (minted as new LP positions)
377
- // - Market rewards = 10 ZORA * 80% = 8 ZORA
378
- // - Protocol receives = 8 ZORA (100% of market rewards for TrendCoins)
395
+ // - Protocol receives = 10 ZORA (100% of fees, no LP remint for TrendCoins)
379
396
  uint256 expectedTotalFees = (uint256(tradeAmount) * CoinConstants.LP_FEE_V4) / 1_000_000;
380
- uint256 expectedMarketRewards = (expectedTotalFees * (10_000 - CoinConstants.LP_REWARD_BPS)) / 10_000;
381
397
 
382
- // Protocol should receive approximately all market rewards (allowing small rounding tolerance)
383
- assertApproxEqRel(protocolReward, expectedMarketRewards, 0.01e18, "Protocol should receive ~80% of total fees");
398
+ // Protocol should receive approximately all fees (allowing small rounding tolerance)
399
+ assertApproxEqRel(protocolReward, expectedTotalFees, 0.01e18, "Protocol should receive ~100% of total fees");
384
400
 
385
- // Verify actual value is reasonable (should be ~8 ZORA for 1000 ZORA trade)
386
- assertGt(protocolReward, 7.9 ether, "Protocol reward should be > 7.9 ZORA");
387
- assertLt(protocolReward, 8.1 ether, "Protocol reward should be < 8.1 ZORA");
401
+ // Verify actual value is reasonable (should be ~10 ZORA for 1000 ZORA trade at 1% fee)
402
+ assertGt(protocolReward, 9.9 ether, "Protocol reward should be > 9.9 ZORA");
403
+ assertLt(protocolReward, 10.1 ether, "Protocol reward should be < 10.1 ZORA");
388
404
 
389
405
  // Verify TrendCoin recipients are correctly configured (no creator/referrer rewards)
390
406
  assertEq(trendCoin.payoutRecipient(), address(0), "Payout recipient should be zero");
@@ -416,9 +432,9 @@ contract TrendCoinTest is BaseTest {
416
432
 
417
433
  // ============ Zero Fee After Launch Tests ============
418
434
 
419
- /// @notice TrendCoins should have 0 swap fee after the launch fee duration
435
+ /// @notice TrendCoins should have 0.01% swap fee after the launch fee duration
420
436
  /// forge-config: default.isolate = true
421
- function test_trendCoin_zeroFeeAfterLaunchDuration() public {
437
+ function test_trendCoin_minimalFeeAfterLaunchDuration() public {
422
438
  _deployTrendCoin();
423
439
 
424
440
  uint128 amountIn = 100 ether;
@@ -427,7 +443,7 @@ contract TrendCoinTest is BaseTest {
427
443
  // Snapshot at same pool state for both swaps
428
444
  uint256 snapshot = vm.snapshotState();
429
445
 
430
- // Swap right at the end of launch period (still 1% LP fee for non-trend coins, but 0 for trend)
446
+ // Swap right at the end of launch period (0.01% fee for trend coins)
431
447
  vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION);
432
448
  deal(address(zoraToken), trader, amountIn);
433
449
  vm.startPrank(trader);
@@ -446,7 +462,7 @@ contract TrendCoinTest is BaseTest {
446
462
 
447
463
  vm.revertToState(snapshot);
448
464
 
449
- // Swap well after launch period — should yield same amount (both 0 fee)
465
+ // Swap well after launch period — should yield same amount (both 0.01% fee)
450
466
  vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1 days);
451
467
  deal(address(zoraToken), trader, amountIn);
452
468
  vm.startPrank(trader);
@@ -463,8 +479,8 @@ contract TrendCoinTest is BaseTest {
463
479
  vm.stopPrank();
464
480
  uint256 coinsAfterDuration = trendCoin.balanceOf(trader);
465
481
 
466
- // Both should be approximately equal (0% fee in both cases, same pool state)
467
- assertApproxEqRel(coinsAtDuration, coinsAfterDuration, 0.01e18, "both swaps should yield same coins at 0 fee");
482
+ // Both should be approximately equal (0.01% fee in both cases, same pool state)
483
+ assertApproxEqRel(coinsAtDuration, coinsAfterDuration, 0.01e18, "both swaps should yield same coins at 0.01% fee");
468
484
  }
469
485
 
470
486
  /// @notice TrendCoins should still have the launch fee during the launch period
@@ -495,7 +511,7 @@ contract TrendCoinTest is BaseTest {
495
511
 
496
512
  vm.revertToState(snapshot);
497
513
 
498
- // Swap after launch period (0% fee for trend coins)
514
+ // Swap after launch period (0.01% fee for trend coins)
499
515
  vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1);
500
516
  deal(address(zoraToken), trader, amountIn);
501
517
  vm.startPrank(trader);
@@ -512,7 +528,7 @@ contract TrendCoinTest is BaseTest {
512
528
  vm.stopPrank();
513
529
  uint256 coinsPostLaunch = trendCoin.balanceOf(trader);
514
530
 
515
- // Post-launch (0% fee) should yield significantly more coins than during launch (~99% fee)
531
+ // Post-launch (0.01% fee) should yield significantly more coins than during launch (~99% fee)
516
532
  assertGt(coinsPostLaunch, coinsAtLaunch, "should receive more coins after launch fee ends");
517
533
  }
518
534
 
@@ -635,82 +651,37 @@ contract TrendCoinTest is BaseTest {
635
651
  factory.deployTrendCoin("TeSt", address(0), "");
636
652
  }
637
653
 
638
- // ============ URI Encoding Tests ============
639
-
640
- function test_deployTrendCoin_uriEncoding_singleSpace() public {
641
- string memory symbol = "TEST COIN";
642
- (address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
643
-
644
- trendCoin = TrendCoin(coinAddress);
645
- string memory uri = trendCoin.tokenURI();
646
-
647
- // URI should have space converted to +
648
- assertTrue(keccak256(bytes(uri)) == keccak256(bytes("https://trends.theme.wtf/trend/TEST+COIN")), "URI should have space converted to +");
649
- }
650
-
651
- function test_deployTrendCoin_uriEncoding_multipleSpaces() public {
652
- string memory symbol = "TEST COIN 123";
653
- (address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
654
-
655
- trendCoin = TrendCoin(coinAddress);
656
- string memory uri = trendCoin.tokenURI();
657
-
658
- // URI should have all spaces converted to +
659
- assertTrue(keccak256(bytes(uri)) == keccak256(bytes("https://trends.theme.wtf/trend/TEST+COIN+123")), "URI should have all spaces converted to +");
660
- }
654
+ // ============ URI Tests ============
661
655
 
662
- function test_deployTrendCoin_uriEncoding_consecutiveSpaces() public {
663
- string memory symbol = "TEST COIN";
656
+ function test_deployTrendCoin_uri_usesSymbolDirectly() public {
657
+ string memory symbol = "TESTCOIN";
664
658
  (address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
665
659
 
666
660
  trendCoin = TrendCoin(coinAddress);
667
661
  string memory uri = trendCoin.tokenURI();
668
662
 
669
- // URI should have consecutive spaces converted to consecutive +
670
- assertTrue(
671
- keccak256(bytes(uri)) == keccak256(bytes("https://trends.theme.wtf/trend/TEST++COIN")),
672
- "URI should have consecutive spaces converted to consecutive +"
673
- );
663
+ assertEq(uri, "https://trends.theme.wtf/trend/TESTCOIN", "URI should use symbol directly");
674
664
  }
675
665
 
676
- function test_deployTrendCoin_uriEncoding_leadingTrailingSpaces() public {
677
- string memory symbol = " TEST ";
666
+ function test_deployTrendCoin_uri_preservesCase() public {
667
+ string memory symbol = "TestCoin";
678
668
  (address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
679
669
 
680
670
  trendCoin = TrendCoin(coinAddress);
681
671
  string memory uri = trendCoin.tokenURI();
682
672
 
683
- // URI should have leading and trailing spaces converted to +
684
- assertTrue(
685
- keccak256(bytes(uri)) == keccak256(bytes("https://trends.theme.wtf/trend/+TEST+")),
686
- "URI should have leading and trailing spaces converted to +"
687
- );
673
+ assertEq(uri, "https://trends.theme.wtf/trend/TestCoin", "URI should preserve original case");
688
674
  }
689
675
 
690
- function test_deployTrendCoin_uriEncoding_noSpaces_unchanged() public {
676
+ function test_deployTrendCoin_uri_symbolAndNameMatch() public {
691
677
  string memory symbol = "TESTCOIN";
692
678
  (address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
693
679
 
694
- trendCoin = TrendCoin(coinAddress);
695
- string memory uri = trendCoin.tokenURI();
696
-
697
- // URI should be unchanged when no spaces
698
- assertTrue(keccak256(bytes(uri)) == keccak256(bytes("https://trends.theme.wtf/trend/TESTCOIN")), "URI should be unchanged when symbol has no spaces");
699
- }
700
-
701
- function test_deployTrendCoin_uriEncoding_preservesSymbol() public {
702
- string memory symbol = "TEST COIN";
703
- (address coinAddress, ) = factory.deployTrendCoin(symbol, address(0), "");
704
-
705
680
  trendCoin = TrendCoin(coinAddress);
706
681
 
707
- // Symbol should remain unchanged (only URI is encoded)
708
- assertEq(trendCoin.symbol(), symbol, "Symbol should remain unchanged");
709
- assertEq(trendCoin.name(), symbol, "Name should remain unchanged");
710
-
711
- // URI should have space converted to +
712
- string memory uri = trendCoin.tokenURI();
713
- assertTrue(keccak256(bytes(uri)) == keccak256(bytes("https://trends.theme.wtf/trend/TEST+COIN")), "URI should have space converted to +");
682
+ assertEq(trendCoin.symbol(), symbol, "Symbol should match");
683
+ assertEq(trendCoin.name(), symbol, "Name should equal symbol for trend coins");
684
+ assertEq(trendCoin.tokenURI(), string.concat("https://trends.theme.wtf/trend/", symbol), "URI should be base URI + symbol");
714
685
  }
715
686
 
716
687
  // ============ Metadata Manager Tests ============
@@ -1075,3 +1046,83 @@ contract TrendCoinTest is BaseTest {
1075
1046
  );
1076
1047
  }
1077
1048
  }
1049
+
1050
+ /// @notice Tests that exercise the real post-launch 0.01% fee path for trend coins.
1051
+ /// By deploying the coin in setUp(), the deployment transaction completes and transient
1052
+ /// storage (isDeploying) is cleared before any test function runs. This lets
1053
+ /// _calculateLaunchFee reach the TREND_LP_FEE_V4 branch instead of the isDeploying bypass.
1054
+ contract TrendCoinFeeTest is BaseTest {
1055
+ TrendCoin internal trendCoin;
1056
+
1057
+ function setUp() public override {
1058
+ super.setUpNonForked();
1059
+
1060
+ // Deploy in setUp so transient storage is cleared before tests run
1061
+ (address coinAddress, ) = factory.deployTrendCoin("FEETEST", address(0), "");
1062
+ trendCoin = TrendCoin(coinAddress);
1063
+ vm.label(address(trendCoin), "TEST_TREND_COIN");
1064
+ }
1065
+
1066
+ /// @notice Verify that trend coins charge 0.01% after the launch fee window
1067
+ function test_trendCoin_postLaunchFeeIs1Bps() public {
1068
+ // Warp past the launch fee period
1069
+ vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1);
1070
+
1071
+ uint128 tradeAmount = 1_000_000 ether;
1072
+
1073
+ address protocolRecipient = trendCoin.protocolRewardRecipient();
1074
+ uint256 protocolBefore = zoraToken.balanceOf(protocolRecipient);
1075
+
1076
+ deal(address(zoraToken), users.buyer, tradeAmount);
1077
+ vm.startPrank(users.buyer);
1078
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), tradeAmount, uint48(block.timestamp + 1 days));
1079
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
1080
+ address(zoraToken),
1081
+ tradeAmount,
1082
+ address(trendCoin),
1083
+ 0,
1084
+ trendCoin.getPoolKey(),
1085
+ bytes("")
1086
+ );
1087
+ router.execute(commands, inputs, block.timestamp + 1 days);
1088
+ vm.stopPrank();
1089
+
1090
+ uint256 protocolAfter = zoraToken.balanceOf(protocolRecipient);
1091
+ uint256 protocolReward = protocolAfter - protocolBefore;
1092
+
1093
+ // 0.01% of 1M = 100 ZORA. Allow 10% tolerance for swap path conversion.
1094
+ uint256 expectedFee = (uint256(tradeAmount) * CoinConstants.TREND_LP_FEE_V4) / 1_000_000;
1095
+ assertApproxEqRel(protocolReward, expectedFee, 0.10e18, "Protocol reward should be ~0.01% of trade");
1096
+
1097
+ // Sanity: must be far below what 1% would yield (10,000 ZORA)
1098
+ uint256 onePercentFee = (uint256(tradeAmount) * CoinConstants.LP_FEE_V4) / 1_000_000;
1099
+ assertLt(protocolReward, onePercentFee / 5, "Fee must be well below the 1% deployment-bypass level");
1100
+ }
1101
+
1102
+ /// @notice Smoke test that a post-launch trend coin swap still succeeds on the 1 bp fee path
1103
+ function test_trendCoin_postLaunchSwapSucceeds() public {
1104
+ vm.warp(block.timestamp + CoinConstants.LAUNCH_FEE_DURATION + 1);
1105
+
1106
+ uint128 amountIn = 100 ether;
1107
+ address trader = makeAddr("trader");
1108
+
1109
+ // Execute a trend coin swap
1110
+ deal(address(zoraToken), trader, amountIn);
1111
+ vm.startPrank(trader);
1112
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(zoraToken), amountIn, uint48(block.timestamp + 1 days));
1113
+ (bytes memory commands, bytes[] memory inputs) = UniV4SwapHelper.buildExactInputSingleSwapCommand(
1114
+ address(zoraToken),
1115
+ amountIn,
1116
+ address(trendCoin),
1117
+ 0,
1118
+ trendCoin.getPoolKey(),
1119
+ bytes("")
1120
+ );
1121
+ router.execute(commands, inputs, block.timestamp + 1 days);
1122
+ vm.stopPrank();
1123
+
1124
+ uint256 trendCoinsReceived = trendCoin.balanceOf(trader);
1125
+
1126
+ assertGt(trendCoinsReceived, 0, "Post-launch trend swap should return coins");
1127
+ }
1128
+ }
@@ -14,7 +14,7 @@ import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientSta
14
14
  import {CoinCommon} from "../../src/libs/CoinCommon.sol";
15
15
  import {V4Liquidity} from "../../src/libs/V4Liquidity.sol";
16
16
  import {BaseHook} from "@uniswap/v4-periphery/src/utils/BaseHook.sol";
17
- import {ICoin, IHasSwapPath} from "../../src/interfaces/ICoin.sol";
17
+ import {ICoin, IHasSwapPath, IHasCoinType} from "../../src/interfaces/ICoin.sol";
18
18
  import {UniV4SwapToCurrency} from "../../src/libs/UniV4SwapToCurrency.sol";
19
19
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
20
20
  import {CoinRewardsV4} from "../../src/libs/CoinRewardsV4.sol";
@@ -71,7 +71,8 @@ contract FeeEstimatorHook is ZoraV4CoinHook {
71
71
 
72
72
  (fee0, fee1) = V4Liquidity.collectFees(poolManager, key, poolCoins[poolKeyHash].positions);
73
73
 
74
- (uint128 remainingFee0, uint128 remainingFee1) = CoinRewardsV4.mintLpReward(poolManager, key, fee0, fee1);
74
+ IHasCoinType.CoinType coinType = CoinRewardsV4.getCoinType(IHasRewardsRecipients(coin));
75
+ (uint128 remainingFee0, uint128 remainingFee1) = CoinRewardsV4.mintLpReward(poolManager, key, fee0, fee1, coinType);
75
76
 
76
77
  // Execute the swap path to estimate the payout amount, but don't distribute
77
78
  // This mirrors the logic in ZoraV4CoinHook._afterSwap