@croptop/core-v6 0.0.27 → 0.0.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@croptop/core-v6",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,13 +16,12 @@
16
16
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v5'"
17
17
  },
18
18
  "dependencies": {
19
- "@bananapus/721-hook-v6": "^0.0.28",
20
- "@bananapus/address-registry-v6": "^0.0.16",
21
- "@bananapus/buyback-hook-v6": "^0.0.24",
22
- "@bananapus/core-v6": "^0.0.30",
19
+ "@bananapus/721-hook-v6": "^0.0.30",
20
+ "@bananapus/buyback-hook-v6": "^0.0.25",
21
+ "@bananapus/core-v6": "^0.0.31",
23
22
  "@bananapus/ownable-v6": "^0.0.16",
24
23
  "@bananapus/permission-ids-v6": "^0.0.15",
25
- "@bananapus/router-terminal-v6": "^0.0.24",
24
+ "@bananapus/router-terminal-v6": "^0.0.25",
26
25
  "@bananapus/suckers-v6": "^0.0.20",
27
26
  "@openzeppelin/contracts": "^5.6.1"
28
27
  },
@@ -0,0 +1,25 @@
1
+ # Croptop Operations
2
+
3
+ ## Deployment Surface
4
+
5
+ - [`src/CTDeployer.sol`](../src/CTDeployer.sol) is the first stop for project launch shape, hook forwarding, and optional sucker integration.
6
+ - [`script/Deploy.s.sol`](../script/Deploy.s.sol) and [`script/ConfigureFeeProject.s.sol`](../script/ConfigureFeeProject.s.sol) cover the current deployment and fee-project wiring.
7
+ - [`src/structs/`](../src/structs/) contains config types that often drift from memory.
8
+
9
+ ## Change Checklist
10
+
11
+ - If you edit posting criteria, verify both direct publisher calls and deployer-created project flows.
12
+ - If you edit fee behavior, check both the designated fee project path and any exemption behavior.
13
+ - If you edit burn-lock ownership assumptions, confirm the intended irreversibility still holds.
14
+ - If you edit data-hook forwarding, re-check sucker-related fee-free cash-out behavior.
15
+
16
+ ## Common Failure Modes
17
+
18
+ - Publishing bug is blamed on the publisher when the deployer packaged the project or hook incorrectly.
19
+ - Immutable-owner expectations are missed after ownership moves into [`src/CTProjectOwner.sol`](../src/CTProjectOwner.sol).
20
+ - Content reuse or duplicate-post behavior changes and silently alters user-facing publishing semantics.
21
+
22
+ ## Useful Proof Points
23
+
24
+ - [`test/fork/`](../test/fork/) when deployment shape matters.
25
+ - [`script/helpers/`](../script/helpers/) if the issue is really script/config assembly.
@@ -0,0 +1,27 @@
1
+ # Croptop Runtime
2
+
3
+ ## Contract Roles
4
+
5
+ - [`src/CTPublisher.sol`](../src/CTPublisher.sol) validates posts, configures or reuses tiers, mints first copies, and routes Croptop fees.
6
+ - [`src/CTDeployer.sol`](../src/CTDeployer.sol) packages project deployment, hook forwarding, and optional sucker support.
7
+ - [`src/CTProjectOwner.sol`](../src/CTProjectOwner.sol) is the burn-lock ownership helper for immutable administration patterns.
8
+
9
+ ## Runtime Path
10
+
11
+ 1. A project is deployed or configured with Croptop posting rules.
12
+ 2. Publishers call into [`src/CTPublisher.sol`](../src/CTPublisher.sol) with content, supply, and pricing data.
13
+ 3. The publisher validates category-level rules, creates or reuses tiers, mints the first copy, and routes fees and proceeds.
14
+ 4. If the project uses the deployer wrapper, data-hook calls forward through [`src/CTDeployer.sol`](../src/CTDeployer.sol).
15
+
16
+ ## High-Risk Areas
17
+
18
+ - Posting criteria: category rules are the policy surface that protects the project from bad content or bad economics.
19
+ - Fee routing: fee-project assumptions and fee exemptions are operationally important.
20
+ - Tier reuse and duplicate content: content identity is part of runtime behavior, not just metadata.
21
+ - Burn-lock ownership: once ownership moves into the lock helper, reversibility expectations change drastically.
22
+
23
+ ## Tests To Trust First
24
+
25
+ - [`test/regression/`](../test/regression/) for pinned content and tier edge cases.
26
+ - [`test/fork/`](../test/fork/) for live integration assumptions.
27
+ - [`test/`](../test/) broadly when the issue could be in publisher or deployer behavior rather than one isolated function.
@@ -64,29 +64,10 @@ contract DeployScript is Script, Sphinx {
64
64
  }
