@croptop/core-v6 0.0.42 → 0.0.43

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/README.md CHANGED
@@ -53,7 +53,7 @@ Many Croptop bugs are really deployment-shape bugs or posting-policy bugs, not g
53
53
  1. `test/CTPublisher.t.sol`
54
54
  2. `test/CTDeployer.t.sol`
55
55
  3. `test/ClaimCollectionOwnership.t.sol`
56
- 4. `test/audit/FeeFallbackBlackhole.t.sol`
56
+ 4. `test/regression/FeeFallbackBlackhole.t.sol`
57
57
  5. `test/regression/DuplicateUriFeeEvasion.t.sol`
58
58
 
59
59
  ## Integration Traps
@@ -105,7 +105,7 @@ src/
105
105
  interfaces/
106
106
  structs/
107
107
  test/
108
- publisher, deployer, fork, attack, audit, metadata, and regression coverage
108
+ publisher, deployer, fork, attack, review, metadata, and regression coverage
109
109
  script/
110
110
  Deploy.s.sol
111
111
  ConfigureFeeProject.s.sol
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@croptop/core-v6",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,17 +29,17 @@
29
29
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v6'"
30
30
  },
31
31
  "dependencies": {
32
- "@bananapus/721-hook-v6": "0.0.43",
33
- "@bananapus/core-v6": "0.0.39",
34
- "@bananapus/ownable-v6": "0.0.24",
35
- "@bananapus/permission-ids-v6": "0.0.22",
36
- "@bananapus/router-terminal-v6": "0.0.36",
37
- "@bananapus/suckers-v6": "0.0.33",
32
+ "@bananapus/721-hook-v6": "^0.0.47",
33
+ "@bananapus/core-v6": "^0.0.44",
34
+ "@bananapus/ownable-v6": "^0.0.24",
35
+ "@bananapus/permission-ids-v6": "^0.0.23",
36
+ "@bananapus/router-terminal-v6": "^0.0.37",
37
+ "@bananapus/suckers-v6": "^0.0.37",
38
38
  "@openzeppelin/contracts": "5.6.1",
39
- "@rev-net/core-v6": "0.0.37"
39
+ "@rev-net/core-v6": "^0.0.45"
40
40
  },
41
41
  "devDependencies": {
42
- "@bananapus/address-registry-v6": "0.0.25",
42
+ "@bananapus/address-registry-v6": "^0.0.25",
43
43
  "@sphinx-labs/plugins": "0.33.3"
44
44
  }
45
45
  }
@@ -24,4 +24,4 @@
24
24
 
25
25
  - [`test/CTPublisher.t.sol`](../test/CTPublisher.t.sol) and [`test/Test_MetadataGeneration.t.sol`](../test/Test_MetadataGeneration.t.sol) for content and metadata behavior.
26
26
  - [`test/CTDeployer.t.sol`](../test/CTDeployer.t.sol) and [`test/Fork.t.sol`](../test/Fork.t.sol) for live deployment assumptions.
27
- - [`test/CroptopAttacks.t.sol`](../test/CroptopAttacks.t.sol) and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) when the issue could be in publisher or deployer behavior rather than one isolated function.
27
+ - [`test/CroptopAttacks.t.sol`](../test/CroptopAttacks.t.sol) and [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) when the issue could be in publisher or deployer behavior rather than one isolated function.
@@ -120,7 +120,8 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
120
120
 
121
121
  /// @notice Claim ownership of the collection.
122
122
  /// @dev Two-step ownership transfer process:
123
- /// Step 1 (this function): Transfers hook ownership to the project via `transferOwnershipToProject()`.
123
+ /// Step 1 (this function): Revokes the deployer-scoped permissions granted at launch, then transfers hook
124
+ /// ownership to the project via `transferOwnershipToProject()`.
124
125
  /// After this call, `hook.owner()` resolves dynamically through `PROJECTS.ownerOf(projectId)`.
125
126
  /// Step 2 (caller must do separately): The project owner grants CTPublisher the `ADJUST_721_TIERS` permission
126
127
  /// for the project so that `mintFrom()` continues to work.
@@ -132,11 +133,29 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
132
133
  // Get the project ID of the hook.
