@croptop/core-v6 0.0.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/SKILLS.md +88 -0
  4. package/deployments/croptop-core-v5/arbitrum/CTDeployer.json +1896 -0
  5. package/deployments/croptop-core-v5/arbitrum/CTProjectOwner.json +186 -0
  6. package/deployments/croptop-core-v5/arbitrum/CTPublisher.json +738 -0
  7. package/deployments/croptop-core-v5/arbitrum_sepolia/CTDeployer.json +1883 -0
  8. package/deployments/croptop-core-v5/arbitrum_sepolia/CTProjectOwner.json +186 -0
  9. package/deployments/croptop-core-v5/arbitrum_sepolia/CTPublisher.json +738 -0
  10. package/deployments/croptop-core-v5/base/CTDeployer.json +1908 -0
  11. package/deployments/croptop-core-v5/base/CTProjectOwner.json +190 -0
  12. package/deployments/croptop-core-v5/base/CTPublisher.json +741 -0
  13. package/deployments/croptop-core-v5/base_sepolia/CTDeployer.json +1894 -0
  14. package/deployments/croptop-core-v5/base_sepolia/CTProjectOwner.json +190 -0
  15. package/deployments/croptop-core-v5/base_sepolia/CTPublisher.json +741 -0
  16. package/deployments/croptop-core-v5/ethereum/CTDeployer.json +1894 -0
  17. package/deployments/croptop-core-v5/ethereum/CTProjectOwner.json +190 -0
  18. package/deployments/croptop-core-v5/ethereum/CTPublisher.json +741 -0
  19. package/deployments/croptop-core-v5/optimism/CTDeployer.json +1894 -0
  20. package/deployments/croptop-core-v5/optimism/CTProjectOwner.json +190 -0
  21. package/deployments/croptop-core-v5/optimism/CTPublisher.json +741 -0
  22. package/deployments/croptop-core-v5/optimism_sepolia/CTDeployer.json +1894 -0
  23. package/deployments/croptop-core-v5/optimism_sepolia/CTProjectOwner.json +190 -0
  24. package/deployments/croptop-core-v5/optimism_sepolia/CTPublisher.json +741 -0
  25. package/deployments/croptop-core-v5/sepolia/CTDeployer.json +1894 -0
  26. package/deployments/croptop-core-v5/sepolia/CTProjectOwner.json +190 -0
  27. package/deployments/croptop-core-v5/sepolia/CTPublisher.json +741 -0
  28. package/foundry.toml +25 -0
  29. package/package.json +31 -0
  30. package/remappings.txt +2 -0
  31. package/script/ConfigureFeeProject.s.sol +386 -0
  32. package/script/Deploy.s.sol +138 -0
  33. package/script/helpers/CroptopDeploymentLib.sol +75 -0
  34. package/slither-ci.config.json +10 -0
  35. package/sphinx.lock +507 -0
  36. package/src/CTDeployer.sol +425 -0
  37. package/src/CTProjectOwner.sol +78 -0
  38. package/src/CTPublisher.sol +540 -0
  39. package/src/interfaces/ICTDeployer.sol +56 -0
  40. package/src/interfaces/ICTProjectOwner.sol +24 -0
  41. package/src/interfaces/ICTPublisher.sol +91 -0
  42. package/src/structs/CTAllowedPost.sol +22 -0
  43. package/src/structs/CTDeployerAllowedPost.sol +20 -0
  44. package/src/structs/CTPost.sol +22 -0
  45. package/src/structs/CTProjectConfig.sol +22 -0
  46. package/src/structs/CTSuckerDeploymentConfig.sol +11 -0
  47. package/test/CTPublisher.t.sol +672 -0
  48. package/test/CroptopAttacks.t.sol +439 -0
  49. package/test/Fork.t.sol +114 -0
  50. package/test/Test_MetadataGeneration.t.sol +70 -0