65
65
 
66
66
  function deploy() public sphinx {
67
- // If the fee project id is 0, then we want to deploy a new fee project but only if the publisher
68
- // singleton doesn't already exist. Re-running with FEE_PROJECT_ID=0 would create a second fee project
69
- // with different CREATE2 addresses, stranding the previous suite.
70
- if (FEE_PROJECT_ID == 0) {
71
- // Check if the publisher already exists by scanning existing project IDs.
72
- uint256 _existingCount = core.projects.count();
73
- bool _found;
74
- for (uint256 _candidateId = 1; _candidateId <= _existingCount; _candidateId++) {
75
- (, bool _exists) = _isDeployed({
76
- salt: PUBLISHER_SALT,
77
- creationCode: type(CTPublisher).creationCode,
78
- arguments: abi.encode(core.directory, core.permissions, _candidateId, TRUSTED_FORWARDER)
79
- });
80
- if (_exists) {
81
- FEE_PROJECT_ID = _candidateId;
82
- _found = true;
83
- break;
84
- }
85
- }
86
- if (!_found) {
87
- FEE_PROJECT_ID = core.projects.createFor(safeAddress());
88
- }
89
- }
67
+ // Canonical Croptop deployments must bind fees to an explicit fee project. Autodiscovering the first
68
+ // matching publisher by scanning project IDs is unsafe because a preexisting publisher can pin fees to
69
+ // the wrong project forever.
70
+ require(FEE_PROJECT_ID != 0, "explicit fee project id required");
90
71
 
91
72
  CTPublisher publisher;
92
73
  {
@@ -248,6 +248,9 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
248
248
  }
249
249
 
250
250
  /// @notice Deploy a simple project meant to receive posts from Croptop templates.
251
+ /// @dev The initial project owner is intentionally granted direct hook-management permissions from
252
+ /// `CTDeployer`. This means the owner/operator can bypass the Croptop publisher path and interact
253
+ /// with the hook directly if they choose to. That is an explicit product tradeoff.
251
254
  /// @param owner The address that'll own the project.
252
255
  /// @param projectConfig The configuration for the project.
253
256
  /// @param suckerDeploymentConfiguration The configuration for the suckers to deploy.
@@ -342,7 +345,8 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
342
345
  // These permissions are granted from CTDeployer (address(this)) to the initial owner.
343
346
  // The hook checks permissions against hook.owner(), which after claimCollectionOwnershipOf() resolves
344
347
  // dynamically via PROJECTS.ownerOf(projectId). Before claiming, CTDeployer is the static hook owner,
345
- // so these permissions allow the project owner to manage tiers through CTDeployer.
348
+ // so these permissions allow the project owner to manage tiers through CTDeployer. As a tradeoff,
349
+ // the owner can also bypass the Croptop publisher surface until ownership is claimed away.
346
350
  uint8[] memory permissionIds = new uint8[](4);
347
351
  permissionIds[0] = JBPermissionIds.ADJUST_721_TIERS;
348
352
  permissionIds[1] = JBPermissionIds.SET_721_METADATA;
@@ -5,6 +5,7 @@ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Tiers
5
5
  import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
6
6
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
7
7
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
8
+ import {JB721TierConfigFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierConfigFlags.sol";
8
9
  import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
9
10
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
10
11
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
@@ -33,6 +34,7 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
33
34
  error CTPublisher_MaxTotalSupplyLessThanMin(uint256 min, uint256 max);
34
35
  error CTPublisher_NotInAllowList(address addr, address[] allowedAddresses);
35
36
  error CTPublisher_PriceTooSmall(uint256 price, uint256 minimumPrice);
37
+ error CTPublisher_FeePaymentFailed(uint256 feeAmount);
36
38
  error CTPublisher_SplitPercentExceedsMaximum(uint256 splitPercent, uint256 maximumSplitPercent);
37
39
  error CTPublisher_TotalSupplyTooBig(uint256 totalSupply, uint256 maximumTotalSupply);
38
40
  error CTPublisher_TotalSupplyTooSmall(uint256 totalSupply, uint256 minimumTotalSupply);
@@ -416,7 +418,8 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
416
418
  IJBTerminal feeTerminal =
417
419
  DIRECTORY.primaryTerminalOf({projectId: FEE_PROJECT_ID, token: JBConstants.NATIVE_TOKEN});
418
420
 
419
- // Make the fee payment. Wrapped in try-catch so a reverting fee terminal doesn't block mints.
421
+ // Make the fee payment. If the fee sink is unavailable, refund the fee to the caller
422
+ // rather than trapping or silently redirecting protocol funds.
420
423
  // slither-disable-next-line unused-return
421
424
  try feeTerminal.pay{value: payValue}({
422
425
  projectId: FEE_PROJECT_ID,
@@ -428,13 +431,9 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
428
431
  metadata: feeMetadata
429
432
  }) {}
430
433
  catch {
431
- // If the fee payment fails, send the fee to the beneficiary instead.
432
- (bool success,) = feeBeneficiary.call{value: payValue}("");
433
- if (!success) {
434
- // If that also fails, send to the msg.sender.
435
- // slither-disable-next-line low-level-calls
436
- (success,) = msg.sender.call{value: payValue}("");
437
- }
434
+ // slither-disable-next-line low-level-calls
435
+ (bool success,) = _msgSender().call{value: payValue}("");
436
+ if (!success) revert CTPublisher_FeePaymentFailed(payValue);
438
437
  }
439
438
  }
440
439
  }