133
134
  uint256 projectId = hook.PROJECT_ID();
134
135
 
136
+ // Keep a reference to the caller.
137
+ address caller = _msgSender();
138
+
135
139
  // Make sure the caller is the owner of the project.
136
- if (PROJECTS.ownerOf(projectId) != _msgSender()) {
137
- revert CTDeployer_NotOwnerOfProject({projectId: projectId, hook: address(hook), caller: _msgSender()});
140
+ if (PROJECTS.ownerOf(projectId) != caller) {
141
+ revert CTDeployer_NotOwnerOfProject({projectId: projectId, hook: address(hook), caller: caller});
138
142
  }
139
143
 
144
+ // Revoke the deployer-scoped permissions that were granted to the caller during deployment.
145
+ // These permissions (ADJUST_721_TIERS, SET_721_METADATA, MINT_721, SET_721_DISCOUNT_PERCENT) allowed the
146
+ // project owner to manage the hook while the deployer owned it. After transferring hook ownership to the
147
+ // project, these deployer-scoped grants are no longer needed and should be cleaned up to prevent stale
148
+ // permission leakage.
149
+ PERMISSIONS.setPermissionsFor({
150
+ account: address(this),
151
+ permissionsData: JBPermissionsData({
152
+ operator: caller,
153
+ // forge-lint: disable-next-line(unsafe-typecast)
154
+ projectId: uint64(projectId),
155
+ permissionIds: new uint8[](0)
156
+ })
157
+ });
158
+
140
159
  // Transfer the hook's ownership to the project.
141
160
  JBOwnable(address(hook)).transferOwnershipToProject(projectId);
142
161
  }
@@ -171,7 +190,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
171
190
  projectId = PROJECTS.createFor(address(this));
172
191
 
173
192
  // Deploy a blank project.
