@croptop/core-v6 0.0.38 → 0.0.40

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 (43) hide show
  1. package/README.md +2 -2
  2. package/foundry.toml +2 -1
  3. package/package.json +25 -13
  4. package/script/ConfigureFeeProject.s.sol +8 -5
  5. package/src/CTDeployer.sol +67 -58
  6. package/src/CTProjectOwner.sol +6 -4
  7. package/src/CTPublisher.sol +14 -4
  8. package/src/interfaces/ICTDeployer.sol +2 -2
  9. package/src/structs/CTProjectConfig.sol +7 -6
  10. package/ADMINISTRATION.md +0 -94
  11. package/ARCHITECTURE.md +0 -96
  12. package/AUDIT_INSTRUCTIONS.md +0 -88
  13. package/RISKS.md +0 -78
  14. package/SKILLS.md +0 -46
  15. package/STYLE_GUIDE.md +0 -610
  16. package/USER_JOURNEYS.md +0 -134
  17. package/foundry.lock +0 -11
  18. package/slither-ci.config.json +0 -10
  19. package/sphinx.lock +0 -507
  20. package/test/CTDeployer.t.sol +0 -616
  21. package/test/CTProjectOwner.t.sol +0 -185
  22. package/test/CTPublisher.t.sol +0 -869
  23. package/test/ClaimCollectionOwnership.t.sol +0 -315
  24. package/test/CroptopAttacks.t.sol +0 -437
  25. package/test/Fork.t.sol +0 -227
  26. package/test/TestAuditGaps.sol +0 -696
  27. package/test/Test_MetadataGeneration.t.sol +0 -79
  28. package/test/audit/CodexNemesisCroptopPublisherBoundary.t.sol +0 -329
  29. package/test/audit/CodexNemesisCurrencyPoCs.t.sol +0 -371
  30. package/test/audit/CodexNemesisFreshRound.t.sol +0 -395
  31. package/test/audit/CodexNemesisMetadataShadow.t.sol +0 -196
  32. package/test/audit/CodexNemesisPoCs.t.sol +0 -263
  33. package/test/audit/CodexNemesisPolicyReuse.t.sol +0 -168
  34. package/test/audit/CodexNemesisUriDrift.t.sol +0 -252
  35. package/test/audit/DeployerPermissionBypass.t.sol +0 -213
  36. package/test/audit/EmptyPostFeeBypass.t.sol +0 -53
  37. package/test/audit/FeeBeneficiaryReentrancy.t.sol +0 -247
  38. package/test/audit/FeeFallbackBlackhole.t.sol +0 -263
  39. package/test/audit/Pass12Fixes.t.sol +0 -388
  40. package/test/fork/PublishFork.t.sol +0 -440
  41. package/test/regression/DuplicateUriFeeEvasion.t.sol +0 -312
  42. package/test/regression/FeeEvasion.t.sol +0 -286
  43. package/test/regression/StaleTierIdMapping.t.sol +0 -228
package/README.md CHANGED
@@ -80,8 +80,8 @@ npm install @croptop/core-v6
80
80
 
81
81
  ```bash
82
82
  npm install
83
- forge build
84
- forge test
83
+ forge build --deny notes
84
+ forge test --deny notes --fail-fast --summary --detailed --skip "*/script/**"
85
85
  ```
86
86
 
87
87
  Useful scripts:
package/foundry.toml CHANGED
@@ -17,7 +17,8 @@ fail_on_revert = false
17
17
  ethereum = "${RPC_ETHEREUM_MAINNET}"
18
18
 
19
19
  [lint]
20
- exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
20
+ exclude_lints = ["mixed-case-variable", "pascal-case-struct"]
21
+
21
22
  [fmt]
22
23
  number_underscore = "thousands"
23
24
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,11 +1,24 @@
1
1
  {
2
2
  "name": "@croptop/core-v6",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/mejango/croptop-core-v6"
8
8
  },