@@ -569,13 +568,15 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
569
568
  encodedIPFSUri: post.encodedIPFSUri,
570
569
  category: post.category,
571
570
  discountPercent: 0,
572
- allowOwnerMint: false,
573
- useReserveBeneficiaryAsDefault: false,
574
- transfersPausable: false,
575
- useVotingUnits: true,
576
- cantBeRemoved: false,
577
- cantIncreaseDiscountPercent: false,
578
- cantBuyWithCredits: false,
571
+ flags: JB721TierConfigFlags({
572
+ allowOwnerMint: false,
573
+ useReserveBeneficiaryAsDefault: false,
574
+ transfersPausable: false,
575
+ useVotingUnits: true,
576
+ cantBeRemoved: false,
577
+ cantIncreaseDiscountPercent: false,
578
+ cantBuyWithCredits: false
579
+ }),
579
580
  splitPercent: post.splitPercent,
580
581
  splits: post.splits
581
582
  });
@@ -15,6 +15,7 @@ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
15
15
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
16
16
 
17
17
  import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
18
+ import {JB721TierConfigFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierConfigFlags.sol";
18
19
 
19
20
  import {CTPublisher} from "../src/CTPublisher.sol";
20
21
  import {CTAllowedPost} from "../src/structs/CTAllowedPost.sol";
@@ -809,13 +810,15 @@ contract TestCTPublisher is Test {
809
810
  encodedIPFSUri: keccak256("split-beneficiary-test"),
810
811
  category: 5,
811
812
  discountPercent: 0,
812
- allowOwnerMint: false,
813
- useReserveBeneficiaryAsDefault: false,
814
- transfersPausable: false,
815
- useVotingUnits: true,
816
- cantBeRemoved: false,
817
- cantIncreaseDiscountPercent: false,
818
- cantBuyWithCredits: false,
813
+ flags: JB721TierConfigFlags({
814
+ allowOwnerMint: false,
815
+ useReserveBeneficiaryAsDefault: false,
816
+ transfersPausable: false,
817
+ useVotingUnits: true,
818
+ cantBeRemoved: false,
819
+ cantIncreaseDiscountPercent: false,
820
+ cantBuyWithCredits: false
821
+ }),
819
822
  splitPercent: 250_000_000,
820
823
  splits: splits
821
824
  });