174
- // slither-disable-next-line reentrancy-benign
175
193
  hook = DEPLOYER.deployHookFor({
176
194
  projectId: projectId,
177
195
  deployTiersHookConfig: JBDeploy721TiersHookConfig({
@@ -200,7 +218,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
200
218
  rulesetConfigurations[0].metadata.useDataHookForCashOut = true;
201
219
 
202
220
  // Launch the rulesets for the reserved project.
203
- // slither-disable-next-line unused-return
204
221
  controller.launchRulesetsFor({
205
222
  projectId: projectId,
206
223
  projectUri: projectConfig.projectUri,
@@ -214,7 +231,7 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
214
231
 
215
232
  // Configure allowed posts.
216
233
  if (projectConfig.allowedPosts.length > 0) {
217
- _configurePostingCriteriaFor(address(hook), projectConfig.allowedPosts);
234
+ _configurePostingCriteriaFor({hook: address(hook), allowedPosts: projectConfig.allowedPosts});
218
235
  }
219
236
 
220
237
  // Deploy the suckers (if applicable).
@@ -226,7 +243,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
226
243
 
227
244
  // Successful deployments are discoverable from the registry, and failures are reported without reverting
228
245
  // the project launch.
229
- // slither-disable-next-line unused-return
230
246
  try SUCKER_REGISTRY.deploySuckersFor({
231
247
  projectId: projectId,
232
248
  salt: suckerSalt,
@@ -237,7 +253,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
237
253
  // no-op
238
254
  }
239
255
  catch (bytes memory reason) {
240
- // slither-disable-next-line reentrancy-events
241
256
  emit CTDeployer_SuckerDeploymentFailed({projectId: projectId, salt: suckerSalt, reason: reason});
242
257
  }
243
258
  }
@@ -283,7 +298,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
283
298
 
284
299
  // Deploy the suckers. The sucker registry performs its own permission check against this forwarding helper,
285
300
  // so an unapproved CTDeployer fails at the downstream registry boundary without an extra preflight read here.
286
- // slither-disable-next-line unused-return
287
301
  suckers = SUCKER_REGISTRY.deploySuckersFor({
288
302
  projectId: projectId,
289
303
  salt: keccak256(abi.encode(suckerDeploymentConfiguration.salt, _msgSender())),
@@ -334,7 +348,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
334
348
  hookSpecifications
335
349
  );
336
350
  }
337
- // slither-disable-next-line unused-return
338
351
  return hook.beforeCashOutRecordedWith(context);
339
352
  }
340
353
 
@@ -358,7 +371,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
358
371
  return (context.weight, hookSpecifications);
359
372
  }
360
373
 
361
- // slither-disable-next-line unused-return
362
374
  return hook.beforePayRecordedWith(context);
363
375
  }
364
376
 
@@ -32,19 +32,20 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
32
32
  //*********************************************************************//
33
33
 
34
34
  error CTPublisher_DuplicatePost(bytes32 encodedIPFSUri);
35
- error CTPublisher_EmptyEncodedIPFSUri();
35
+ error CTPublisher_EmptyEncodedIPFSUri(uint256 postIndex);
36
36
  error CTPublisher_InsufficientEthSent(uint256 expected, uint256 sent);
37
37
  error CTPublisher_MaxTotalSupplyLessThanMin(uint256 min, uint256 max);
38
38
  error CTPublisher_NotInAllowList(address addr, address[] allowedAddresses);
39
39
  error CTPublisher_PriceTooSmall(uint256 price, uint256 minimumPrice);
40
- error CTPublisher_DuplicatePayMetadata();
40
+ error CTPublisher_DuplicatePayMetadata(bytes4 payMetadataId);
41
41
  error CTPublisher_FeePaymentFailed(uint256 feeAmount);
42
42
  error CTPublisher_SplitPercentExceedsMaximum(uint256 splitPercent, uint256 maximumSplitPercent);
43
43
  error CTPublisher_TotalSupplyTooBig(uint256 totalSupply, uint256 maximumTotalSupply);
44
44
  error CTPublisher_TotalSupplyTooSmall(uint256 totalSupply, uint256 minimumTotalSupply);
45
- error CTPublisher_NoPosts();
46
- error CTPublisher_UnauthorizedToPostInCategory();
47
- error CTPublisher_ZeroTotalSupply();
45
+ error CTPublisher_NoPosts(address caller);
46
+ error CTPublisher_InvalidFeeBeneficiary();
47
+ error CTPublisher_UnauthorizedToPostInCategory(address hook, uint24 category);
48
+ error CTPublisher_ZeroTotalSupply(address hook, uint24 category);
48
49
 
49
50
  //*********************************************************************//
50
51
  // ------------------------- public constants ------------------------ //
@@ -133,7 +134,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
133
134
  emit ConfigurePostingCriteria({hook: allowedPost.hook, allowedPost: allowedPost, caller: _msgSender()});
134
135
 
135
136
  // Enforce permissions.
136
- // slither-disable-next-line reentrancy-events,calls-loop
137
137
  _requirePermissionFrom({
138
138
  account: JBOwnable(allowedPost.hook).owner(),
139
139
  projectId: IJB721TiersHook(allowedPost.hook).PROJECT_ID(),
@@ -142,14 +142,14 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
142
142
 
143
143
  // Make sure there is a minimum supply.
144
144
  if (allowedPost.minimumTotalSupply == 0) {
145
- revert CTPublisher_ZeroTotalSupply();
145
+ revert CTPublisher_ZeroTotalSupply({hook: allowedPost.hook, category: allowedPost.category});
146
146
  }
147
147
 
148
148
  // Make sure the minimum supply does not surpass the maximum supply.
149
149
  if (allowedPost.minimumTotalSupply > allowedPost.maximumTotalSupply) {
150
- revert CTPublisher_MaxTotalSupplyLessThanMin(
151
- allowedPost.minimumTotalSupply, allowedPost.maximumTotalSupply
152
- );
150
+ revert CTPublisher_MaxTotalSupplyLessThanMin({
151
+ min: allowedPost.minimumTotalSupply, max: allowedPost.maximumTotalSupply
152
+ });
153
153
  }
154
154
 
155
155
  uint256 packed;
@@ -202,7 +202,10 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
202
202
  override
203
203
  {
204
204
  // Reject empty posts to prevent fee-free metadata shadowing.
205
- if (posts.length == 0) revert CTPublisher_NoPosts();
205
+ if (posts.length == 0) revert CTPublisher_NoPosts(_msgSender());
206
+
207
+ // Reject address(0) as fee beneficiary to prevent burning fee project tokens.
208
+ if (feeBeneficiary == address(0)) revert CTPublisher_InvalidFeeBeneficiary();
206
209
 
207
210
  // Keep a reference to the amount being paid, which is msg.value minus the fee.
208
211
  uint256 payValue = msg.value;
@@ -216,7 +219,7 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
216
219
  {
217
220
  // Setup the posts.
218
221
  (JB721TierConfig[] memory tiersToAdd, uint256[] memory tierIdsToMint, uint256 totalPrice) =
219
- _setupPosts(hook, posts);
222
+ _setupPosts({hook: hook, posts: posts});
220
223
 
221
224
  if (projectId != FEE_PROJECT_ID) {
222
225
  // Keep a reference to the fee that will be paid.
@@ -227,7 +230,7 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
227
230
 
228
231
  // Make sure enough ETH was sent to cover the fee.
229
232
  if (payValue < fee) {
230
- revert CTPublisher_InsufficientEthSent(totalPrice + fee, msg.value);
233
+ revert CTPublisher_InsufficientEthSent({expected: totalPrice + fee, sent: msg.value});
231
234
  }
232
235
 
233
236
  payValue -= fee;
@@ -235,11 +238,10 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
235
238
 
236
239
  // Make sure the amount sent to this function is at least the specified price of the tier plus the fee.
237
240
  if (totalPrice > payValue) {
238
- revert CTPublisher_InsufficientEthSent(totalPrice, msg.value);
241
+ revert CTPublisher_InsufficientEthSent({expected: totalPrice, sent: msg.value});
239
242
  }
240
243
 
241
244
  // Add the new tiers.
242
- // slither-disable-next-line reentrancy-events
243
245
  hook.adjustTiers({tiersToAdd: tiersToAdd, tierIdsToRemove: new uint256[](0)});
244
246
 
245
247
  // Keep a reference to the metadata ID target.
@@ -249,9 +251,8 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
249
251
  // tier selection, allowing the caller to mint arbitrary tiers.
250
252
  {
251
253
  bytes4 payId = JBMetadataResolver.getId({purpose: "pay", target: metadataIdTarget});
252
- // slither-disable-next-line unused-return
253
254
  (bool exists,) = JBMetadataResolver.getDataFor({id: payId, metadata: additionalPayMetadata});
254
- if (exists) revert CTPublisher_DuplicatePayMetadata();
255
+ if (exists) revert CTPublisher_DuplicatePayMetadata(payId);
255
256
  }
256
257
 
257
258
  // Create the metadata for the payment to specify the tier IDs that should be minted. We create manually the
@@ -288,7 +289,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
288
289
  DIRECTORY.primaryTerminalOf({projectId: projectId, token: JBConstants.NATIVE_TOKEN});
289
290
 
290
291
  // Make the payment.
291
- // slither-disable-next-line unused-return
292
292
  projectTerminal.pay{value: payValue}({
293
293
  projectId: projectId,
294
294
  token: JBConstants.NATIVE_TOKEN,
@@ -312,7 +312,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
312
312
 
313
313
  // Make the fee payment. If the fee sink is unavailable, refund the fee to the caller
314
314
  // rather than trapping or silently redirecting protocol funds.
315
- // slither-disable-next-line unused-return
316
315
  try feeTerminal.pay{value: payValue}({
317
316
  projectId: FEE_PROJECT_ID,
318
317
  amount: payValue,
@@ -323,7 +322,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
323
322
  metadata: ""
324
323
  }) {}
325
324
  catch {
326
- // slither-disable-next-line low-level-calls
327
325
  (bool success,) = _msgSender().call{value: payValue}("");
328
326
  if (!success) revert CTPublisher_FeePaymentFailed(payValue);
329
327
  }
@@ -363,7 +361,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
363
361
 
364
362
  // If there's a tier ID stored, resolve it.
365
363
  if (tierId != 0) {
366
- // slither-disable-next-line calls-loop
367
364
  tiers[i] = IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false});
368
365
  }
369
366
 
@@ -463,13 +460,13 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
463
460
  // Make sure the post includes an encodedIPFSUri.
464
461
  // forge-lint: disable-next-line(unsafe-typecast)
465
462
  if (post.encodedIPFSUri == bytes32("")) {
466
- revert CTPublisher_EmptyEncodedIPFSUri();
463
+ revert CTPublisher_EmptyEncodedIPFSUri({postIndex: i});
467
464
  }
468
465
 
469
466
  // Check for duplicate encodedIPFSUri within the same batch to prevent fee evasion.
470
467
  for (uint256 j; j < i;) {
471
468
  if (posts[j].encodedIPFSUri == post.encodedIPFSUri) {
472
- revert CTPublisher_DuplicatePost(post.encodedIPFSUri);
469
+ revert CTPublisher_DuplicatePost({encodedIPFSUri: post.encodedIPFSUri});
473
470
  }
474
471
  unchecked {
475
472
  ++j;
@@ -486,10 +483,8 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
486
483
  // The cache can become stale if the tier was removed (via adjustTiers) or
487
484
  // its URI was changed (via setMetadata). In either case, clear the stale
488
485
  // mapping and fall through to create a new tier.
489
- // slither-disable-next-line calls-loop
490
486
  JB721Tier memory cachedTier =
491
487
  store.tierOf({hook: address(hook), id: tierId, includeResolvedUri: false});
492
- // slither-disable-next-line calls-loop
493
488
  if (store.isTierRemoved(address(hook), tierId) || cachedTier.encodedIPFSUri != post.encodedIPFSUri)
494
489
  {
495
490
  delete tierIdForEncodedIPFSUriOf[address(hook)][post.encodedIPFSUri];
@@ -518,34 +513,40 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
518
513
 
519
514
  // Make sure the category being posted to allows publishing.
520
515
  if (minimumTotalSupply == 0) {
521
- revert CTPublisher_UnauthorizedToPostInCategory();
516
+ revert CTPublisher_UnauthorizedToPostInCategory({hook: address(hook), category: post.category});
522
517
  }
523
518
 
524
519
  // Make sure the price being paid for the post is at least the allowed minimum price.
525
520
  if (post.price < minimumPrice) {
526
- revert CTPublisher_PriceTooSmall(post.price, minimumPrice);
521
+ revert CTPublisher_PriceTooSmall({price: post.price, minimumPrice: minimumPrice});
527
522
  }
528
523
 
529
524
  // Make sure the total supply being made available for the post is at least the allowed minimum
530
525
  // total supply.
531
526
  if (post.totalSupply < minimumTotalSupply) {
532
- revert CTPublisher_TotalSupplyTooSmall(post.totalSupply, minimumTotalSupply);
527
+ revert CTPublisher_TotalSupplyTooSmall({
528
+ totalSupply: post.totalSupply, minimumTotalSupply: minimumTotalSupply
529
+ });
533
530
  }
534
531
 
535
532
  // Make sure the total supply being made available for the post is at most the allowed maximum total
536
533
  // supply.
537
534
  if (post.totalSupply > maximumTotalSupply) {
538
- revert CTPublisher_TotalSupplyTooBig(post.totalSupply, maximumTotalSupply);
535
+ revert CTPublisher_TotalSupplyTooBig({
536
+ totalSupply: post.totalSupply, maximumTotalSupply: maximumTotalSupply
537
+ });
539
538
  }
540
539
 
541
540
  // Make sure the split percent is within the allowed maximum.
542
541
  if (post.splitPercent > maximumSplitPercent) {
543
- revert CTPublisher_SplitPercentExceedsMaximum(post.splitPercent, maximumSplitPercent);
542
+ revert CTPublisher_SplitPercentExceedsMaximum({
543
+ splitPercent: post.splitPercent, maximumSplitPercent: maximumSplitPercent
544
+ });
544
545
  }
545
546
 
546
547
  // Make sure the address is allowed to post.
547
548
  if (addresses.length != 0 && !_isAllowed({addrs: _msgSender(), addresses: addresses})) {
548
- revert CTPublisher_NotInAllowList(_msgSender(), addresses);
549
+ revert CTPublisher_NotInAllowList({addr: _msgSender(), allowedAddresses: addresses});
549
550
  }
550
551
  }
551
552