@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.
- package/.turbo/turbo-build$colon$js.log +114 -120
- package/CHANGELOG.md +8 -0
- package/abis/ITrendCoin.json +15 -5
- package/abis/ITrendCoinErrors.json +15 -5
- package/abis/IZoraFactory.json +15 -5
- package/abis/TrendCoin.json +16 -6
- package/abis/ZoraFactoryImpl.json +15 -5
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +12 -4
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/foundry.toml +1 -0
- package/package/wagmiGenerated.ts +3 -1
- package/package.json +2 -2
- package/src/TrendCoin.sol +3 -3
- package/src/hooks/ZoraV4CoinHook.sol +6 -4
- package/src/interfaces/ITrendCoinErrors.sol +8 -2
- package/src/libs/CoinConstants.sol +4 -0
- package/src/libs/CoinRewardsV4.sol +15 -2
- package/src/libs/TickerUtils.sol +16 -34
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/CoinRewardsV4.t.sol +48 -0
- package/test/TrendCoin.t.sol +173 -122
- package/test/utils/FeeEstimatorHook.sol +3 -2
package/test/TrendCoin.t.sol
CHANGED
|
@@ -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
|
|
168
|
-
(
|
|
169
|
-
|
|
167
|
+
function test_deployTrendCoin_invalidSymbols_dashOnly() public {
|
|
168
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
169
|
+
factory.deployTrendCoin("--", address(0), "");
|
|
170
170
|
|
|
171
|
-
(
|
|
172
|
-
|
|
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
|
|
179
|
-
(
|
|
180
|
-
|
|
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
|
-
(
|
|
186
|
-
|
|
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("
|
|
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("
|
|
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[](
|
|
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.
|
|
236
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
242
237
|
factory.deployTrendCoin(invalidSymbols.symbol, address(0), "");
|
|
243
238
|
}
|
|
244
239
|
|
|
245
|
-
function
|
|
246
|
-
(
|
|
247
|
-
|
|
240
|
+
function test_deployTrendCoin_invalidSymbols_withSpaces() public {
|
|
241
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
242
|
+
factory.deployTrendCoin("TEST COIN", address(0), "");
|
|
248
243
|
|
|
249
|
-
(
|
|
250
|
-
|
|
244
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
245
|
+
factory.deployTrendCoin(" TEST ", address(0), "");
|
|
251
246
|
|
|
252
|
-
(
|
|
253
|
-
|
|
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
|
|
268
|
-
(
|
|
269
|
-
|
|
262
|
+
function test_deployTrendCoin_invalidSymbols_lettersAndDash() public {
|
|
263
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
264
|
+
factory.deployTrendCoin("TEST-COIN", address(0), "");
|
|
270
265
|
|
|
271
|
-
(
|
|
272
|
-
|
|
266
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
267
|
+
factory.deployTrendCoin("-TEST-", address(0), "");
|
|
273
268
|
|
|
274
|
-
(
|
|
275
|
-
|
|
269
|
+
vm.expectRevert(ITrendCoinErrors.TickerInvalidCharacters.selector);
|
|
270
|
+
factory.deployTrendCoin("TEST--COIN", address(0), "");
|
|
276
271
|
}
|
|
277
272
|
|
|
278
|
-
function
|
|
279
|
-
(address coin1, ) = factory.deployTrendCoin("
|
|
280
|
-
assertTrue(coin1 != address(0), "Coin with all
|
|
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("
|
|
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("
|
|
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
|
|
347
|
-
/// For TrendCoins,
|
|
348
|
-
|
|
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
|
|
369
|
-
// - Total fee: 1% (LP_FEE_V4
|
|
370
|
-
// - LP
|
|
371
|
-
// -
|
|
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
|
-
// -
|
|
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
|
|
383
|
-
assertApproxEqRel(protocolReward,
|
|
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 ~
|
|
386
|
-
assertGt(protocolReward,
|
|
387
|
-
assertLt(protocolReward,
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
663
|
-
string memory symbol = "
|
|
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
|
-
|
|
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
|
|
677
|
-
string memory symbol = "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
708
|
-
assertEq(trendCoin.
|
|
709
|
-
assertEq(trendCoin.
|
|
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
|
-
|
|
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
|