@croptop/core-v6 0.0.38 → 0.0.39

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 (40) 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 +52 -51
  6. package/src/interfaces/ICTDeployer.sol +2 -2
  7. package/ADMINISTRATION.md +0 -94
  8. package/ARCHITECTURE.md +0 -96
  9. package/AUDIT_INSTRUCTIONS.md +0 -88
  10. package/RISKS.md +0 -78
  11. package/SKILLS.md +0 -46
  12. package/STYLE_GUIDE.md +0 -610
  13. package/USER_JOURNEYS.md +0 -134
  14. package/foundry.lock +0 -11
  15. package/slither-ci.config.json +0 -10
  16. package/sphinx.lock +0 -507
  17. package/test/CTDeployer.t.sol +0 -616
  18. package/test/CTProjectOwner.t.sol +0 -185
  19. package/test/CTPublisher.t.sol +0 -869
  20. package/test/ClaimCollectionOwnership.t.sol +0 -315
  21. package/test/CroptopAttacks.t.sol +0 -437
  22. package/test/Fork.t.sol +0 -227
  23. package/test/TestAuditGaps.sol +0 -696
  24. package/test/Test_MetadataGeneration.t.sol +0 -79
  25. package/test/audit/CodexNemesisCroptopPublisherBoundary.t.sol +0 -329
  26. package/test/audit/CodexNemesisCurrencyPoCs.t.sol +0 -371
  27. package/test/audit/CodexNemesisFreshRound.t.sol +0 -395
  28. package/test/audit/CodexNemesisMetadataShadow.t.sol +0 -196
  29. package/test/audit/CodexNemesisPoCs.t.sol +0 -263
  30. package/test/audit/CodexNemesisPolicyReuse.t.sol +0 -168
  31. package/test/audit/CodexNemesisUriDrift.t.sol +0 -252
  32. package/test/audit/DeployerPermissionBypass.t.sol +0 -213
  33. package/test/audit/EmptyPostFeeBypass.t.sol +0 -53
  34. package/test/audit/FeeBeneficiaryReentrancy.t.sol +0 -247
  35. package/test/audit/FeeFallbackBlackhole.t.sol +0 -263
  36. package/test/audit/Pass12Fixes.t.sol +0 -388
  37. package/test/fork/PublishFork.t.sol +0 -440
  38. package/test/regression/DuplicateUriFeeEvasion.t.sol +0 -312
  39. package/test/regression/FeeEvasion.t.sol +0 -286
  40. package/test/regression/StaleTierIdMapping.t.sol +0 -228