@@ -0,0 +1,213 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+
7
+ import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
8
+ import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
9
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
10
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
11
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
12
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
13
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
14
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
15
+ import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
16
+ import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
17
+ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
18
+ import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
19
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
20
+ import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
21
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
22
+
23
+ import {CTDeployer} from "../../src/CTDeployer.sol";
24
+ import {CTPublisher} from "../../src/CTPublisher.sol";
25
+ import {ICTPublisher} from "../../src/interfaces/ICTPublisher.sol";
26
+ import {CTDeployerAllowedPost} from "../../src/structs/CTDeployerAllowedPost.sol";
27
+ import {CTProjectConfig} from "../../src/structs/CTProjectConfig.sol";
28
+ import {CTSuckerDeploymentConfig} from "../../src/structs/CTSuckerDeploymentConfig.sol";
29
+
30
+ contract MockProjects {
31
+ uint256 public countValue;
32
+ address public ownerOfProject;
33
+
34
+ function setCount(uint256 count_) external {
35
+ countValue = count_;
36
+ }
37
+
38
+ function setOwner(address owner_) external {
39
+ ownerOfProject = owner_;
40
+ }
41
+
42
+ function count() external view returns (uint256) {
43
+ return countValue;
44
+ }
45
+
46
+ function ownerOf(uint256) external view returns (address) {
47
+ return ownerOfProject;
48
+ }
49
+
50
+ function transferFrom(address, address to, uint256) external {
51
+ ownerOfProject = to;
52
+ }
53
+ }
54
+
55
+ contract MockController {
56
+ MockProjects public immutable PROJECTS;
57
+ // forge-lint: disable-next-line(screaming-snake-case-immutable)
58
+ uint256 public immutable nextProjectId;
59
+
60
+ constructor(MockProjects projects_, uint256 nextProjectId_) {
61
+ PROJECTS = projects_;
62
+ nextProjectId = nextProjectId_;
63
+ }
64
+
65
+ function launchProjectFor(
66
+ address,
67
+ string calldata,
68
+ JBRulesetConfig[] calldata,
69
+ JBTerminalConfig[] calldata,
70
+ string calldata
71
+ )
72
+ external
73
+ view
74
+ returns (uint256)
75
+ {
76
+ return nextProjectId;
77
+ }
78
+ }
79
+
80
+ contract MockSuckerRegistry {
81
+ function isSuckerOf(uint256, address) external pure returns (bool) {
82
+ return false;
83
+ }
84
+
85
+ function deploySuckersFor(
86
+ uint256,
87
+ bytes32,
88
+ JBSuckerDeployerConfig[] calldata
89
+ )
90
+ external
91
+ pure
92
+ returns (address[] memory suckers)
93
+ {
94
+ return suckers;
95
+ }
96
+ }
97
+
98
+ contract PermissionedHook is JBPermissioned {
99
+ // forge-lint: disable-next-line(screaming-snake-case-immutable)
100
+ address public immutable ownerAccount;
101
+ // forge-lint: disable-next-line(screaming-snake-case-immutable)
102
+ uint256 public immutable projectId;
103
+ bool public adjusted;
104
+
105
+ constructor(IJBPermissions permissions, address ownerAccount_, uint256 projectId_) JBPermissioned(permissions) {
106
+ ownerAccount = ownerAccount_;
107
+ projectId = projectId_;
108
+ }
109
+
110
+ // forge-lint: disable-next-line(mixed-case-function)
111
+ function PROJECT_ID() external view returns (uint256) {
112
+ return projectId;
113
+ }
114
+
115
+ function owner() external view returns (address) {
116
+ return ownerAccount;
117
+ }
118
+
119
+ function adjustTiers(JB721TierConfig[] calldata, uint256[] calldata) external {
120
+ _requirePermissionFrom(ownerAccount, projectId, JBPermissionIds.ADJUST_721_TIERS);
121
+ adjusted = true;
122
+ }
123
+ }
124
+
125
+ contract MockHookDeployer {
126
+ IJB721TiersHook public hook;
127
+
128
+ function setHook(IJB721TiersHook hook_) external {
129
+ hook = hook_;
130
+ }
131
+
132
+ function deployHookFor(
133
+ uint256,
134
+ JBDeploy721TiersHookConfig calldata,
135
+ bytes32
136
+ )
137
+ external
138
+ view
139
+ returns (IJB721TiersHook)
140
+ {
141
+ return hook;
142
+ }
143
+ }
144
+
145
+ contract DeployerPermissionBypassTest is Test {
146
+ JBPermissions permissions;
147
+ MockProjects projects;
148
+ MockHookDeployer hookDeployer;
149
+ MockSuckerRegistry suckerRegistry;
150
+ MockController controller;
151
+ CTPublisher publisher;
152
+ CTDeployer deployer;
153
+ PermissionedHook hook;
154
+
155
+ address owner = makeAddr("owner");
156
+
157
+ function setUp() public {
158
+ permissions = new JBPermissions(address(0));
159
+ projects = new MockProjects();
160
+ projects.setCount(5);
161
+ hookDeployer = new MockHookDeployer();
162
+ suckerRegistry = new MockSuckerRegistry();
163
+ publisher = new CTPublisher(IJBDirectory(makeAddr("directory")), permissions, 1, address(0));
164
+ deployer = new CTDeployer(
165
+ permissions,
166
+ IJBProjects(address(projects)),
167
+ IJB721TiersHookDeployer(address(hookDeployer)),
168
+ ICTPublisher(address(publisher)),
169
+ IJBSuckerRegistry(address(suckerRegistry)),
170
+ address(0)
171
+ );
172
+ hook = new PermissionedHook(permissions, address(deployer), 6);
173
+ hookDeployer.setHook(IJB721TiersHook(address(hook)));
174
+ controller = new MockController(projects, 6);
175
+ }
176
+
177
+ function test_projectOwnerCanBypassCroptopAndCallHookDirectlyAfterDeployment() public {
178
+ CTDeployerAllowedPost[] memory allowedPosts = new CTDeployerAllowedPost[](1);
179
+ allowedPosts[0] = CTDeployerAllowedPost({
180
+ category: 1,
181
+ minimumPrice: 1 ether,
182
+ minimumTotalSupply: 10,
183
+ maximumTotalSupply: 10,
184
+ maximumSplitPercent: 0,
185
+ allowedAddresses: new address[](0)
186
+ });
187
+
188
+ CTProjectConfig memory config = CTProjectConfig({
189
+ terminalConfigurations: new JBTerminalConfig[](0),
190
+ projectUri: "ipfs://project",
191
+ allowedPosts: allowedPosts,
192
+ contractUri: "ipfs://contract",
193
+ name: "Croptop",
194
+ symbol: "CT",
195
+ salt: bytes32(0)
196
+ });
197
+
198
+ CTSuckerDeploymentConfig memory suckerConfig =
199
+ CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
200
+
201
+ deployer.deployProjectFor(owner, config, suckerConfig, IJBController(address(controller)));
202
+
203
+ assertEq(projects.ownerOf(6), owner, "deployment should hand the project NFT to the owner");
204
+
205
+ JB721TierConfig[] memory arbitraryTiers = new JB721TierConfig[](0);
206
+ uint256[] memory removals = new uint256[](0);
207
+
208
+ vm.prank(owner);
209
+ PermissionedHook(address(hook)).adjustTiers(arbitraryTiers, removals);
210
+
211
+ assertTrue(hook.adjusted(), "project owner can mutate the hook without going through Croptop");
212
+ }
213
+ }