9
+ "files": [
10
+ "CHANGELOG.md",
11
+ "foundry.toml",
12
+ "references/",
13
+ "remappings.txt",
14
+ "script/ConfigureFeeProject.s.sol",
15
+ "script/Deploy.s.sol",
16
+ "script/helpers/",
17
+ "src/"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20.0.0"
21
+ },
9
22
  "scripts": {
10
23
  "test": "forge test",
11
24
  "coverage:integration": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
@@ -13,21 +26,20 @@
13
26
  "deploy:mainnets:project": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/ConfigureFeeProject.s.sol --networks mainnets",
14
27
  "deploy:testnets": "source ./.env && npx sphinx propose ./script/Deploy.s.sol --networks testnets",
15
28
  "deploy:testnets:project": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/ConfigureFeeProject.s.sol --networks testnets",
16
- "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v5'"
29
+ "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v6'"
17
30
  },
18
31
  "dependencies": {
19
- "@bananapus/721-hook-v6": "^0.0.38",
20
- "@bananapus/buyback-hook-v6": "^0.0.30",
21
- "@bananapus/core-v6": "^0.0.36",
22
- "@bananapus/ownable-v6": "^0.0.20",
23
- "@bananapus/permission-ids-v6": "^0.0.19",
24
- "@bananapus/router-terminal-v6": "^0.0.30",
25
- "@bananapus/suckers-v6": "^0.0.28",
26
- "@openzeppelin/contracts": "^5.6.1"
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",
38
+ "@openzeppelin/contracts": "5.6.1",
39
+ "@rev-net/core-v6": "0.0.37"
27
40
  },
28
41
  "devDependencies": {
29
- "@bananapus/address-registry-v6": "^0.0.17",
30
- "@rev-net/core-v6": "^0.0.32",
31
- "@sphinx-labs/plugins": "^0.33.3"
42
+ "@bananapus/address-registry-v6": "0.0.25",
43
+ "@sphinx-labs/plugins": "0.33.3"
32
44
  }
33
45
  }