@@ -0,0 +1,672 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "forge-std/Test.sol";
5
+
6
+ import {IJBPermissions} from "@bananapus/core-v5/src/interfaces/IJBPermissions.sol";
7
+ import {IJBDirectory} from "@bananapus/core-v5/src/interfaces/IJBDirectory.sol";
8
+ import {IJBSplitHook} from "@bananapus/core-v5/src/interfaces/IJBSplitHook.sol";
9
+ import {IJBOwnable} from "@bananapus/ownable-v5/src/interfaces/IJBOwnable.sol";
10
+ import {IJB721TiersHook} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHook.sol";
11
+ import {IJB721TiersHookStore} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHookStore.sol";
12
+ import {IJB721Hook} from "@bananapus/721-hook-v5/src/interfaces/IJB721Hook.sol";
13
+ import {JBConstants} from "@bananapus/core-v5/src/libraries/JBConstants.sol";
14
+ import {JBSplit} from "@bananapus/core-v5/src/structs/JBSplit.sol";
15
+ import {JBPermissionIds} from "@bananapus/permission-ids-v5/src/JBPermissionIds.sol";
16
+
17
+ import {CTPublisher} from "../src/CTPublisher.sol";
18
+ import {CTAllowedPost} from "../src/structs/CTAllowedPost.sol";
19
+ import {CTPost} from "../src/structs/CTPost.sol";
20
+
21
+ /// @notice Unit tests for CTPublisher.
22
+ contract TestCTPublisher is Test {
23
+ CTPublisher publisher;
24
+
25
+ IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
26
+ IJBDirectory directory = IJBDirectory(makeAddr("directory"));
27
+
28
+ address hookOwner = makeAddr("hookOwner");
29
+ address hookAddr = makeAddr("hook");
30
+ address hookStoreAddr = makeAddr("hookStore");
31
+ address poster = makeAddr("poster");
32
+ address unauthorized = makeAddr("unauthorized");
33
+
34
+ uint256 feeProjectId = 1;
35
+ uint256 hookProjectId = 42;
36
+
37
+ function setUp() public {
38
+ publisher = new CTPublisher(directory, permissions, feeProjectId, address(0));
39
+
40
+ // Mock hook.owner() for permission checks.
41
+ vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(hookOwner));
42
+
43
+ // Mock hook.PROJECT_ID() for permission checks.
44
+ vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(hookProjectId));
45
+
46
+ // Mock hook.STORE().
47
+ vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.STORE.selector), abi.encode(hookStoreAddr));
48
+
49
+ // Mock permissions to return true by default.
50
+ vm.mockCall(
51
+ address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
52
+ );
53
+
54
+ // Fund poster.
55
+ vm.deal(poster, 100 ether);
56
+ }
57
+
58
+ //*********************************************************************//
59
+ // --- Constructor --------------------------------------------------- //
60
+ //*********************************************************************//
61
+
62
+ function test_constructor() public {
63
+ assertEq(address(publisher.DIRECTORY()), address(directory));
64
+ assertEq(publisher.FEE_PROJECT_ID(), feeProjectId);
65
+ assertEq(publisher.FEE_DIVISOR(), 20);
66
+ }
67
+
68
+ //*********************************************************************//
69
+ // --- configurePostingCriteriaFor + allowanceFor Round-Trip ---------- //
70
+ //*********************************************************************//
71
+
72
+ function test_configureAndReadAllowance() public {
73
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
74
+ posts[0] = CTAllowedPost({
75
+ hook: hookAddr,
76
+ category: 5,
77
+ minimumPrice: 0.01 ether,
78
+ minimumTotalSupply: 10,
79
+ maximumTotalSupply: 1000,
80
+ maximumSplitPercent: 0,
81
+ allowedAddresses: new address[](0)
82
+ });
83
+
84
+ vm.prank(hookOwner);
85
+ publisher.configurePostingCriteriaFor(posts);
86
+
87
+ (uint256 minPrice, uint256 minSupply, uint256 maxSupply, uint256 maxSplit, address[] memory allowed) =
88
+ publisher.allowanceFor(hookAddr, 5);
89
+
90
+ assertEq(minPrice, 0.01 ether, "minimum price should match");
91
+ assertEq(minSupply, 10, "minimum supply should match");
92
+ assertEq(maxSupply, 1000, "maximum supply should match");
93
+ assertEq(maxSplit, 0, "maximum split percent should be zero");
94
+ assertEq(allowed.length, 0, "no allowlist");
95
+ }
96
+
97
+ function test_configureWithAllowlist() public {
98
+ address[] memory allowList = new address[](2);
99
+ allowList[0] = poster;
100
+ allowList[1] = hookOwner;
101
+
102
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
103
+ posts[0] = CTAllowedPost({
104
+ hook: hookAddr,
105
+ category: 3,
106
+ minimumPrice: 0,
107
+ minimumTotalSupply: 1,
108
+ maximumTotalSupply: 100,
109
+ maximumSplitPercent: 0,
110
+ allowedAddresses: allowList
111
+ });
112
+
113
+ vm.prank(hookOwner);
114
+ publisher.configurePostingCriteriaFor(posts);
115
+
116
+ (,,,, address[] memory allowed) = publisher.allowanceFor(hookAddr, 3);
117
+ assertEq(allowed.length, 2, "should have 2 allowed addresses");
118
+ assertEq(allowed[0], poster);
119
+ assertEq(allowed[1], hookOwner);
120
+ }
121
+
122
+ function test_configureMultipleCategories() public {
123
+ CTAllowedPost[] memory posts = new CTAllowedPost[](2);
124
+ posts[0] = CTAllowedPost({
125
+ hook: hookAddr,
126
+ category: 1,
127
+ minimumPrice: 100,
128
+ minimumTotalSupply: 5,
129
+ maximumTotalSupply: 50,
130
+ maximumSplitPercent: 0,
131
+ allowedAddresses: new address[](0)
132
+ });
133
+ posts[1] = CTAllowedPost({
134
+ hook: hookAddr,
135
+ category: 2,
136
+ minimumPrice: 200,
137
+ minimumTotalSupply: 10,
138
+ maximumTotalSupply: 100,
139
+ maximumSplitPercent: 0,
140
+ allowedAddresses: new address[](0)
141
+ });
142
+
143
+ vm.prank(hookOwner);
144
+ publisher.configurePostingCriteriaFor(posts);
145
+
146
+ (uint256 minPrice1, uint256 minSupply1, uint256 maxSupply1,,) = publisher.allowanceFor(hookAddr, 1);
147
+ assertEq(minPrice1, 100);
148
+ assertEq(minSupply1, 5);
149
+ assertEq(maxSupply1, 50);
150
+
151
+ (uint256 minPrice2, uint256 minSupply2, uint256 maxSupply2,,) = publisher.allowanceFor(hookAddr, 2);
152
+ assertEq(minPrice2, 200);
153
+ assertEq(minSupply2, 10);
154
+ assertEq(maxSupply2, 100);
155
+ }
156
+
157
+ //*********************************************************************//
158
+ // --- configurePostingCriteriaFor: Bit Packing Fuzz ----------------- //
159
+ //*********************************************************************//
160
+
161
+ function testFuzz_allowanceBitPacking(
162
+ uint104 minPrice,
163
+ uint32 minSupply,
164
+ uint32 maxSupply,
165
+ uint32 maxSplitPercent
166
+ )
167
+ public
168
+ {
169
+ vm.assume(minSupply > 0);
170
+ vm.assume(maxSupply >= minSupply);
171
+
172
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
173
+ posts[0] = CTAllowedPost({
174
+ hook: hookAddr,
175
+ category: 0,
176
+ minimumPrice: minPrice,
177
+ minimumTotalSupply: minSupply,
178
+ maximumTotalSupply: maxSupply,
179
+ maximumSplitPercent: maxSplitPercent,
180
+ allowedAddresses: new address[](0)
181
+ });
182
+
183
+ vm.prank(hookOwner);
184
+ publisher.configurePostingCriteriaFor(posts);
185
+
186
+ (uint256 readPrice, uint256 readMinSupply, uint256 readMaxSupply, uint256 readMaxSplit,) =
187
+ publisher.allowanceFor(hookAddr, 0);
188
+ assertEq(readPrice, uint256(minPrice), "price round-trip");
189
+ assertEq(readMinSupply, uint256(minSupply), "min supply round-trip");
190
+ assertEq(readMaxSupply, uint256(maxSupply), "max supply round-trip");
191
+ assertEq(readMaxSplit, uint256(maxSplitPercent), "max split percent round-trip");
192
+ }
193
+
194
+ //*********************************************************************//
195
+ // --- configurePostingCriteriaFor: Validation Errors ----------------- //
196
+ //*********************************************************************//
197
+
198
+ function test_configureReverts_zeroMinSupply() public {
199
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
200
+ posts[0] = CTAllowedPost({
201
+ hook: hookAddr,
202
+ category: 1,
203
+ minimumPrice: 0,
204
+ minimumTotalSupply: 0,
205
+ maximumTotalSupply: 100,
206
+ maximumSplitPercent: 0,
207
+ allowedAddresses: new address[](0)
208
+ });
209
+
210
+ vm.prank(hookOwner);
211
+ vm.expectRevert(CTPublisher.CTPublisher_ZeroTotalSupply.selector);
212
+ publisher.configurePostingCriteriaFor(posts);
213
+ }
214
+
215
+ function test_configureReverts_minGreaterThanMax() public {
216
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
217
+ posts[0] = CTAllowedPost({
218
+ hook: hookAddr,
219
+ category: 1,
220
+ minimumPrice: 0,
221
+ minimumTotalSupply: 100,
222
+ maximumTotalSupply: 50,
223
+ maximumSplitPercent: 0,
224
+ allowedAddresses: new address[](0)
225
+ });
226
+
227
+ vm.prank(hookOwner);
228
+ vm.expectRevert(abi.encodeWithSelector(CTPublisher.CTPublisher_MaxTotalSupplyLessThanMin.selector, 100, 50));
229
+ publisher.configurePostingCriteriaFor(posts);
230
+ }
231
+
232
+ //*********************************************************************//
233
+ // --- configurePostingCriteriaFor: Permission Checks ----------------- //
234
+ //*********************************************************************//
235
+
236
+ function test_configureReverts_ifUnauthorized() public {
237
+ vm.mockCall(
238
+ address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false)
239
+ );
240
+
241
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
242
+ posts[0] = CTAllowedPost({
243
+ hook: hookAddr,
244
+ category: 1,
245
+ minimumPrice: 0,
246
+ minimumTotalSupply: 1,
247
+ maximumTotalSupply: 100,
248
+ maximumSplitPercent: 0,
249
+ allowedAddresses: new address[](0)
250
+ });
251
+
252
+ vm.prank(unauthorized);
253
+ vm.expectRevert();
254
+ publisher.configurePostingCriteriaFor(posts);
255
+ }
256
+
257
+ //*********************************************************************//
258
+ // --- configurePostingCriteriaFor: Overwrite Previous Config --------- //
259
+ //*********************************************************************//
260
+
261
+ function test_configureOverwritesPrevious() public {
262
+ CTAllowedPost[] memory posts1 = new CTAllowedPost[](1);
263
+ posts1[0] = CTAllowedPost({
264
+ hook: hookAddr,
265
+ category: 1,
266
+ minimumPrice: 100,
267
+ minimumTotalSupply: 10,
268
+ maximumTotalSupply: 50,
269
+ maximumSplitPercent: 500_000_000,
270
+ allowedAddresses: new address[](0)
271
+ });
272
+ vm.prank(hookOwner);
273
+ publisher.configurePostingCriteriaFor(posts1);
274
+
275
+ CTAllowedPost[] memory posts2 = new CTAllowedPost[](1);
276
+ posts2[0] = CTAllowedPost({
277
+ hook: hookAddr,
278
+ category: 1,
279
+ minimumPrice: 999,
280
+ minimumTotalSupply: 1,
281
+ maximumTotalSupply: 9999,
282
+ maximumSplitPercent: 1_000_000_000,
283
+ allowedAddresses: new address[](0)
284
+ });
285
+ vm.prank(hookOwner);
286
+ publisher.configurePostingCriteriaFor(posts2);
287
+
288
+ (uint256 minPrice, uint256 minSupply, uint256 maxSupply, uint256 maxSplit,) =
289
+ publisher.allowanceFor(hookAddr, 1);
290
+ assertEq(minPrice, 999, "price should be overwritten");
291
+ assertEq(minSupply, 1, "min supply should be overwritten");
292
+ assertEq(maxSupply, 9999, "max supply should be overwritten");
293
+ assertEq(maxSplit, 1_000_000_000, "max split should be overwritten");
294
+ }
295
+
296
+ //*********************************************************************//
297
+ // --- allowanceFor: Unconfigured Category --------------------------- //
298
+ //*********************************************************************//
299
+
300
+ function test_allowanceFor_unconfiguredReturnsZero() public {
301
+ (uint256 minPrice, uint256 minSupply, uint256 maxSupply, uint256 maxSplit, address[] memory allowed) =
302
+ publisher.allowanceFor(hookAddr, 999);
303
+
304
+ assertEq(minPrice, 0);
305
+ assertEq(minSupply, 0);
306
+ assertEq(maxSupply, 0);
307
+ assertEq(maxSplit, 0);
308
+ assertEq(allowed.length, 0);
309
+ }
310
+
311
+ //*********************************************************************//
312
+ // --- tierIdForEncodedIPFSUriOf ------------------------------------- //
313
+ //*********************************************************************//
314
+
315
+ function test_tierIdForEncodedIPFSUriOf_returnsZeroByDefault() public {
316
+ bytes32 uri = keccak256("test");
317
+ assertEq(publisher.tierIdForEncodedIPFSUriOf(hookAddr, uri), 0);
318
+ }
319
+
320
+ //*********************************************************************//
321
+ // --- Split Configuration Round-Trip -------------------------------- //
322
+ //*********************************************************************//
323
+
324
+ function test_configureWithMaxSplitPercent() public {
325
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
326
+ posts[0] = CTAllowedPost({
327
+ hook: hookAddr,
328
+ category: 5,
329
+ minimumPrice: 0.01 ether,
330
+ minimumTotalSupply: 1,
331
+ maximumTotalSupply: 100,
332
+ maximumSplitPercent: 500_000_000,
333
+ allowedAddresses: new address[](0)
334
+ });
335
+
336
+ vm.prank(hookOwner);
337
+ publisher.configurePostingCriteriaFor(posts);
338
+
339
+ (,,, uint256 maxSplit,) = publisher.allowanceFor(hookAddr, 5);
340
+ assertEq(maxSplit, 500_000_000, "max split percent should be 50%");
341
+ }
342
+
343
+ function test_configureMaxSplitPercent_fullRange() public {
344
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
345
+ posts[0] = CTAllowedPost({
346
+ hook: hookAddr,
347
+ category: 5,
348
+ minimumPrice: 0,
349
+ minimumTotalSupply: 1,
350
+ maximumTotalSupply: 100,
351
+ maximumSplitPercent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
352
+ allowedAddresses: new address[](0)
353
+ });
354
+
355
+ vm.prank(hookOwner);
356
+ publisher.configurePostingCriteriaFor(posts);
357
+
358
+ (,,, uint256 maxSplit,) = publisher.allowanceFor(hookAddr, 5);
359
+ assertEq(maxSplit, JBConstants.SPLITS_TOTAL_PERCENT, "max split should be 100%");
360
+ }
361
+
362
+ //*********************************************************************//
363
+ // --- Split Percent Validation in mintFrom -------------------------- //
364
+ //*********************************************************************//
365
+
366
+ /// @dev Helper to configure a category with a maximum split percent.
367
+ function _configureCategoryWithSplits(
368
+ uint24 category,
369
+ uint104 minPrice,
370
+ uint32 minSupply,
371
+ uint32 maxSupply,
372
+ uint32 maxSplitPercent
373
+ )
374
+ internal
375
+ {
376
+ CTAllowedPost[] memory posts = new CTAllowedPost[](1);
377
+ posts[0] = CTAllowedPost({
378
+ hook: hookAddr,
379
+ category: category,
380
+ minimumPrice: minPrice,
381
+ minimumTotalSupply: minSupply,
382
+ maximumTotalSupply: maxSupply,
383
+ maximumSplitPercent: maxSplitPercent,
384
+ allowedAddresses: new address[](0)
385
+ });
386
+
387
+ vm.prank(hookOwner);
388
+ publisher.configurePostingCriteriaFor(posts);
389
+ }
390
+
391
+ /// @dev Set up mocks for a successful mintFrom path (up to adjustTiers call).
392
+ function _setupMintMocks() internal {
393
+ vm.mockCall(
394
+ hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.maxTierIdOf.selector), abi.encode(uint256(0))
395
+ );
396
+ vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.adjustTiers.selector), abi.encode());
397
+ // METADATA_ID_TARGET() selector.
398
+ vm.mockCall(hookAddr, abi.encodeWithSelector(bytes4(keccak256("METADATA_ID_TARGET()"))), abi.encode(address(0)));
399
+ vm.mockCall(
400
+ address(directory),
401
+ abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector),
402
+ abi.encode(makeAddr("terminal"))
403
+ );
404
+ // Mock terminal.pay() — use a broad mock for the terminal address.
405
+ vm.mockCall(makeAddr("terminal"), "", abi.encode(uint256(0)));
406
+ }
407
+
408
+ function test_mintFrom_splitPercentExceedsLimit_reverts() public {
409
+ _configureCategoryWithSplits(5, 0.01 ether, 1, 100, 500_000_000);
410
+ _setupMintMocks();
411
+
412
+ CTPost[] memory posts = new CTPost[](1);
413
+ posts[0] = CTPost({
414
+ encodedIPFSUri: keccak256("greedy-split"),
415
+ totalSupply: 10,
416
+ price: 0.1 ether,
417
+ category: 5,
418
+ splitPercent: 600_000_000, // 60% exceeds 50% maximum!
419
+ splits: new JBSplit[](0)
420
+ });
421
+
422
+ vm.prank(poster);
423
+ vm.expectRevert(
424
+ abi.encodeWithSelector(
425
+ CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 600_000_000, 500_000_000
426
+ )
427
+ );
428
+ publisher.mintFrom{value: 0.2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
429
+ }
430
+
431
+ function test_mintFrom_splitPercentExactlyAtLimit_succeeds() public {
432
+ _configureCategoryWithSplits(5, 0.01 ether, 1, 100, 500_000_000);
433
+ _setupMintMocks();
434
+
435
+ CTPost[] memory posts = new CTPost[](1);
436
+ posts[0] = CTPost({
437
+ encodedIPFSUri: keccak256("exact-split"),
438
+ totalSupply: 10,
439
+ price: 0.1 ether,
440
+ category: 5,
441
+ splitPercent: 500_000_000, // Exactly at 50% limit.
442
+ splits: new JBSplit[](0)
443
+ });
444
+
445
+ // Should pass validation. May revert downstream in mock, but NOT with split percent error.
446
+ vm.prank(poster);
447
+ try publisher.mintFrom{value: 0.2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "") {}
448
+ catch (bytes memory reason) {
449
+ assertTrue(
450
+ keccak256(reason)
451
+ != keccak256(
452
+ abi.encodeWithSelector(
453
+ CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 500_000_000, 500_000_000
454
+ )
455
+ ),
456
+ "should not revert with split percent error"
457
+ );
458
+ }
459
+ }
460
+
461
+ function test_mintFrom_zeroSplitPercent_alwaysAllowed() public {
462
+ // Configure with zero max split (splits disabled).
463
+ _configureCategoryWithSplits(5, 0.01 ether, 1, 100, 0);
464
+ _setupMintMocks();
465
+
466
+ CTPost[] memory posts = new CTPost[](1);
467
+ posts[0] = CTPost({
468
+ encodedIPFSUri: keccak256("no-split"),
469
+ totalSupply: 10,
470
+ price: 0.1 ether,
471
+ category: 5,
472
+ splitPercent: 0,
473
+ splits: new JBSplit[](0)
474
+ });
475
+
476
+ // splitPercent=0 should always be allowed (0 <= 0).
477
+ vm.prank(poster);
478
+ try publisher.mintFrom{value: 0.2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "") {}
479
+ catch (bytes memory reason) {
480
+ assertTrue(
481
+ keccak256(reason)
482
+ != keccak256(
483
+ abi.encodeWithSelector(CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 0, 0)
484
+ ),
485
+ "should not revert with split percent error"
486
+ );
487
+ }
488
+ }
489
+
490
+ function test_mintFrom_nonzeroSplitPercent_whenDisabled_reverts() public {
491
+ // Configure with zero max split (splits disabled).
492
+ _configureCategoryWithSplits(5, 0.01 ether, 1, 100, 0);
493
+ _setupMintMocks();
494
+
495
+ CTPost[] memory posts = new CTPost[](1);
496
+ posts[0] = CTPost({
497
+ encodedIPFSUri: keccak256("sneaky-split"),
498
+ totalSupply: 10,
499
+ price: 0.1 ether,
500
+ category: 5,
501
+ splitPercent: 1, // Even 1 should fail when disabled.
502
+ splits: new JBSplit[](0)
503
+ });
504
+
505
+ vm.prank(poster);
506
+ vm.expectRevert(abi.encodeWithSelector(CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 1, 0));
507
+ publisher.mintFrom{value: 0.2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
508
+ }
509
+
510
+ function test_mintFrom_splitPercentWithinLimit_passesValidation() public {
511
+ _configureCategoryWithSplits(5, 0.01 ether, 1, 100, 500_000_000);
512
+ _setupMintMocks();
513
+
514
+ JBSplit[] memory splits = new JBSplit[](1);
515
+ splits[0] = JBSplit({
516
+ percent: 250_000_000,
517
+ projectId: 0,
518
+ beneficiary: payable(poster),
519
+ preferAddToBalance: false,
520
+ lockedUntil: 0,
521
+ hook: IJBSplitHook(address(0))
522
+ });
523
+
524
+ CTPost[] memory posts = new CTPost[](1);
525
+ posts[0] = CTPost({
526
+ encodedIPFSUri: keccak256("split-content"),
527
+ totalSupply: 10,
528
+ price: 0.1 ether,
529
+ category: 5,
530
+ splitPercent: 250_000_000, // 25% within 50% limit.
531
+ splits: splits
532
+ });
533
+
534
+ // Should pass split validation.
535
+ vm.prank(poster);
536
+ try publisher.mintFrom{value: 0.2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "") {}
537
+ catch (bytes memory reason) {
538
+ assertTrue(
539
+ keccak256(reason)
540
+ != keccak256(
541
+ abi.encodeWithSelector(
542
+ CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 250_000_000, 500_000_000
543
+ )
544
+ ),
545
+ "should not revert with split percent error"
546
+ );
547
+ }
548
+ }
549
+
550
+ //*********************************************************************//
551
+ // --- Split Percent Fuzz -------------------------------------------- //
552
+ //*********************************************************************//
553
+
554
+ function testFuzz_splitPercentValidation(uint32 maxSplitPercent, uint32 postSplitPercent) public {
555
+ vm.assume(maxSplitPercent <= uint32(JBConstants.SPLITS_TOTAL_PERCENT));
556
+
557
+ _configureCategoryWithSplits(5, 0, 1, 100, maxSplitPercent);
558
+ _setupMintMocks();
559
+
560
+ CTPost[] memory posts = new CTPost[](1);
561
+ posts[0] = CTPost({
562
+ encodedIPFSUri: keccak256(abi.encode("fuzz", postSplitPercent)),
563
+ totalSupply: 10,
564
+ price: 0.01 ether,
565
+ category: 5,
566
+ splitPercent: postSplitPercent,
567
+ splits: new JBSplit[](0)
568
+ });
569
+
570
+ if (postSplitPercent > maxSplitPercent) {
571
+ vm.prank(poster);
572
+ vm.expectRevert(
573
+ abi.encodeWithSelector(
574
+ CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, postSplitPercent, maxSplitPercent
575
+ )
576
+ );
577
+ publisher.mintFrom{value: 0.02 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
578
+ } else {
579
+ vm.prank(poster);
580
+ try publisher.mintFrom{value: 0.02 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "") {}
581
+ catch (bytes memory reason) {
582
+ assertTrue(
583
+ keccak256(reason)
584
+ != keccak256(
585
+ abi.encodeWithSelector(
586
+ CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector,
587
+ postSplitPercent,
588
+ maxSplitPercent
589
+ )
590
+ ),
591
+ "should not revert with split percent error when within limit"
592
+ );
593
+ }
594
+ }
595
+ }
596
+
597
+ //*********************************************************************//
598
+ // --- Overwrite Split Config ---------------------------------------- //
599
+ //*********************************************************************//
600
+
601
+ function test_configureOverwritesSplitPercent() public {
602
+ CTAllowedPost[] memory posts1 = new CTAllowedPost[](1);
603
+ posts1[0] = CTAllowedPost({
604
+ hook: hookAddr,
605
+ category: 1,
606
+ minimumPrice: 0,
607
+ minimumTotalSupply: 1,
608
+ maximumTotalSupply: 100,
609
+ maximumSplitPercent: 500_000_000,
610
+ allowedAddresses: new address[](0)
611
+ });
612
+ vm.prank(hookOwner);
613
+ publisher.configurePostingCriteriaFor(posts1);
614
+
615
+ (,,, uint256 maxSplit1,) = publisher.allowanceFor(hookAddr, 1);
616
+ assertEq(maxSplit1, 500_000_000);
617
+
618
+ CTAllowedPost[] memory posts2 = new CTAllowedPost[](1);
619
+ posts2[0] = CTAllowedPost({
620
+ hook: hookAddr,
621
+ category: 1,
622
+ minimumPrice: 0,
623
+ minimumTotalSupply: 1,
624
+ maximumTotalSupply: 100,
625
+ maximumSplitPercent: 0,
626
+ allowedAddresses: new address[](0)
627
+ });
628
+ vm.prank(hookOwner);
629
+ publisher.configurePostingCriteriaFor(posts2);
630
+
631
+ (,,, uint256 maxSplit2,) = publisher.allowanceFor(hookAddr, 1);
632
+ assertEq(maxSplit2, 0, "max split should be overwritten to 0");
633
+ }
634
+
635
+ //*********************************************************************//
636
+ // --- Multiple Posts With Different Split Percents ------------------- //
637
+ //*********************************************************************//
638
+
639
+ function test_mintFrom_multiplePostsDifferentSplits() public {
640
+ // Category 5 allows up to 50% splits.
641
+ _configureCategoryWithSplits(5, 0, 1, 100, 500_000_000);
642
+ _setupMintMocks();
643
+
644
+ CTPost[] memory posts = new CTPost[](2);
645
+ // First post: 25% split (within limit).
646
+ posts[0] = CTPost({
647
+ encodedIPFSUri: keccak256("post-1"),
648
+ totalSupply: 10,
649
+ price: 0.1 ether,
650
+ category: 5,
651
+ splitPercent: 250_000_000,
652
+ splits: new JBSplit[](0)
653
+ });
654
+ // Second post: 60% split (exceeds limit).
655
+ posts[1] = CTPost({
656
+ encodedIPFSUri: keccak256("post-2"),
657
+ totalSupply: 10,
658
+ price: 0.1 ether,
659
+ category: 5,
660
+ splitPercent: 600_000_000,
661
+ splits: new JBSplit[](0)
662
+ });
663
+
664
+ vm.prank(poster);
665
+ vm.expectRevert(
666
+ abi.encodeWithSelector(
667
+ CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 600_000_000, 500_000_000
668
+ )
669
+ );
670
+ publisher.mintFrom{value: 0.4 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
671
+ }
672
+ }