@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,213 +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 {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
- }
@@ -1,53 +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 {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
9
- import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
10
-
11
- import {CTPublisher} from "../../src/CTPublisher.sol";
12
- import {CTPost} from "../../src/structs/CTPost.sol";
13
-
14
- /// @title M24_EmptyPostFeeBypass
15
- /// @notice Verifies that calling mintFrom with an empty posts array reverts,
16
- /// preventing fee-free metadata shadowing via additionalPayMetadata.
17
- contract M24_EmptyPostFeeBypass is Test {
18
- CTPublisher publisher;
19
-
20
- IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
21
- IJBDirectory directory = IJBDirectory(makeAddr("directory"));
22
- address hookAddr = makeAddr("hook");
23
- address poster = makeAddr("poster");
24
-
25
- uint256 feeProjectId = 1;
26
-
27
- function setUp() public {
28
- publisher = new CTPublisher(directory, permissions, feeProjectId, address(0));
29
- vm.deal(poster, 10 ether);
30
- }
31
-
32
- /// @notice mintFrom with empty posts should revert with CTPublisher_NoPosts.
33
- function test_revert_emptyPostsArray() public {
34
- CTPost[] memory emptyPosts = new CTPost[](0);
35
-
36
- vm.prank(poster);
37
- vm.expectRevert(CTPublisher.CTPublisher_NoPosts.selector);
38
- publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), emptyPosts, poster, poster, "", "");
39
- }
40
-
41
- /// @notice mintFrom with empty posts and crafted additionalPayMetadata should still revert.
42
- function test_revert_emptyPostsWithMetadata() public {
43
- CTPost[] memory emptyPosts = new CTPost[](0);
44
-
45
- // Attacker preloads additionalPayMetadata with hook mint metadata.
46
- bytes memory craftedMetadata =
47
- abi.encodePacked(bytes32(uint256(1)), bytes4(0xdeadbeef), uint256(32), uint256(1));
48
-
49
- vm.prank(poster);
50
- vm.expectRevert(CTPublisher.CTPublisher_NoPosts.selector);
51
- publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), emptyPosts, poster, poster, craftedMetadata, "");
52
- }
53
- }
@@ -1,247 +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 {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
8
- import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
9
- import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
10
- import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
11
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
12
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
13
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
14
- import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
15
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
16
-
17
- import {CTPublisher} from "../../src/CTPublisher.sol";
18
- import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
19
- import {CTPost} from "../../src/structs/CTPost.sol";
20
-
21
- contract MockPermissions is IJBPermissions {
22
- // forge-lint: disable-next-line(mixed-case-function)
23
- function WILDCARD_PROJECT_ID() external pure returns (uint256) {
24
- return 0;
25
- }
26
-
27
- function permissionsOf(address, address, uint256) external pure returns (uint256) {
28
- return 0;
29
- }
30
-
31
- function hasPermission(address, address, uint256, uint256, bool, bool) external pure returns (bool) {
32
- return true;
33
- }
34
-
35
- function hasPermissions(address, address, uint256, uint256[] calldata, bool, bool) external pure returns (bool) {
36
- return true;
37
- }
38
-
39
- function setPermissionsFor(address, JBPermissionsData calldata) external {}
40
- }
41
-
42
- contract MockStore {
43
- function maxTierIdOf(address) external pure returns (uint256) {
44
- return 0;
45
- }
46
-
47
- function isTierRemoved(address, uint256) external pure returns (bool) {
48
- return false;
49
- }
50
-
51
- function tierOf(address, uint256, bool) external pure returns (JB721Tier memory tier) {
52
- return tier;
53
- }
54
-
55
- // Accept direct ether transfers (required alongside payable fallback to silence compiler warning 3628).
56
- receive() external payable {}
57
-
58
- fallback() external payable {}
59
- }
60
-
61
- contract MockHook {
62
- uint256 public immutable PROJECT_ID;
63
- IJB721TiersHookStore public immutable STORE;
64
- address public immutable OWNER;
65
-
66
- constructor(uint256 projectId, IJB721TiersHookStore store_, address owner_) {
67
- PROJECT_ID = projectId;
68
- STORE = store_;
69
- OWNER = owner_;
70
- }
71
-
72
- function adjustTiers(JB721TierConfig[] calldata, uint256[] calldata) external {}
73
-
74
- function METADATA_ID_TARGET() external view returns (address) {
75
- return address(this);
76
- }
77
-
78
- function owner() external view returns (address) {
79
- return OWNER;
80
- }
81
-
82
- // Accept direct ether transfers (required alongside payable fallback to silence compiler warning 3628).
83
- receive() external payable {}
84
-
85
- fallback() external payable {}
86
- }
87
-
88
- contract MockDirectory {
89
- address public projectTerminal;
90
- address public feeTerminal;
91
-
92
- function setTerminals(address projectTerminal_, address feeTerminal_) external {
93
- projectTerminal = projectTerminal_;
94
- feeTerminal = feeTerminal_;
95
- }
96
-
97
- function primaryTerminalOf(uint256 projectId, address) external view returns (IJBTerminal) {
98
- return IJBTerminal(projectId == 1 ? feeTerminal : projectTerminal);
99
- }
100
-
101
- // Accept direct ether transfers (required alongside payable fallback to silence compiler warning 3628).
102
- receive() external payable {}
103
-
104
- fallback() external payable {}
105
- }
106
-
107
- contract FeeTerminalRecorder {
108
- uint256 public callCount;
109
- uint256 public totalReceived;
110
- address public lastBeneficiary;
111
- uint256 public lastAmount;
112
-
113
- function pay(
114
- uint256,
115
- address,
116
- uint256 amount,
117
- address beneficiary,
118
- uint256,
119
- string calldata,
120
- bytes calldata
121
- )
122
- external
123
- payable
124
- returns (uint256)
125
- {
126
- callCount++;
127
- totalReceived += msg.value;
128
- lastBeneficiary = beneficiary;
129
- lastAmount = amount;
130
- return 0;
131
- }
132
-
133
- // Accept direct ether transfers (required alongside payable fallback to silence compiler warning 3628).
134
- receive() external payable {}
135
-
136
- fallback() external payable {}
137
- }
138
-
139
- contract ReentrantProjectTerminal {
140
- // forge-lint: disable-next-line(screaming-snake-case-immutable)
141
- CTPublisher public immutable publisher;
142
- // forge-lint: disable-next-line(screaming-snake-case-immutable)
143
- IJB721TiersHook public immutable hook;
144
- // forge-lint: disable-next-line(screaming-snake-case-immutable)
145
- address public immutable attackerFeeBeneficiary;
146
- bool internal entered;
147
-
148
- constructor(CTPublisher publisher_, IJB721TiersHook hook_, address attackerFeeBeneficiary_) {
149
- publisher = publisher_;
150
- hook = hook_;
151
- attackerFeeBeneficiary = attackerFeeBeneficiary_;
152
- }
153
-
154
- function pay(
155
- uint256,
156
- address,
157
- uint256,
158
- address,
159
- uint256,
160
- string calldata,
161
- bytes calldata
162
- )
163
- external
164
- payable
165
- returns (uint256)
166
- {
167
- if (!entered) {
168
- entered = true;
169
-
170
- CTPost[] memory posts = new CTPost[](1);
171
- posts[0] = CTPost({
172
- encodedIPFSUri: keccak256("inner"),
173
- totalSupply: 1,
174
- price: 20,
175
- category: 1,
176
- splitPercent: 0,
177
- splits: new JBSplit[](0)
178
- });
179
-
180
- publisher.mintFrom{value: 21}(hook, posts, address(this), attackerFeeBeneficiary, bytes(""), bytes(""));
181
- }
182
-
183
- return 0;
184
- }
185
-
186
- receive() external payable {}
187
- }
188
-
189
- contract FeeBeneficiaryReentrancyTest is Test {
190
- MockPermissions permissions;
191
- MockDirectory directory;
192
- MockStore store;
193
- MockHook hook;
194
- FeeTerminalRecorder feeTerminal;
195
- ReentrantProjectTerminal projectTerminal;
196
- CTPublisher publisher;
197
-
198
- address victimFeeBeneficiary = makeAddr("victimFeeBeneficiary");
199
- address attackerFeeBeneficiary = makeAddr("attackerFeeBeneficiary");
200
-
201
- function setUp() public {
202
- permissions = new MockPermissions();
203
- directory = new MockDirectory();
204
- store = new MockStore();
205
- hook = new MockHook(2, IJB721TiersHookStore(address(store)), address(this));
206
- publisher = new CTPublisher(IJBDirectory(address(directory)), permissions, 1, address(0));
207
- feeTerminal = new FeeTerminalRecorder();
208
- projectTerminal =
209
- new ReentrantProjectTerminal(publisher, IJB721TiersHook(address(hook)), attackerFeeBeneficiary);
210
- directory.setTerminals(address(projectTerminal), address(feeTerminal));
211
-
212
- CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
213
- allowedPosts[0] = CTAllowedPost({
214
- hook: address(hook),
215
- category: 1,
216
- minimumPrice: 1,
217
- minimumTotalSupply: 1,
218
- maximumTotalSupply: type(uint32).max,
219
- maximumSplitPercent: 0,
220
- allowedAddresses: new address[](0)
221
- });
222
- publisher.configurePostingCriteriaFor(allowedPosts);
223
- }
224
-
225
- function test_reentrantInnerCallCannotStealOuterFee() public {
226
- CTPost[] memory posts = new CTPost[](1);
227
- posts[0] = CTPost({
228
- encodedIPFSUri: keccak256("outer"),
229
- totalSupply: 1,
230
- price: 100,
231
- category: 1,
232
- splitPercent: 0,
233
- splits: new JBSplit[](0)
234
- });
235
-
236
- publisher.mintFrom{value: 105}(
237
- IJB721TiersHook(address(hook)), posts, address(this), victimFeeBeneficiary, bytes(""), bytes("")
238
- );
239
-
240
- // With the fix, fee amounts are pinned before external calls, so both inner and outer fees
241
- // are paid separately with correct beneficiaries.
242
- assertEq(feeTerminal.callCount(), 2, "both inner and outer fee payments should execute");
243
- assertEq(feeTerminal.totalReceived(), 6, "total fees should be inner(1) + outer(5) = 6");
244
- assertEq(feeTerminal.lastBeneficiary(), victimFeeBeneficiary, "outer fee should go to victim beneficiary");
245
- assertEq(address(publisher).balance, 0, "publisher balance should be empty after both fee payments");
246
- }
247
- }