@@ -248,14 +248,16 @@ contract ConfigureFeeProjectScript is Script, Sphinx {
248
248
  if (block.chainid == 1 || block.chainid == 11_155_111) {
249
249
  suckerDeployerConfigurations = new JBSuckerDeployerConfig[](3);
250
250
  // OP
251
- suckerDeployerConfigurations[0] =
252
- JBSuckerDeployerConfig({deployer: suckers.optimismDeployer, mappings: tokenMappings});
251
+ suckerDeployerConfigurations[0] = JBSuckerDeployerConfig({
252
+ deployer: suckers.optimismDeployer, peer: bytes32(0), mappings: tokenMappings
253
+ });
253
254
 
254
255
  suckerDeployerConfigurations[1] =
255
- JBSuckerDeployerConfig({deployer: suckers.baseDeployer, mappings: tokenMappings});
256
+ JBSuckerDeployerConfig({deployer: suckers.baseDeployer, peer: bytes32(0), mappings: tokenMappings});
256
257
 
257
- suckerDeployerConfigurations[2] =
258
- JBSuckerDeployerConfig({deployer: suckers.arbitrumDeployer, mappings: tokenMappings});
258
+ suckerDeployerConfigurations[2] = JBSuckerDeployerConfig({
259
+ deployer: suckers.arbitrumDeployer, peer: bytes32(0), mappings: tokenMappings
260
+ });
259
261
  } else {
260
262
  suckerDeployerConfigurations = new JBSuckerDeployerConfig[](1);
261
263
  // L2 -> Mainnet
@@ -263,6 +265,7 @@ contract ConfigureFeeProjectScript is Script, Sphinx {
263
265
  deployer: address(suckers.optimismDeployer) != address(0)
264
266
  ? suckers.optimismDeployer
265
267
  : address(suckers.baseDeployer) != address(0) ? suckers.baseDeployer : suckers.arbitrumDeployer,
268
+ peer: bytes32(0),
266
269
  mappings: tokenMappings
267
270
  });
268
271
 
@@ -36,24 +36,33 @@ import {CTDeployerAllowedPost} from "./structs/CTDeployerAllowedPost.sol";
36
36
  import {CTProjectConfig} from "./structs/CTProjectConfig.sol";
37
37
  import {CTSuckerDeploymentConfig} from "./structs/CTSuckerDeploymentConfig.sol";
38
38
 
39
- /// @notice A contract that facilitates deploying a simple Juicebox project to receive posts from Croptop templates.
39
+ /// @notice Deploys Juicebox projects pre-configured for Croptop a permissionless NFT publishing system. Each
40
+ /// deployed project gets a tiered 721 hook (for minting posted NFTs), an optional set of cross-chain suckers, and this
41
+ /// contract set as the data hook so suckers get 0% cash-out tax and mint permission. The hook initially remains owned
42
+ /// by this deployer (allowing the publisher to add tiers); the project owner can later claim full hook ownership via
43
+ /// `claimCollectionOwnershipOf`.
40
44
  contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC721Receiver, ICTDeployer {
41
45
  //*********************************************************************//
42
46
  // --------------------------- custom errors ------------------------- //
43
47
  //*********************************************************************//
44
48
 
45
49
  error CTDeployer_NotOwnerOfProject(uint256 projectId, address hook, address caller);
50
+ //*********************************************************************//
51
+ // ---------------------------- events -------------------------------- //
52
+ //*********************************************************************//
53
+
54
+ event CTDeployer_SuckerDeploymentFailed(uint256 indexed projectId, bytes32 indexed salt, bytes reason);
46
55
 
47
56
  //*********************************************************************//
48
57
  // ---------------- public immutable stored properties --------------- //
49
58
  //*********************************************************************//
50
59
 
51
- /// @notice Mints ERC-721s that represent Juicebox project ownership and transfers.
52
- IJBProjects public immutable override PROJECTS;
53
-
54
60
  /// @notice The deployer to launch Croptop recorded collections from.
55
61
  IJB721TiersHookDeployer public immutable override DEPLOYER;
56
62
 
63
+ /// @notice Mints ERC-721s that represent Juicebox project ownership and transfers.
64
+ IJBProjects public immutable override PROJECTS;
65
+
57
66
  /// @notice The Croptop publisher.
58
67
  ICTPublisher public immutable override PUBLISHER;
59
68
 
@@ -95,25 +104,13 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
95
104
  PUBLISHER = publisher;
96
105
  SUCKER_REGISTRY = suckerRegistry;
97
106
 
98
- // Give the sucker registry permission to map tokens for all revnets.
107
+ // Set permission for the CTPublisher to adjust tiers while the deployer temporarily owns new hooks.
99
108
  uint8[] memory permissionIds = new uint8[](1);
100
- permissionIds[0] = JBPermissionIds.MAP_SUCKER_TOKEN;
101
-
102
- // Give the operator the permission.
103
- // Set up the permission data.
104
- JBPermissionsData memory permissionData =
105
- JBPermissionsData({operator: address(SUCKER_REGISTRY), projectId: 0, permissionIds: permissionIds});
106
-
107
- // Set the permissions.
108
- PERMISSIONS.setPermissionsFor({account: address(this), permissionsData: permissionData});
109
-
110
- // Set permission for the CTPublisher to adjust the tier.
111
109
  permissionIds[0] = JBPermissionIds.ADJUST_721_TIERS;
112
110
 
113
- // Set permission for the CTPublisher to mint the NFT.
114
- permissionData = JBPermissionsData({operator: address(PUBLISHER), projectId: 0, permissionIds: permissionIds});
111
+ JBPermissionsData memory permissionData =
112
+ JBPermissionsData({operator: address(PUBLISHER), projectId: 0, permissionIds: permissionIds});
115
113
 
116
- // Set permission for the CTPublisher to adjust the tier.
117
114
  PERMISSIONS.setPermissionsFor({account: address(this), permissionsData: permissionData});
118
115
  }
119
116
 
@@ -137,7 +134,7 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
137
134
 
138
135
  // Make sure the caller is the owner of the project.
139
136
  if (PROJECTS.ownerOf(projectId) != _msgSender()) {
140
- revert CTDeployer_NotOwnerOfProject(projectId, address(hook), _msgSender());
137
+ revert CTDeployer_NotOwnerOfProject({projectId: projectId, hook: address(hook), caller: _msgSender()});
141
138
  }
142
139
 
143
140
  // Transfer the hook's ownership to the project.
@@ -145,9 +142,9 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
145
142
  }
146
143
 
147
144
  /// @notice Deploy a simple project meant to receive posts from Croptop templates.