@@ -1,315 +0,0 @@
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 {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
8
- import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
9
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
10
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
11
- import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
12
- import {IJBOwnable} from "@bananapus/ownable-v6/src/interfaces/IJBOwnable.sol";
13
- import {IJB721Hook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Hook.sol";
14
- import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
15
- import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
16
- import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
17
- import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.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 {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
21
-
22
- import {CTDeployer} from "../src/CTDeployer.sol";
23
- import {CTPublisher} from "../src/CTPublisher.sol";
24
- import {ICTPublisher} from "../src/interfaces/ICTPublisher.sol";
25
- import {CTAllowedPost} from "../src/structs/CTAllowedPost.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
- /// @title ClaimCollectionOwnershipTest
31
- /// @notice Integration tests for the post-claimCollectionOwnership permission lifecycle:
32
- /// 1. Deploy a croptop collection
33
- /// 2. Claim ownership (transfers hook ownership to project)
34
- /// 3. Verify permissions are correctly transferred
35
- /// 4. Verify post-claim the hook owner changes and publisher permissions must be re-granted
36
- contract ClaimCollectionOwnershipTest is Test {
37
- CTDeployer ctDeployer;
38
- CTPublisher publisher;
39
-
40
- IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
41
- IJBProjects projects = IJBProjects(makeAddr("projects"));
42
- IJB721TiersHookDeployer hookDeployer = IJB721TiersHookDeployer(makeAddr("hookDeployer"));
43
- IJBSuckerRegistry suckerRegistry = IJBSuckerRegistry(makeAddr("suckerRegistry"));
44
- IJBController controller = IJBController(makeAddr("controller"));
45
-
46
- address owner = makeAddr("owner");
47
- address newOwner = makeAddr("newOwner");
48
- address unauthorized = makeAddr("unauthorized");
49
- address hookAddr = makeAddr("hook");
50
-
51
- uint256 projectCount = 5;
52
- uint256 deployedProjectId = projectCount + 1; // 6
53
- uint256 feeProjectId = 1;
54
-
55
- // Track which permissions were set.
56
- PermissionRecord[] permissionRecords;
57
-
58
- struct PermissionRecord {
59
- address account;
60
- address operator;
61
- uint256 projectId;
62
- uint8[] permissionIds;
63
- }
64
-
65
- function setUp() public {
66
- // Mock permissions.setPermissionsFor (called in CTDeployer constructor + deployment).
67
- vm.mockCall(
68
- address(permissions), abi.encodeWithSelector(IJBPermissions.setPermissionsFor.selector), abi.encode()
69
- );
70
-
71
- // Mock permissions.hasPermission to return true by default.
72
- vm.mockCall(
73
- address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
74
- );
75
-
76
- // Mock sucker registry.
77
- vm.mockCall(
78
- address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
79
- );
80
-
81
- // Deploy publisher.
82
- publisher = new CTPublisher(IJBDirectory(makeAddr("directory")), permissions, feeProjectId, address(0));
83
-
84
- // Deploy the CTDeployer.
85
- ctDeployer = new CTDeployer(
86
- permissions, projects, hookDeployer, ICTPublisher(address(publisher)), suckerRegistry, address(0)
87
- );
88
-
89
- // Fund test accounts.
90
- vm.deal(owner, 100 ether);
91
- vm.deal(newOwner, 100 ether);
92
- vm.deal(unauthorized, 100 ether);
93
- }
94
-
95
- //*********************************************************************//
96
- // --- Full Lifecycle: Deploy -> Claim -> Verify ---------------------- //
97
- //*********************************************************************//
98
-
99
- /// @notice Tests the full lifecycle: deploy project, then claim collection ownership.
100
- /// After claiming, the hook's owner should become the project (via transferOwnershipToProject).
101
- function test_fullLifecycle_deploy_claim_ownershipTransfers() public {
102
- // Step 1: Deploy the project.
103
- _mockDeployProjectInfra();
104
-
105
- CTProjectConfig memory config = _defaultProjectConfig();
106
- CTSuckerDeploymentConfig memory suckerConfig = _emptySuckerConfig();
107
-
108
- (uint256 projectId, IJB721TiersHook hook) = ctDeployer.deployProjectFor(owner, config, suckerConfig, controller);
109
- assertEq(projectId, deployedProjectId, "project ID should match");
110
-
111
- // Step 2: After deployment, the CTDeployer is the hook's owner.
112
- // (The deployer owns the hook because it deployed it.)
113
- // Mock hook.PROJECT_ID() to return deployedProjectId.
114
- vm.mockCall(address(hook), abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(projectId));
115
-
116
- // Mock PROJECTS.ownerOf(projectId) to return owner.
117
- vm.mockCall(address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), abi.encode(owner));
118
-
119
- // Mock JBOwnable.transferOwnershipToProject to succeed.
120
- vm.mockCall(address(hook), abi.encodeWithSelector(IJBOwnable.transferOwnershipToProject.selector), abi.encode());
121
-
122
- // Step 3: Owner claims collection ownership.
123
- vm.prank(owner);
124
- ctDeployer.claimCollectionOwnershipOf(hook);
125
-
126
- // Step 4: After claiming, the hook ownership has been transferred to the project.
127
- // Verify transferOwnershipToProject was called with the correct projectId.
128
- // (If it wasn't, the mock would not match and the call would have failed.)
129
- }
130
-
131
- /// @notice After claiming ownership, the hook's owner is resolved via PROJECTS.ownerOf(projectId).
132
- /// If the project NFT is transferred to a new owner, the new owner becomes the hook's owner.
133
- function test_postClaim_projectTransfer_newOwnerControlsHook() public {
134
- // Mock hook.PROJECT_ID().
135
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
136
-
137
- // Initially, 'owner' owns the project.
138
- vm.mockCall(
139
- address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(owner)
140
- );
141
-
142
- // Mock transferOwnershipToProject.
143
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.transferOwnershipToProject.selector), abi.encode());
144
-
145
- // Owner claims collection ownership.
146
- vm.prank(owner);
147
- ctDeployer.claimCollectionOwnershipOf(IJB721TiersHook(hookAddr));
148
-
149
- // Now simulate the project NFT being transferred to newOwner.
150
- vm.mockCall(
151
- address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(newOwner)
152
- );
153
-
154
- // After the hook is owned by the project, the JBOwnable.owner() resolves to PROJECTS.ownerOf(projectId).
155
- // Verify that if newOwner holds the project NFT, they are the effective owner.
156
- // Mock hook.owner() to return newOwner (simulating JBOwnable resolution).
157
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(newOwner));
158
-
159
- // Verify: the old owner can no longer claim (since they don't own the project NFT anymore).
160
- vm.prank(owner);
161
- vm.expectRevert(
162
- abi.encodeWithSelector(CTDeployer.CTDeployer_NotOwnerOfProject.selector, deployedProjectId, hookAddr, owner)
163
- );
164
- ctDeployer.claimCollectionOwnershipOf(IJB721TiersHook(hookAddr));
165
- }
166
-
167
- /// @notice After claimCollectionOwnership, the deployer's permissions from address(this) are
168
- /// no longer relevant. The project owner must grant CTPublisher the ADJUST_721_TIERS
169
- /// permission for mintFrom() to work. Without it, posts would revert.
170
- ///
171
- /// This test verifies the permission gap: after claim, the hook checks permissions
172
- /// against the project (not the deployer), so the publisher needs new permissions.
173
- function test_postClaim_publisherNeedsNewPermissions() public {
174
- // After claiming, hook.owner() resolves to PROJECTS.ownerOf(projectId) = owner.
175
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(owner));
176
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
177
-
178
- // The publisher's configurePostingCriteriaFor calls _requirePermissionFrom(hook.owner(), ...).
179
- // After claiming, hook.owner() is the project owner, not the deployer.
180
- // So the project owner must grant CTPublisher the ADJUST_721_TIERS permission.
181
-
182
- // Mock permissions: simulate that the project owner has NOT yet granted the publisher permission.
183
- vm.mockCall(
184
- address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false)
185
- );
186
-
187
- // The publisher's configurePostingCriteriaFor should revert because the publisher
188
- // doesn't have permission from the new owner.
189
- CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
190
- allowedPosts[0] = CTAllowedPost({
191
- hook: hookAddr,
192
- category: 1,
193
- minimumPrice: 0.01 ether,
194
- minimumTotalSupply: 1,
195
- maximumTotalSupply: 100,
196
- maximumSplitPercent: 0,
197
- allowedAddresses: new address[](0)
198
- });
199
-
200
- // Expect the publisher to revert due to missing permissions.
201
- vm.expectRevert(
202
- abi.encodeWithSelector(
203
- JBPermissioned.JBPermissioned_Unauthorized.selector,
204
- owner, // account (hook.owner())
205
- address(this), // caller (us, calling configurePostingCriteriaFor)
206
- deployedProjectId,
207
- JBPermissionIds.ADJUST_721_TIERS
208
- )
209
- );
210
- publisher.configurePostingCriteriaFor(allowedPosts);
211
- }
212
-
213
- /// @notice After granting the publisher new permissions, posting works again.
214
- function test_postClaim_publisherWorksAfterPermissionGrant() public {
215
- // After claiming, hook.owner() = owner.
216
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(owner));
217
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
218
-
219
- // The project owner grants CTPublisher the ADJUST_721_TIERS permission.
220
- // Simulate: hasPermission returns true for publisher calling from owner's context.
221
- vm.mockCall(
222
- address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
223
- );
224
-
225
- CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
226
- allowedPosts[0] = CTAllowedPost({
227
- hook: hookAddr,
228
- category: 2,
229
- minimumPrice: 0.05 ether,
230
- minimumTotalSupply: 1,
231
- maximumTotalSupply: 50,
232
- maximumSplitPercent: 0,
233
- allowedAddresses: new address[](0)
234
- });
235
-
236
- // This should succeed because the publisher has the correct permission.
237
- publisher.configurePostingCriteriaFor(allowedPosts);
238
-
239
- // Verify the allowance was set.
240
- (uint256 minPrice, uint256 minSupply, uint256 maxSupply,,) = publisher.allowanceFor(hookAddr, 2);
241
- assertEq(minPrice, 0.05 ether, "minimum price should be configured after re-grant");
242
- assertEq(minSupply, 1, "minimum supply should be configured");
243
- assertEq(maxSupply, 50, "maximum supply should be configured");
244
- }
245
-
246
- /// @notice Claiming twice for the same hook should succeed (idempotent — just calls
247
- /// transferOwnershipToProject again, which the hook handles internally).
248
- function test_claim_calledTwice_succeeds() public {
249
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
250
- vm.mockCall(
251
- address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(owner)
252
- );
253
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.transferOwnershipToProject.selector), abi.encode());
254
-
255
- // First claim.
256
- vm.prank(owner);
257
- ctDeployer.claimCollectionOwnershipOf(IJB721TiersHook(hookAddr));
258
-
259
- // Second claim (should not revert with our mocks).
260
- vm.prank(owner);
261
- ctDeployer.claimCollectionOwnershipOf(IJB721TiersHook(hookAddr));
262
- }
263
-
264
- /// @notice Non-owner cannot claim even after project ownership changes.
265
- function test_claim_revertsForNonProjectOwner() public {
266
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
267
- vm.mockCall(
268
- address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(owner)
269
- );
270
-
271
- vm.prank(unauthorized);
272
- vm.expectRevert(
273
- abi.encodeWithSelector(
274
- CTDeployer.CTDeployer_NotOwnerOfProject.selector, deployedProjectId, hookAddr, unauthorized
275
- )
276
- );
277
- ctDeployer.claimCollectionOwnershipOf(IJB721TiersHook(hookAddr));
278
- }
279
-
280
- //*********************************************************************//
281
- // --- Internal Helpers ---------------------------------------------- //
282
- //*********************************************************************//
283
-
284
- function _defaultProjectConfig() internal pure returns (CTProjectConfig memory) {
285
- return CTProjectConfig({
286
- terminalConfigurations: new JBTerminalConfig[](0),
287
- projectUri: "https://croptop.test/",
288
- allowedPosts: new CTDeployerAllowedPost[](0),
289
- contractUri: "https://croptop.test/contract",
290
- name: "TestCrop",
291
- symbol: "TC",
292
- salt: bytes32(0)
293
- });
294
- }
295
-
296
- function _emptySuckerConfig() internal pure returns (CTSuckerDeploymentConfig memory) {
297
- return CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
298
- }
299
-
300
- function _mockDeployProjectInfra() internal {
301
- vm.mockCall(address(controller), abi.encodeWithSelector(IJBController.PROJECTS.selector), abi.encode(projects));
302
- vm.mockCall(address(projects), abi.encodeWithSelector(IJBProjects.count.selector), abi.encode(projectCount));
303
- vm.mockCall(
304
- address(hookDeployer),
305
- abi.encodeWithSelector(IJB721TiersHookDeployer.deployHookFor.selector),
306
- abi.encode(IJB721TiersHook(hookAddr))
307
- );
308
- vm.mockCall(
309
- address(controller),
310
- abi.encodeWithSelector(IJBController.launchProjectFor.selector),
311
- abi.encode(deployedProjectId)
312
- );
313
- vm.mockCall(address(projects), abi.encodeWithSelector(IERC721.transferFrom.selector), abi.encode());
314
- }
315
- }