@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
@@ -1,263 +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 {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
9
- import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.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 {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
22
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
23
-
24
- import {CTDeployer} from "../../src/CTDeployer.sol";
25
- import {CTPublisher} from "../../src/CTPublisher.sol";
26
- import {ICTPublisher} from "../../src/interfaces/ICTPublisher.sol";
27
- import {CTDeployerAllowedPost} from "../../src/structs/CTDeployerAllowedPost.sol";
28
- import {CTProjectConfig} from "../../src/structs/CTProjectConfig.sol";
29
- import {CTSuckerDeploymentConfig} from "../../src/structs/CTSuckerDeploymentConfig.sol";
30
-
31
- contract NemesisMockProjects {
32
- uint256 public countValue;
33
- mapping(uint256 => address) internal _ownerOf;
34
-
35
- function setCount(uint256 count_) external {
36
- countValue = count_;
37
- }
38
-
39
- function setOwner(uint256 projectId, address owner_) external {
40
- _ownerOf[projectId] = owner_;
41
- }
42
-
43
- function count() external view returns (uint256) {
44
- return countValue;
45
- }
46
-
47
- function ownerOf(uint256 projectId) external view returns (address) {
48
- return _ownerOf[projectId];
49
- }
50
-
51
- function transferFrom(address from, address to, uint256 tokenId) external {
52
- require(_ownerOf[tokenId] == from, "wrong from");
53
- _ownerOf[tokenId] = to;
54
- }
55
- }
56
-
57
- contract NemesisMockController {
58
- NemesisMockProjects public immutable PROJECTS;
59
- uint256 public immutable nextProjectId;
60
-
61
- constructor(NemesisMockProjects projects_, uint256 nextProjectId_) {
62
- PROJECTS = projects_;
63
- nextProjectId = nextProjectId_;
64
- }
65
-
66
- function launchProjectFor(
67
- address owner,
68
- string calldata,
69
- JBRulesetConfig[] calldata,
70
- JBTerminalConfig[] calldata,
71
- string calldata
72
- )
73
- external
74
- returns (uint256)
75
- {
76
- PROJECTS.setOwner(nextProjectId, owner);
77
- return nextProjectId;
78
- }
79
- }
80
-
81
- contract NemesisPermissionedHook is JBPermissioned {
82
- address public immutable ownerAccount;
83
- uint256 public immutable projectId;
84
- bool public adjusted;
85
-
86
- constructor(IJBPermissions permissions, address ownerAccount_, uint256 projectId_) JBPermissioned(permissions) {
87
- ownerAccount = ownerAccount_;
88
- projectId = projectId_;
89
- }
90
-
91
- // forge-lint: disable-next-line(mixed-case-function)
92
- function PROJECT_ID() external view returns (uint256) {
93
- return projectId;
94
- }
95
-
96
- function owner() external view returns (address) {
97
- return ownerAccount;
98
- }
99
-
100
- function adjustTiers(JB721TierConfig[] calldata, uint256[] calldata) external {
101
- _requirePermissionFrom(ownerAccount, projectId, JBPermissionIds.ADJUST_721_TIERS);
102
- adjusted = true;
103
- }
104
- }
105
-
106
- contract NemesisMockHookDeployer {
107
- IJB721TiersHook public hook;
108
-
109
- function setHook(IJB721TiersHook hook_) external {
110
- hook = hook_;
111
- }
112
-
113
- function deployHookFor(
114
- uint256,
115
- JBDeploy721TiersHookConfig calldata,
116
- bytes32
117
- )
118
- external
119
- view
120
- returns (IJB721TiersHook)
121
- {
122
- return hook;
123
- }
124
- }
125
-
126
- contract NemesisNoopSuckerRegistry {
127
- function isSuckerOf(uint256, address) external pure returns (bool) {
128
- return false;
129
- }
130
-
131
- function deploySuckersFor(
132
- uint256,
133
- bytes32,
134
- JBSuckerDeployerConfig[] calldata
135
- )
136
- external
137
- pure
138
- returns (address[] memory suckers)
139
- {
140
- return suckers;
141
- }
142
- }
143
-
144
- contract NemesisMockDirectory {
145
- IJBProjects public immutable PROJECTS;
146
-
147
- constructor(IJBProjects projects_) {
148
- PROJECTS = projects_;
149
- }
150
- }
151
-
152
- contract CodexNemesisPoCs is Test {
153
- JBPermissions permissions;
154
- NemesisMockProjects projects;
155
- NemesisMockHookDeployer hookDeployer;
156
- NemesisMockController controller;
157
- CTPublisher publisher;
158
- CTDeployer deployer;
159
- NemesisPermissionedHook hook;
160
-
161
- address alice = makeAddr("alice");
162
- address bob = makeAddr("bob");
163
-
164
- function setUp() public {
165
- permissions = new JBPermissions(address(0));
166
- projects = new NemesisMockProjects();
167
- projects.setCount(5);
168
-
169
- hookDeployer = new NemesisMockHookDeployer();
170
- publisher = new CTPublisher(IJBDirectory(makeAddr("directory")), permissions, 1, address(0));
171
- deployer = new CTDeployer(
172
- permissions,
173
- IJBProjects(address(projects)),
174
- IJB721TiersHookDeployer(address(hookDeployer)),
175
- ICTPublisher(address(publisher)),
176
- IJBSuckerRegistry(address(new NemesisNoopSuckerRegistry())),
177
- address(0)
178
- );
179
-
180
- hook = new NemesisPermissionedHook(permissions, address(deployer), 6);
181
- hookDeployer.setHook(IJB721TiersHook(address(hook)));
182
- controller = new NemesisMockController(projects, 6);
183
- }
184
-
185
- function test_oldProjectOwnerRetainsHookControlAfterProjectNftTransferUntilClaim() public {
186
- CTProjectConfig memory config = CTProjectConfig({
187
- terminalConfigurations: new JBTerminalConfig[](0),
188
- projectUri: "ipfs://project",
189
- allowedPosts: new CTDeployerAllowedPost[](0),
190
- contractUri: "ipfs://contract",
191
- name: "Croptop",
192
- symbol: "CT",
193
- salt: bytes32(0)
194
- });
195
-
196
- CTSuckerDeploymentConfig memory suckerConfig =
197
- CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
198
-
199
- deployer.deployProjectFor(alice, config, suckerConfig, IJBController(address(controller)));
200
- assertEq(projects.ownerOf(6), alice, "alice should receive the project NFT");
201
-
202
- projects.transferFrom(alice, bob, 6);
203
- assertEq(projects.ownerOf(6), bob, "bob should own the project NFT after transfer");
204
-
205
- JB721TierConfig[] memory arbitraryTiers = new JB721TierConfig[](0);
206
- uint256[] memory removals = new uint256[](0);
207
-
208
- vm.prank(alice);
209
- NemesisPermissionedHook(address(hook)).adjustTiers(arbitraryTiers, removals);
210
-
211
- assertTrue(
212
- hook.adjusted(),
213
- "the previous owner should still be able to mutate hook state until the new NFT owner explicitly claims"
214
- );
215
- }
216
-
217
- function test_deploySuckersHelperBreaksAfterOwnershipTransferBecauseRegistrySeesCtDeployerAsCaller() public {
218
- NemesisMockDirectory directory = new NemesisMockDirectory(IJBProjects(address(projects)));
219
- JBSuckerRegistry registry =
220
- new JBSuckerRegistry(IJBDirectory(address(directory)), permissions, address(this), address(0));
221
-
222
- deployer = new CTDeployer(
223
- permissions,
224
- IJBProjects(address(projects)),
225
- IJB721TiersHookDeployer(address(hookDeployer)),
226
- ICTPublisher(address(publisher)),
227
- IJBSuckerRegistry(address(registry)),
228
- address(0)
229
- );
230
-
231
- hook = new NemesisPermissionedHook(permissions, address(deployer), 6);
232
- hookDeployer.setHook(IJB721TiersHook(address(hook)));
233
-
234
- CTProjectConfig memory config = CTProjectConfig({
235
- terminalConfigurations: new JBTerminalConfig[](0),
236
- projectUri: "ipfs://project",
237
- allowedPosts: new CTDeployerAllowedPost[](0),
238
- contractUri: "ipfs://contract",
239
- name: "Croptop",
240
- symbol: "CT",
241
- salt: bytes32(0)
242
- });
243
-
244
- CTSuckerDeploymentConfig memory emptySuckerConfig =
245
- CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
246
- deployer.deployProjectFor(alice, config, emptySuckerConfig, IJBController(address(controller)));
247
-
248
- CTSuckerDeploymentConfig memory laterSuckerConfig =
249
- CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32("later")});
250
-
251
- vm.prank(alice);
252
- vm.expectRevert(
253
- abi.encodeWithSelector(
254
- JBPermissioned.JBPermissioned_Unauthorized.selector,
255
- alice,
256
- address(deployer),
257
- 6,
258
- JBPermissionIds.DEPLOY_SUCKERS
259
- )
260
- );
261
- deployer.deploySuckersFor(6, laterSuckerConfig);
262
- }
263
- }
@@ -1,168 +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 {IJBOwnable} from "@bananapus/ownable-v6/src/interfaces/IJBOwnable.sol";
10
- import {IJB721Hook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Hook.sol";
11
- import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
12
- import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
13
- import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
14
- import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
15
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
16
-
17
- import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
18
- import {CTPost} from "../../src/structs/CTPost.sol";
19
- import {CTPublisher} from "../../src/CTPublisher.sol";
20
-
21
- contract CodexNemesisPolicyReuseTest is Test {
22
- CTPublisher internal publisher;
23
-
24
- IJBPermissions internal permissions = IJBPermissions(makeAddr("permissions"));
25
- IJBDirectory internal directory = IJBDirectory(makeAddr("directory"));
26
-
27
- address internal hookOwner = makeAddr("hookOwner");
28
- address internal hookAddr = makeAddr("hook");
29
- address internal hookStoreAddr = makeAddr("hookStore");
30
- address internal projectTerminal = makeAddr("projectTerminal");
31
- address internal feeTerminal = makeAddr("feeTerminal");
32
- address internal alice = makeAddr("alice");
33
- address internal bob = makeAddr("bob");
34
-
35
- uint256 internal constant FEE_PROJECT_ID = 1;
36
- uint256 internal constant PROJECT_ID = 42;
37
- bytes32 internal constant URI = keccak256("stale-policy-uri");
38
- uint104 internal constant PRICE = 1 ether;
39
-
40
- function setUp() public {
41
- publisher = new CTPublisher(directory, permissions, FEE_PROJECT_ID, address(0));
42
-
43
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(hookOwner));
44
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(PROJECT_ID));
45
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.STORE.selector), abi.encode(hookStoreAddr));
46
-
47
- vm.mockCall(
48
- address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
49
- );
50
-
51
- vm.mockCall(
52
- address(directory),
53
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, PROJECT_ID),
54
- abi.encode(projectTerminal)
55
- );
56
- vm.mockCall(
57
- address(directory),
58
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, FEE_PROJECT_ID),
59
- abi.encode(feeTerminal)
60
- );
61
- vm.mockCall(projectTerminal, "", abi.encode(uint256(0)));
62
- vm.mockCall(feeTerminal, "", abi.encode(uint256(0)));
63
- vm.mockCall(
64
- hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.isTierRemoved.selector), abi.encode(false)
65
- );
66
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.adjustTiers.selector), abi.encode());
67
- vm.mockCall(hookAddr, abi.encodeWithSelector(bytes4(keccak256("METADATA_ID_TARGET()"))), abi.encode(address(0)));
68
-
69
- vm.deal(alice, 10 ether);
70
- vm.deal(bob, 10 ether);
71
- }
72
-
73
- function test_existingTierReuseIgnoresUpdatedAllowlist() public {
74
- uint256 mintValue = PRICE + (PRICE / publisher.FEE_DIVISOR());
75
-
76
- _configureAllowlist(alice);
77
-
78
- vm.mockCall(
79
- hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.maxTierIdOf.selector), abi.encode(uint256(0))
80
- );
81
-
82
- CTPost[] memory initialPosts = _singlePost();
83
-
84
- vm.prank(alice);
85
- publisher.mintFrom{value: mintValue}(IJB721TiersHook(hookAddr), initialPosts, alice, alice, "", "");
86
-
87
- assertEq(publisher.tierIdForEncodedIPFSUriOf(hookAddr, URI), 1, "initial publish should store tier id");
88
-
89
- _configureAllowlist(bob);
90
-
91
- CTPost[] memory blockedNewUri = new CTPost[](1);
92
- blockedNewUri[0] = CTPost({
93
- encodedIPFSUri: keccak256("new-uri"),
94
- totalSupply: 10,
95
- price: PRICE,
96
- category: 7,
97
- splitPercent: 0,
98
- splits: new JBSplit[](0)
99
- });
100
-
101
- vm.mockCall(
102
- hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.maxTierIdOf.selector), abi.encode(uint256(1))
103
- );
104
-
105
- vm.prank(alice);
106
- vm.expectRevert(abi.encodeWithSelector(CTPublisher.CTPublisher_NotInAllowList.selector, alice, _asArray(bob)));
107
- publisher.mintFrom{value: mintValue}(IJB721TiersHook(hookAddr), blockedNewUri, alice, alice, "", "");
108
-
109
- JB721Tier memory existingTier = JB721Tier({
110
- id: 1,
111
- price: PRICE,
112
- remainingSupply: 9,
113
- initialSupply: 10,
114
- votingUnits: 0,
115
- reserveFrequency: 0,
116
- reserveBeneficiary: address(0),
117
- encodedIPFSUri: URI,
118
- category: 7,
119
- discountPercent: 0,
120
- flags: JB721TierFlags({
121
- allowOwnerMint: false,
122
- transfersPausable: false,
123
- cantBeRemoved: false,
124
- cantIncreaseDiscountPercent: false,
125
- cantBuyWithCredits: false
126
- }),
127
- splitPercent: 0,
128
- resolvedUri: ""
129
- });
130
-
131
- vm.mockCall(
132
- hookStoreAddr,
133
- abi.encodeWithSelector(IJB721TiersHookStore.tierOf.selector, hookAddr, 1, false),
134
- abi.encode(existingTier)
135
- );
136
-
137
- vm.prank(alice);
138
- publisher.mintFrom{value: mintValue}(IJB721TiersHook(hookAddr), initialPosts, alice, alice, "", "");
139
- }
140
-
141
- function _configureAllowlist(address allowedPoster) internal {
142
- CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
143
- allowedPosts[0] = CTAllowedPost({
144
- hook: hookAddr,
145
- category: 7,
146
- minimumPrice: PRICE,
147
- minimumTotalSupply: 1,
148
- maximumTotalSupply: 10,
149
- maximumSplitPercent: 0,
150
- allowedAddresses: _asArray(allowedPoster)
151
- });
152
-
153
- vm.prank(hookOwner);
154
- publisher.configurePostingCriteriaFor(allowedPosts);
155
- }
156
-
157
- function _singlePost() internal pure returns (CTPost[] memory posts) {
158
- posts = new CTPost[](1);
159
- posts[0] = CTPost({
160
- encodedIPFSUri: URI, totalSupply: 10, price: PRICE, category: 7, splitPercent: 0, splits: new JBSplit[](0)
161
- });
162
- }
163
-
164
- function _asArray(address addr) internal pure returns (address[] memory values) {
165
- values = new address[](1);
166
- values[0] = addr;
167
- }
168
- }
@@ -1,252 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {Test} from "forge-std/Test.sol";
5
-
6
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
9
- import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
10
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
11
- import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
12
- import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
13
- import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
14
- import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
15
- import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
16
-
17
- import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
18
- import {CTPost} from "../../src/structs/CTPost.sol";
19
- import {CTPublisher} from "../../src/CTPublisher.sol";
20
-
21
- contract CodexNemesisUriDriftTest is Test {
22
- bytes32 internal constant URI_A = keccak256("uri-a");
23
- bytes32 internal constant URI_B = keccak256("uri-b");
24
-
25
- JBPermissions internal permissions;
26
- MockTerminal internal terminal;
27
- MockDirectory internal directory;
28
- MockStore internal store;
29
- MockHook internal hook;
30
- CTPublisher internal publisher;
31
-
32
- address internal hookOwner = makeAddr("hookOwner");
33
- address internal poster = makeAddr("poster");
34
- uint256 internal constant FEE_PROJECT_ID = 1;
35
- uint256 internal constant PROJECT_ID = 42;
36
-
37
- function setUp() public {
38
- permissions = new JBPermissions(address(0));
39
- terminal = new MockTerminal();
40
- directory = new MockDirectory(IJBTerminal(address(terminal)));
41
- store = new MockStore();
42
- hook = new MockHook(hookOwner, PROJECT_ID, store);
43
- publisher = new CTPublisher(
44
- IJBDirectory(address(directory)), IJBPermissions(address(permissions)), FEE_PROJECT_ID, address(0)
45
- );
46
-
47
- vm.deal(poster, 10 ether);
48
-
49
- CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
50
- allowedPosts[0] = CTAllowedPost({
51
- hook: address(hook),
52
- category: 7,
53
- minimumPrice: 1 ether,
54
- minimumTotalSupply: 1,
55
- maximumTotalSupply: 100,
56
- maximumSplitPercent: 0,
57
- allowedAddresses: new address[](0)
58
- });
59
-
60
- vm.prank(hookOwner);
61
- publisher.configurePostingCriteriaFor(allowedPosts);
62
- }
63
-
64
- function test_uriMutationDesyncAllowsDuplicateContentPublication() public {
65
- _publish(URI_A);
66
-
67
- assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "publisher should index URI_A -> tier 1");
68
- assertEq(store.tierUri(1), URI_A, "store should record tier 1 as URI_A");
69
-
70
- vm.prank(hookOwner);
71
- hook.setMetadata("", "", "", "", address(this), 1, URI_B);
72
-
73
- assertEq(store.tierUri(1), URI_B, "owner metadata update should move tier 1 to URI_B");
74
- assertEq(
75
- publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A),
76
- 1,
77
- "publisher mapping remains stale at URI_A -> tier 1"
78
- );
79
- assertEq(
80
- publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_B), 0, "publisher has no entry for the new URI yet"
81
- );
82
-
83
- _publish(URI_B);
84
-
85
- assertEq(store.maxTierId(), 2, "publishing URI_B again should create a fresh tier");
86
- assertEq(store.tierUri(1), URI_B, "tier 1 still points at URI_B after mutation");
87
- assertEq(store.tierUri(2), URI_B, "tier 2 now also points at URI_B");
88
- assertEq(
89
- publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_B),
90
- 2,
91
- "publisher now tracks URI_B as a second tier instead of rejecting the duplicate"
92
- );
93
- }
94
-
95
- function _publish(bytes32 uri) internal {
96
- CTPost[] memory posts = new CTPost[](1);
97
- posts[0] = CTPost({
98
- encodedIPFSUri: uri, totalSupply: 10, price: 1 ether, category: 7, splitPercent: 0, splits: new JBSplit[](0)
99
- });
100
-
101
- vm.prank(poster);
102
- publisher.mintFrom{value: 1.05 ether}(IJB721TiersHook(address(hook)), posts, poster, poster, "", "");
103
- }
104
- }
105
-
106
- contract MockDirectory {
107
- // forge-lint: disable-next-line(screaming-snake-case-immutable)
108
- IJBTerminal internal immutable _terminal;
109
-
110
- constructor(IJBTerminal terminal) {
111
- _terminal = terminal;
112
- }
113
-
114
- function primaryTerminalOf(uint256, address) external view returns (IJBTerminal) {
115
- return _terminal;
116
- }
117
- }
118
-
119
- contract MockTerminal {
120
- function pay(
121
- uint256,
122
- address,
123
- uint256,
124
- address,
125
- uint256,
126
- string calldata,
127
- bytes calldata
128
- )
129
- external
130
- payable
131
- returns (uint256)
132
- {
133
- return 0;
134
- }
135
- }
136
-
137
- contract MockStore {
138
- struct TierData {
139
- bytes32 uri;
140
- uint104 price;
141
- uint24 category;
142
- uint32 supply;
143
- bool removed;
144
- }
145
-
146
- uint256 internal _maxTierId;
147
- mapping(uint256 tierId => TierData) internal _tiers;
148
-
149
- function maxTierIdOf(address) external view returns (uint256) {
150
- return _maxTierId;
151
- }
152
-
153
- function isTierRemoved(address, uint256 tierId) external view returns (bool) {
154
- return _tiers[tierId].removed;
155
- }
156
-
157
- function tierOf(address, uint256 tierId, bool) external view returns (JB721Tier memory) {
158
- TierData memory tier = _tiers[tierId];
159
- return JB721Tier({
160
- // forge-lint: disable-next-line(unsafe-typecast)
161
- id: uint32(tierId),
162
- price: tier.price,
163
- remainingSupply: tier.supply,
164
- initialSupply: tier.supply,
165
- votingUnits: 0,
166
- reserveFrequency: 0,
167
- reserveBeneficiary: address(0),
168
- encodedIPFSUri: tier.uri,
169
- category: tier.category,
170
- discountPercent: 0,
171
- flags: JB721TierFlags({
172
- allowOwnerMint: false,
173
- transfersPausable: false,
174
- cantBeRemoved: false,
175
- cantIncreaseDiscountPercent: false,
176
- cantBuyWithCredits: false
177
- }),
178
- splitPercent: 0,
179
- resolvedUri: ""
180
- });
181
- }
182
-
183
- function addTier(JB721TierConfig calldata config) external returns (uint256 tierId) {
184
- tierId = ++_maxTierId;
185
- _tiers[tierId] = TierData({
186
- uri: config.encodedIPFSUri,
187
- price: config.price,
188
- category: config.category,
189
- supply: config.initialSupply,
190
- removed: false
191
- });
192
- }
193
-
194
- // forge-lint: disable-next-line(mixed-case-function)
195
- function setEncodedIPFSUriOf(uint256 tierId, bytes32 uri) external {
196
- _tiers[tierId].uri = uri;
197
- }
198
-
199
- function tierUri(uint256 tierId) external view returns (bytes32) {
200
- return _tiers[tierId].uri;
201
- }
202
-
203
- function maxTierId() external view returns (uint256) {
204
- return _maxTierId;
205
- }
206
- }
207
-
208
- contract MockHook {
209
- address internal _owner;
210
- uint256 public immutable PROJECT_ID;
211
- // forge-lint: disable-next-line(screaming-snake-case-immutable)
212
- MockStore internal immutable _store;
213
-
214
- constructor(address owner_, uint256 projectId, MockStore store_) {
215
- _owner = owner_;
216
- PROJECT_ID = projectId;
217
- _store = store_;
218
- }
219
-
220
- function owner() external view returns (address) {
221
- return _owner;
222
- }
223
-
224
- function STORE() external view returns (IJB721TiersHookStore) {
225
- return IJB721TiersHookStore(address(_store));
226
- }
227
-
228
- function METADATA_ID_TARGET() external view returns (address) {
229
- return address(this);
230
- }
231
-
232
- function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata) external {
233
- for (uint256 i; i < tiersToAdd.length; i++) {
234
- _store.addTier(tiersToAdd[i]);
235
- }
236
- }
237
-
238
- function setMetadata(
239
- string calldata,
240
- string calldata,
241
- string calldata,
242
- string calldata,
243
- address,
244
- uint256 encodedIPFSUriTierId,
245
- bytes32 encodedIPFSUri
246
- )
247
- external
248
- {
249
- require(msg.sender == _owner, "not owner");
250
- _store.setEncodedIPFSUriOf(encodedIPFSUriTierId, encodedIPFSUri);
251
- }
252
- }