148
- /// @dev The initial project owner is intentionally granted direct hook-management permissions from
149
- /// `CTDeployer`. This means the owner/operator can bypass the Croptop publisher path and interact
150
- /// with the hook directly if they choose to. That is an explicit product tradeoff.
145
+ /// @dev The deployed hook remains owned by `CTDeployer` until the project owner claims collection ownership.
146
+ /// The initial owner is granted direct deployer-scoped hook permissions as a launch-time convenience. Those
147
+ /// permissions can bypass Croptop's publisher surface until ownership is claimed away from the deployer.
151
148
  /// @param owner The address that'll own the project.
152
149
  /// @param projectConfig The configuration for the project.
153
150
  /// @param suckerDeploymentConfiguration The configuration for the suckers to deploy.
@@ -170,8 +167,8 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
170
167
  rulesetConfigurations[0].weight = 1_000_000 * (10 ** 18);
171
168
  rulesetConfigurations[0].metadata.baseCurrency = JBCurrencyIds.ETH;
172
169
 
173
- // Get the next project ID.
174
- projectId = PROJECTS.count() + 1;
170
+ // Reserve the project ID up front so permissionless project creations cannot invalidate hook deployment.
171
+ projectId = PROJECTS.createFor(address(this));
175
172
 
176
173
  // Deploy a blank project.
177
174
  // slither-disable-next-line reentrancy-benign
@@ -202,17 +199,15 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
202
199
  rulesetConfigurations[0].metadata.useDataHookForPay = true;
203
200
  rulesetConfigurations[0].metadata.useDataHookForCashOut = true;
204
201
 
205
- // Launch the project, and sanity check the project ID.
206
- assert(
207
- projectId
208
- == controller.launchProjectFor({
209
- owner: address(this),
210
- projectUri: projectConfig.projectUri,
211
- rulesetConfigurations: rulesetConfigurations,
212
- terminalConfigurations: projectConfig.terminalConfigurations,
213
- memo: "Deployed from Croptop"
214
- })
215
- );
202
+ // Launch the rulesets for the reserved project.
203
+ // slither-disable-next-line unused-return
204
+ controller.launchRulesetsFor({
205
+ projectId: projectId,
206
+ projectUri: projectConfig.projectUri,
207
+ rulesetConfigurations: rulesetConfigurations,
208
+ terminalConfigurations: projectConfig.terminalConfigurations,
209
+ memo: "Deployed from Croptop"
210
+ });
216
211
 
217
212
  // Set the data hook for the project.
218
213
  dataHookOf[projectId] = IJBRulesetDataHook(hook);
@@ -227,23 +222,32 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
227
222
  // intentionally ordered. If both deployers fail, the deployment proceeds without suckers rather than reverting,
228
223
  // allowing projects to launch on unsupported chains with manual sucker setup later.
229
224
  if (suckerDeploymentConfiguration.salt != bytes32(0)) {
225
+ bytes32 suckerSalt = keccak256(abi.encode(suckerDeploymentConfiguration.salt, _msgSender()));
226
+
227
+ // Successful deployments are discoverable from the registry, and failures are reported without reverting
228
+ // the project launch.
230
229
  // slither-disable-next-line unused-return
231
- SUCKER_REGISTRY.deploySuckersFor({
230
+ try SUCKER_REGISTRY.deploySuckersFor({
232
231
  projectId: projectId,
233
- salt: keccak256(abi.encode(suckerDeploymentConfiguration.salt, _msgSender())),
232
+ salt: suckerSalt,
234
233
  configurations: suckerDeploymentConfiguration.deployerConfigurations
235
- });
234
+ }) returns (
235
+ address[] memory
236
+ ) {
237
+ // no-op
238
+ }
239
+ catch (bytes memory reason) {
240
+ // slither-disable-next-line reentrancy-events
241
+ emit CTDeployer_SuckerDeploymentFailed({projectId: projectId, salt: suckerSalt, reason: reason});
242
+ }
236
243
  }
237
244
 
238
- //transfer to _owner.
245
+ // Transfer the project NFT to its intended owner.
239
246
  PROJECTS.transferFrom({from: address(this), to: owner, tokenId: projectId});
240
247
 
241
- // Set permission for the project's owner to do all the NFT things.
242
- // These permissions are granted from CTDeployer (address(this)) to the initial owner.
243
- // The hook checks permissions against hook.owner(), which after claimCollectionOwnershipOf() resolves
244
- // dynamically via PROJECTS.ownerOf(projectId). Before claiming, CTDeployer is the static hook owner,
245
- // so these permissions allow the project owner to manage tiers through CTDeployer. As a tradeoff,
246
- // the owner can also bypass the Croptop publisher surface until ownership is claimed away.
248
+ // Give the initial project owner direct collection-control permissions while CTDeployer remains the hook's
249
+ // owner. This preserves the documented Croptop launch tradeoff: the owner can manage the collection directly
250
+ // before calling `claimCollectionOwnershipOf(...)`, after which hook permissions follow the project NFT owner.
247
251
  uint8[] memory permissionIds = new uint8[](4);
248
252
  permissionIds[0] = JBPermissionIds.ADJUST_721_TIERS;
249
253
  permissionIds[1] = JBPermissionIds.SET_721_METADATA;
@@ -253,7 +257,7 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
253
257
  PERMISSIONS.setPermissionsFor({
254
258
  account: address(this),
255
259
  permissionsData: JBPermissionsData({
256
- operator: address(owner),
260
+ operator: owner,
257
261
  // forge-lint: disable-next-line(unsafe-typecast)
258
262
  projectId: uint64(projectId),
259
263
  permissionIds: permissionIds
@@ -272,12 +276,13 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
272
276
  external
273
277
  returns (address[] memory suckers)
274
278
  {
275
- // Enforce permissions.
276
- _requirePermissionFrom({
277
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.DEPLOY_SUCKERS
278
- });
279
+ address owner = PROJECTS.ownerOf(projectId);
280
+
281
+ // First prove the external caller is allowed to request sucker deployment for the project owner.
282
+ _requirePermissionFrom({account: owner, projectId: projectId, permissionId: JBPermissionIds.DEPLOY_SUCKERS});
279
283
 
280
- // Deploy the suckers.
284
+ // Deploy the suckers. The sucker registry performs its own permission check against this forwarding helper,
285
+ // so an unapproved CTDeployer fails at the downstream registry boundary without an extra preflight read here.
281
286
  // slither-disable-next-line unused-return
282
287
  suckers = SUCKER_REGISTRY.deploySuckersFor({
283
288
  projectId: projectId,
@@ -290,8 +295,10 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
290
295
  // ------------------------- external views -------------------------- //
291
296
  //*********************************************************************//
292
297
 
293
- /// @notice Allow cash outs from suckers without a tax.
294
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
298
+ /// @notice Called before a cash out is recorded. Grants suckers 0% tax so bridged tokens redeem at full value.
299
+ /// For non-sucker holders, delegates to the project's stored data hook (if any) or passes through the original
300
+ /// context values.
301
+ /// @dev Part of `IJBRulesetDataHook`.
295
302
  /// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
296
303
  /// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
297
304
  /// out.
@@ -331,8 +338,9 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
331
338
  return hook.beforeCashOutRecordedWith(context);
332
339
  }
333
340
 
334
- /// @notice Forward the call to the original data hook.
335
- /// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
341
+ /// @notice Called before a payment is recorded. Delegates to the project's stored data hook (the 721 hook) so NFT
342
+ /// tier minting logic runs. If no hook is set, passes through the original weight.
343
+ /// @dev Part of `IJBRulesetDataHook`.
336
344
  /// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
337
345
  /// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
338
346
  /// tokens get minted by a payment.
@@ -354,8 +362,9 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
354
362
  return hook.beforePayRecordedWith(context);
355
363
  }
356
364
 
357
- /// @notice A flag indicating whether an address has permission to mint a project's tokens on-demand.
358
- /// @dev A project's data hook can allow any address to mint its tokens.
365
+ /// @notice Returns whether an address may mint a project's tokens on-demand. Only suckers get this permission, so
366
+ /// bridged tokens can be minted on the destination chain.
367
+ /// @dev Part of `IJBRulesetDataHook`.
359
368
  /// @param projectId The ID of the project whose token can be minted.
360
369
  /// @param addr The address to check the token minting permission of.
361
370
  /// @return flag A flag indicating whether the address has permission to mint the project's tokens on-demand.
@@ -10,10 +10,12 @@ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.
10
10
  import {ICTProjectOwner} from "./interfaces/ICTProjectOwner.sol";
11
11
  import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
12
12
 
13
- /// @notice A contract that can be sent a project to be burned, while still allowing croptop posts.
14
- /// @dev This contract does not expose any function to reconfigure posting criteria. This is by design: posting
15
- /// criteria are set before transferring the project here, and become immutable once ownership is transferred.
16
- /// The project owner should configure all desired posting criteria before sending the project NFT to this contract.
13
+ /// @notice A dead-end owner for Juicebox projects that locks ownership while preserving Croptop posting. When a project
14
+ /// NFT is transferred to this contract via `safeTransferFrom`, it automatically grants the Croptop publisher
15
+ /// `ADJUST_721_TIERS` permission so posts can continue. The project can never be transferred out again effectively
16
+ /// burning ownership while keeping the collection alive.
17
+ /// @dev This contract does not expose any function to reconfigure posting criteria. Criteria are set before
18
+ /// transferring the project here and become immutable once ownership is transferred.
17
19
  contract CTProjectOwner is IERC721Receiver, ICTProjectOwner {
18
20
  //*********************************************************************//
19
21
  // ---------------- public immutable stored properties --------------- //
@@ -21,7 +21,11 @@ import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
21
21
  import {CTAllowedPost} from "./structs/CTAllowedPost.sol";
22
22
  import {CTPost} from "./structs/CTPost.sol";
23
23
 
24
- /// @notice A contract that facilitates the permissioned publishing of NFT posts to a Juicebox project.
24
+ /// @notice The Croptop publishing engine. Allows anyone to publish NFT posts to a Juicebox project's 721 hook, subject
25
+ /// to per-category posting criteria set by the collection owner (minimum price, supply bounds, allowlist, max split
26
+ /// percent). On each mint, the publisher creates new 721 tiers, routes a 5% fee to the fee project, and pays the
27
+ /// remainder into the project's terminal. Duplicate IPFS URIs are tracked so subsequent mints of the same content reuse
28
+ /// the existing tier rather than creating a new one.
25
29
  contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
26
30
  //*********************************************************************//
27
31
  // --------------------------- custom errors ------------------------- //
@@ -109,7 +113,10 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
109
113
  // ---------------------- external transactions ---------------------- //
110
114
  //*********************************************************************//
111
115
 
112
- /// @notice Collection owners can set the allowed criteria for publishing a new NFT to their project.
116
+ /// @notice Lets collection owners define the rules for what can be posted in each category — minimum price,
117
+ /// supply
118
+ /// bounds, maximum split percent, and an optional allowlist of addresses. Each call replaces the existing criteria
119
+ /// for the specified categories.
113
120
  /// @param allowedPosts An array of criteria for allowed posts.
114
121
  // Categories cannot be fully disabled after creation. This is by design — once a category is
115
122
  // created, removing posting would break expectations for existing posters. Projects can set restrictive
@@ -171,8 +178,11 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
171
178
  }
172
179
  }
173
180
 
174
- /// @notice Publish an NFT to become mintable, and mint a first copy.
175
- /// @dev A fee is taken into the appropriate treasury.
181
+ /// @notice Publish one or more NFT posts to a project's 721 hook and mint a first copy of each. For each new post,
182
+ /// a tier is created on the hook. A 5% fee (1/FEE_DIVISOR) is taken from the total tier prices and routed to the
183
+ /// fee
184
+ /// project; the remainder is paid into the project's terminal, minting NFTs for the beneficiary.
185
+ /// @dev Reverts if any post violates the category's configured allowance (price, supply, split, allowlist).
176
186
  /// @param hook The hook to mint from.
177
187
  /// @param posts An array of posts that should be published as NFTs to the specified project.
178
188
  /// @param nftBeneficiary The beneficiary of the NFT mints.
@@ -24,8 +24,8 @@ interface ICTDeployer {
24
24
  function PUBLISHER() external view returns (ICTPublisher);
25
25
 
26
26
  /// @notice Claim ownership of a tiered ERC-721 hook collection by transferring it to the project.
27
- /// @dev After claiming, CTPublisher is atomically granted the ADJUST_721_TIERS permission so that mintFrom()
28
- /// continues to work.
27
+ /// @dev After claiming, hook ownership resolves through the current project NFT owner. That owner must grant
28
+ /// CTPublisher the ADJUST_721_TIERS permission separately for mintFrom() to continue working.
29
29
  /// @param hook The hook to claim ownership of. The caller must own the project the hook belongs to.
30
30
  function claimCollectionOwnershipOf(IJB721TiersHook hook) external;
31
31
 
@@ -4,13 +4,14 @@ pragma solidity ^0.8.0;
4
4
  import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
5
5
  import {CTDeployerAllowedPost} from "../structs/CTDeployerAllowedPost.sol";
6
6
 
7
- /// @param terminalConfigurations The terminals that the network uses to accept payments through.
7
+ /// @notice Configuration for deploying a new Croptop project via CTDeployer.
8
+ /// @param terminalConfigurations The terminals that the project uses to accept payments through.
8
9
  /// @param projectUri The metadata URI containing project info.
9
- /// @param allowedPosts The type of posts that the project should allow.
10
- /// @param contractUri A link to the collection's metadata.
11
- /// @param name The name of the collection where posts will go.
12
- /// @param symbol The symbol of the collection where posts will go.
13
- /// @param salt A salt to use for the deterministic deployment.
10
+ /// @param allowedPosts The posting criteria that define what kinds of NFTs can be published to each category.
11
+ /// @param contractUri A link to the 721 collection's metadata (e.g. OpenSea-compatible contract-level metadata).
12
+ /// @param name The name of the 721 collection where posts will be minted.
13
+ /// @param symbol The symbol of the 721 collection where posts will be minted.
14
+ /// @param salt A salt for deterministic deployment of the 721 hook (includes msg.sender for replay protection).
14
15
  struct CTProjectConfig {
15
16
  JBTerminalConfig[] terminalConfigurations;
16
17
  string projectUri;
package/ADMINISTRATION.md DELETED
@@ -1,94 +0,0 @@
1
- # Administration
2
-
3
- ## At A Glance
4
-
5
- | Item | Details |
6
- | --- | --- |
7
- | Scope | Croptop deployment flow, publish-policy administration, and irreversible project owner sink behavior |
8
- | Control posture | Mixed deployer-managed and project-local control |
9
- | Highest-risk actions | Burn-locking a project into `CTProjectOwner`, misconfiguring posting criteria, and deploying suckers with the wrong authority assumptions |
10
- | Recovery posture | Posting policy can often be changed, but burn-lock and some deployer wiring choices usually require replacement flows |
11
-
12
- ## Purpose
13
-
14
- `croptop-core-v6` has two distinct control planes: project-local publishing control and deployer-level structural wiring. The high-risk surfaces are posting criteria, hook ownership, publisher permissions, and the irreversible `CTProjectOwner` burn-lock path.
15
-
16
- ## Control Model
17
-
18
- - `CTPublisher` enforces publish policy but does not own the project.
19
- - `CTDeployer` is both a deployment helper and a live ruleset data-hook wrapper.
20
- - The initial project owner receives direct hook-management permissions from `CTDeployer` at deployment time.
21
- - Project owners or delegates administer publishing through the hook owner and `JBPermissions`.
22
- - `CTProjectOwner` is an irreversible ownership sink for projects that want Croptop-mediated control.
23
- - `SUCKER_REGISTRY` and `PUBLISHER` receive structural permissions from `CTDeployer`.
24
-
25
- ## Roles
26
-
27
- | Role | How Assigned | Scope | Notes |
28
- | --- | --- | --- | --- |
29
- | Project owner | `JBProjects.ownerOf(projectId)` | Per project | May grant delegates through `JBPermissions` |
30
- | Hook owner | `JBOwnable(hook).owner()` | Per hook | Often resolves to the project owner after claim |
31
- | `CTDeployer` | Immutable singleton | Global | Launch helper and runtime wrapper |
32
- | `CTPublisher` | Immutable singleton | Global runtime surface | Needs `ADJUST_721_TIERS` authority on relevant hooks |
33
- | `CTProjectOwner` | Receives project NFT transfer | Per project | Burn-lock path with no return function |
34
- | `SUCKER_REGISTRY` | Immutable dependency | Global | Holds wildcard `MAP_SUCKER_TOKEN` from the deployer |
35
-
36
- ## Privileged Surfaces
37
-
38
- | Contract | Function | Who Can Call | Effect |
39
- | --- | --- | --- | --- |
40
- | `CTDeployer` | `deployProjectFor(...)` | Anyone | Launches a Croptop-shaped project and configures initial permissions |
41
- | `CTDeployer` | `claimCollectionOwnershipOf(...)` | Current project owner | Transfers hook ownership from the deployer path to the project |
42
- | `CTDeployer` | `deploySuckersFor(...)` | Project owner or `DEPLOY_SUCKERS` delegate | Extends a project with suckers |
43
- | `CTPublisher` | `configurePostingCriteriaFor(...)` | Hook owner or `ADJUST_721_TIERS` delegate | Changes posting policy for a hook and category |
44
- | `CTPublisher` | `mintFrom(...)` | Anyone subject to policy | Publishes posts, mints first copies, and routes the Croptop fee |
45
- | `CTProjectOwner` | `onERC721Received(...)` | Any project NFT transfer into it | Locks the project into the Croptop owner helper and grants `CTPublisher` tier-adjust authority |
46
-
47
- Important nuance:
48
-
49
- - after `deployProjectFor(...)`, the initial project owner can directly manage tiers, metadata, minting, and discount percent through permissions granted from `CTDeployer`
50
- - that means the owner can bypass the publisher path until ownership is claimed away from `CTDeployer`
51
-
52
- ## Immutable And One-Way
53
-
54
- - `CTDeployer`'s wildcard permission grants to `SUCKER_REGISTRY` and `CTPublisher` are structural.
55
- - `dataHookOf[projectId]` is write-once through deployment flow.
56
- - Sending a project NFT into `CTProjectOwner` is effectively irreversible.
57
- - `FEE_PROJECT_ID` in `CTPublisher` is constructor-immutable.
58
-
59
- ## Operational Notes
60
-
61
- - Validate posting criteria before broad publisher access.
62
- - Decide intentionally whether the project should keep the initial direct-management path or move to project-owned hook control with `claimCollectionOwnershipOf(...)`.
63
- - Use `claimCollectionOwnershipOf(...)` when the project should own the hook directly instead of relying on the deployer as the ownership bridge.
64
- - Treat the burn-lock path as governance finality, not convenience.
65
- - Review Croptop deployer changes as both launch-time and runtime changes.
66
-
67
- ## Machine Notes
68
-
69
- - Do not treat `CTDeployer` as a passive script helper; it is also part of the live runtime path.
70
- - Treat `src/CTPublisher.sol`, `src/CTDeployer.sol`, and `src/CTProjectOwner.sol` as the minimum source set for control-plane review.
71
- - If a project NFT has already been sent to `CTProjectOwner`, stop assuming the original owner can recover it.
72
-
73
- ## Recovery
74
-
75
- - If posting policy is wrong but the project still controls the hook, fix it through `configurePostingCriteriaFor(...)`.
76
- - If the wrong hook path or burn-lock path was chosen, recovery usually means a new project or new hook arrangement.
77
- - `CTProjectOwner` is not a reversible safety valve.
78
-
79
- ## Admin Boundaries
80
-
81
- - Neither project owners nor Croptop can change the fixed fee divisor in `CTPublisher`.
82
- - `CTPublisher` cannot trap fee ETH intentionally; failed fee-terminal payments refund `_msgSender()` or revert.
83
- - `CTProjectOwner` cannot return project ownership once it receives the NFT.
84
- - `CTDeployer` cannot later rewrite `dataHookOf[projectId]` through a setter.
85
- - `CTDeployer` does not stop the initial project owner from using the directly granted hook permissions before ownership is claimed away.
86
-
87
- ## Source Map
88
-
89
- - `src/CTPublisher.sol`
90
- - `src/CTDeployer.sol`
91
- - `src/CTProjectOwner.sol`
92
- - `script/Deploy.s.sol`
93
- - `script/helpers/CroptopDeploymentLib.sol`
94
- - `test/TestAuditGaps.sol`