@croptop/core-v6 0.0.37 → 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/CTPublisher.sol +20 -5
  7. package/src/interfaces/ICTDeployer.sol +2 -2
  8. package/ADMINISTRATION.md +0 -94
  9. package/ARCHITECTURE.md +0 -96
  10. package/AUDIT_INSTRUCTIONS.md +0 -88
  11. package/RISKS.md +0 -78
  12. package/SKILLS.md +0 -46
  13. package/STYLE_GUIDE.md +0 -610
  14. package/USER_JOURNEYS.md +0 -134
  15. package/foundry.lock +0 -11
  16. package/slither-ci.config.json +0 -10
  17. package/sphinx.lock +0 -507
  18. package/test/CTDeployer.t.sol +0 -616
  19. package/test/CTProjectOwner.t.sol +0 -185
  20. package/test/CTPublisher.t.sol +0 -869
  21. package/test/ClaimCollectionOwnership.t.sol +0 -315
  22. package/test/CroptopAttacks.t.sol +0 -437
  23. package/test/Fork.t.sol +0 -227
  24. package/test/TestAuditGaps.sol +0 -696
  25. package/test/Test_MetadataGeneration.t.sol +0 -79
  26. package/test/audit/CodexNemesisCroptopPublisherBoundary.t.sol +0 -329
  27. package/test/audit/CodexNemesisCurrencyPoCs.t.sol +0 -371
  28. package/test/audit/CodexNemesisFreshRound.t.sol +0 -395
  29. package/test/audit/CodexNemesisMetadataShadow.t.sol +0 -203
  30. package/test/audit/CodexNemesisPoCs.t.sol +0 -263
  31. package/test/audit/CodexNemesisPolicyReuse.t.sol +0 -168
  32. package/test/audit/CodexNemesisUriDrift.t.sol +0 -252
  33. package/test/audit/DeployerPermissionBypass.t.sol +0 -213
  34. package/test/audit/EmptyPostFeeBypass.t.sol +0 -53
  35. package/test/audit/FeeBeneficiaryReentrancy.t.sol +0 -247
  36. package/test/audit/FeeFallbackBlackhole.t.sol +0 -263
  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 -218
@@ -1,312 +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 {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
14
-
15
- import {CTPublisher} from "../../src/CTPublisher.sol";
16
- import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
17
- import {CTPost} from "../../src/structs/CTPost.sol";
18
-
19
- /// @title M6_DuplicateUriFeeEvasion
20
- /// @notice Duplicate encodedIPFSUri in a single mintFrom batch
21
- /// enables fee evasion. Before the fix, a second post with the same URI would read
22
- /// a stale tierIdForEncodedIPFSUriOf mapping (written by _setupPosts for the first
23
- /// post but not yet committed to the store), causing store.tierOf() to return price=0,
24
- /// so the fee was computed on 1x the price instead of 2x.
25
- /// The fix reverts with CTPublisher_DuplicatePost when duplicate URIs appear in a batch.
26
- contract M6_DuplicateUriFeeEvasion is Test {
27
- CTPublisher publisher;
28
-
29
- IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
30
- IJBDirectory directory = IJBDirectory(makeAddr("directory"));
31
-
32
- address hookOwner = makeAddr("hookOwner");
33
- address hookAddr = makeAddr("hook");
34
- address hookStoreAddr = makeAddr("hookStore");
35
- address terminalAddr = makeAddr("terminal");
36
- address feeTerminalAddr = makeAddr("feeTerminal");
37
- address poster = makeAddr("poster");
38
-
39
- uint256 feeProjectId = 1;
40
- uint256 hookProjectId = 42;
41
-
42
- function setUp() public {
43
- publisher = new CTPublisher(directory, permissions, feeProjectId, address(0));
44
-
45
- // Mock hook.owner().
46
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(hookOwner));
47
- // Mock hook.PROJECT_ID().
48
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(hookProjectId));
49
- // Mock hook.STORE().
50
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.STORE.selector), abi.encode(hookStoreAddr));
51
-
52
- // Mock permissions to return true by default.
53
- vm.mockCall(
54
- address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
55
- );
56
-
57
- // Fund poster.
58
- vm.deal(poster, 100 ether);
59
- }
60
-
61
- function _configureCategory() internal {
62
- CTAllowedPost[] memory posts = new CTAllowedPost[](1);
63
- posts[0] = CTAllowedPost({
64
- hook: hookAddr,
65
- category: 5,
66
- minimumPrice: 0.01 ether,
67
- minimumTotalSupply: 1,
68
- maximumTotalSupply: 1000,
69
- maximumSplitPercent: 0,
70
- allowedAddresses: new address[](0)
71
- });
72
-
73
- vm.prank(hookOwner);
74
- publisher.configurePostingCriteriaFor(posts);
75
- }
76
-
77
- function _setupMintMocks() internal {
78
- vm.mockCall(
79
- hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.maxTierIdOf.selector), abi.encode(uint256(0))
80
- );
81
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.adjustTiers.selector), abi.encode());
82
- vm.mockCall(hookAddr, abi.encodeWithSelector(bytes4(keccak256("METADATA_ID_TARGET()"))), abi.encode(address(0)));
83
- vm.mockCall(
84
- address(directory),
85
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, hookProjectId),
86
- abi.encode(terminalAddr)
87
- );
88
- vm.mockCall(
89
- address(directory),
90
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, feeProjectId),
91
- abi.encode(feeTerminalAddr)
92
- );
93
- vm.mockCall(terminalAddr, "", abi.encode(uint256(0)));
94
- vm.mockCall(feeTerminalAddr, "", abi.encode(uint256(0)));
95
- }
96
-
97
- // =========================================================================
98
- // Test 1: Duplicate URI in batch reverts with CTPublisher_DuplicatePost
99
- // =========================================================================
100
- /// @notice Sending two posts with the same encodedIPFSUri in a single mintFrom batch
101
- /// must revert with CTPublisher_DuplicatePost.
102
- function test_duplicateUriInBatch_reverts() public {
103
- _configureCategory();
104
- _setupMintMocks();
105
-
106
- bytes32 duplicateUri = keccak256("same-content");
107
-
108
- CTPost[] memory posts = new CTPost[](2);
109
- posts[0] = CTPost({
110
- encodedIPFSUri: duplicateUri,
111
- totalSupply: 10,
112
- price: 0.1 ether,
113
- category: 5,
114
- splitPercent: 0,
115
- splits: new JBSplit[](0)
116
- });
117
- posts[1] = CTPost({
118
- encodedIPFSUri: duplicateUri, // Same URI as posts[0].
119
- totalSupply: 10,
120
- price: 0.1 ether,
121
- category: 5,
122
- splitPercent: 0,
123
- splits: new JBSplit[](0)
124
- });
125
-
126
- vm.prank(poster);
127
- vm.expectRevert(abi.encodeWithSelector(CTPublisher.CTPublisher_DuplicatePost.selector, duplicateUri));
128
- publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
129
- }
130
-
131
- // =========================================================================
132
- // Test 2: Three posts, first and third duplicate — reverts
133
- // =========================================================================
134
- /// @notice Duplicates do not need to be adjacent to be caught.
135
- function test_duplicateUriNonAdjacent_reverts() public {
136
- _configureCategory();
137
- _setupMintMocks();
138
-
139
- bytes32 duplicateUri = keccak256("content-A");
140
- bytes32 uniqueUri = keccak256("content-B");
141
-
142
- CTPost[] memory posts = new CTPost[](3);
143
- posts[0] = CTPost({
144
- encodedIPFSUri: duplicateUri,
145
- totalSupply: 10,
146
- price: 0.1 ether,
147
- category: 5,
148
- splitPercent: 0,
149
- splits: new JBSplit[](0)
150
- });
151
- posts[1] = CTPost({
152
- encodedIPFSUri: uniqueUri, // Different URI.
153
- totalSupply: 10,
154
- price: 0.1 ether,
155
- category: 5,
156
- splitPercent: 0,
157
- splits: new JBSplit[](0)
158
- });
159
- posts[2] = CTPost({
160
- encodedIPFSUri: duplicateUri, // Same as posts[0].
161
- totalSupply: 10,
162
- price: 0.1 ether,
163
- category: 5,
164
- splitPercent: 0,
165
- splits: new JBSplit[](0)
166
- });
167
-
168
- vm.prank(poster);
169
- vm.expectRevert(abi.encodeWithSelector(CTPublisher.CTPublisher_DuplicatePost.selector, duplicateUri));
170
- publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
171
- }
172
-
173
- // =========================================================================
174
- // Test 3: Two posts with different URIs succeed
175
- // =========================================================================
176
- /// @notice Two posts with distinct encodedIPFSUri values should not revert
177
- /// (at least not with the duplicate error).
178
- function test_distinctUrisInBatch_succeeds() public {
179
- _configureCategory();
180
- _setupMintMocks();
181
-
182
- CTPost[] memory posts = new CTPost[](2);
183
- posts[0] = CTPost({
184
- encodedIPFSUri: keccak256("content-1"),
185
- totalSupply: 10,
186
- price: 0.1 ether,
187
- category: 5,
188
- splitPercent: 0,
189
- splits: new JBSplit[](0)
190
- });
191
- posts[1] = CTPost({
192
- encodedIPFSUri: keccak256("content-2"),
193
- totalSupply: 10,
194
- price: 0.1 ether,
195
- category: 5,
196
- splitPercent: 0,
197
- splits: new JBSplit[](0)
198
- });
199
-
200
- // Should not revert with CTPublisher_DuplicatePost.
201
- // May succeed fully or revert downstream in mocks, but never with the duplicate error.
202
- vm.prank(poster);
203
- try publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "") {}
204
- catch (bytes memory reason) {
205
- // Ensure it did NOT revert with CTPublisher_DuplicatePost.
206
- assertTrue(
207
- keccak256(reason)
208
- != keccak256(
209
- abi.encodeWithSelector(CTPublisher.CTPublisher_DuplicatePost.selector, keccak256("content-1"))
210
- ),
211
- "should not revert with duplicate post error for content-1"
212
- );
213
- assertTrue(
214
- keccak256(reason)
215
- != keccak256(
216
- abi.encodeWithSelector(CTPublisher.CTPublisher_DuplicatePost.selector, keccak256("content-2"))
217
- ),
218
- "should not revert with duplicate post error for content-2"
219
- );
220
- }
221
- }
222
-
223
- // =========================================================================
224
- // Test 4: Single post (no duplicates possible) succeeds
225
- // =========================================================================
226
- /// @notice A single post should never trigger the duplicate check.
227
- function test_singlePost_noDuplicateError() public {
228
- _configureCategory();
229
- _setupMintMocks();
230
-
231
- CTPost[] memory posts = new CTPost[](1);
232
- posts[0] = CTPost({
233
- encodedIPFSUri: keccak256("sole-content"),
234
- totalSupply: 10,
235
- price: 0.1 ether,
236
- category: 5,
237
- splitPercent: 0,
238
- splits: new JBSplit[](0)
239
- });
240
-
241
- vm.prank(poster);
242
- try publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "") {}
243
- catch (bytes memory reason) {
244
- assertTrue(
245
- keccak256(reason)
246
- != keccak256(
247
- abi.encodeWithSelector(
248
- CTPublisher.CTPublisher_DuplicatePost.selector, keccak256("sole-content")
249
- )
250
- ),
251
- "should not revert with duplicate post error"
252
- );
253
- }
254
- }
255
-
256
- // =========================================================================
257
- // Test 5: Fuzz — batch of 2 posts, duplicate iff URIs match
258
- // =========================================================================
259
- /// @notice Fuzz test: when two URIs are equal the call must revert with
260
- /// CTPublisher_DuplicatePost; when they differ it must not.
261
- function testFuzz_duplicateDetection(bytes32 uri1, bytes32 uri2) public {
262
- // forge-lint: disable-next-line(unsafe-typecast)
263
- vm.assume(uri1 != bytes32(""));
264
- // forge-lint: disable-next-line(unsafe-typecast)
265
- vm.assume(uri2 != bytes32(""));
266
-
267
- _configureCategory();
268
- _setupMintMocks();
269
-
270
- CTPost[] memory posts = new CTPost[](2);
271
- posts[0] = CTPost({
272
- encodedIPFSUri: uri1,
273
- totalSupply: 10,
274
- price: 0.1 ether,
275
- category: 5,
276
- splitPercent: 0,
277
- splits: new JBSplit[](0)
278
- });
279
- posts[1] = CTPost({
280
- encodedIPFSUri: uri2,
281
- totalSupply: 10,
282
- price: 0.1 ether,
283
- category: 5,
284
- splitPercent: 0,
285
- splits: new JBSplit[](0)
286
- });
287
-
288
- if (uri1 == uri2) {
289
- // Must revert with duplicate error.
290
- vm.prank(poster);
291
- vm.expectRevert(abi.encodeWithSelector(CTPublisher.CTPublisher_DuplicatePost.selector, uri1));
292
- publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
293
- } else {
294
- // Must NOT revert with duplicate error. May still revert for other reasons
295
- // (e.g. mocked terminal behavior), but not CTPublisher_DuplicatePost.
296
- vm.prank(poster);
297
- try publisher.mintFrom{value: 1 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "") {}
298
- catch (bytes memory reason) {
299
- assertTrue(
300
- keccak256(reason)
301
- != keccak256(abi.encodeWithSelector(CTPublisher.CTPublisher_DuplicatePost.selector, uri1)),
302
- "should not revert with duplicate post error for uri1"
303
- );
304
- assertTrue(
305
- keccak256(reason)
306
- != keccak256(abi.encodeWithSelector(CTPublisher.CTPublisher_DuplicatePost.selector, uri2)),
307
- "should not revert with duplicate post error for uri2"
308
- );
309
- }
310
- }
311
- }
312
- }
@@ -1,286 +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 {CTPublisher} from "../../src/CTPublisher.sol";
18
- import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
19
- import {CTPost} from "../../src/structs/CTPost.sol";
20
-
21
- /// @title H19_FeeEvasion
22
- /// @notice Fee evasion for existing tier mints.
23
- /// Before the fix, a user could set post.price = 0 for an existing tier
24
- /// to evade the 5% Croptop fee entirely. The fix reads the actual tier price
25
- /// from the store for existing tiers.
26
- contract H19_FeeEvasion is Test {
27
- CTPublisher publisher;
28
-
29
- IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
30
- IJBDirectory directory = IJBDirectory(makeAddr("directory"));
31
-
32
- address hookOwner = makeAddr("hookOwner");
33
- address hookAddr = makeAddr("hook");
34
- address hookStoreAddr = makeAddr("hookStore");
35
- address terminalAddr = makeAddr("terminal");
36
- address feeTerminalAddr = makeAddr("feeTerminal");
37
- address poster = makeAddr("poster");
38
-
39
- uint256 feeProjectId = 1;
40
- uint256 hookProjectId = 42;
41
-
42
- bytes32 constant TEST_URI = keccak256("existing-tier-content");
43
- uint104 constant TIER_PRICE = 1 ether;
44
-
45
- function setUp() public {
46
- publisher = new CTPublisher(directory, permissions, feeProjectId, address(0));
47
-
48
- // Mock hook.owner().
49
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(hookOwner));
50
- // Mock hook.PROJECT_ID().
51
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(hookProjectId));
52
- // Mock hook.STORE().
53
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.STORE.selector), abi.encode(hookStoreAddr));
54
-
55
- // Mock isTierRemoved to return false by default (tier exists).
56
- vm.mockCall(
57
- hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.isTierRemoved.selector), abi.encode(false)
58
- );
59
-
60
- // Mock permissions to return true by default.
61
- vm.mockCall(
62
- address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
63
- );
64
-
65
- // Fund poster.
66
- vm.deal(poster, 100 ether);
67
- }
68
-
69
- function _configureCategory() internal {
70
- CTAllowedPost[] memory posts = new CTAllowedPost[](1);
71
- posts[0] = CTAllowedPost({
72
- hook: hookAddr,
73
- category: 5,
74
- minimumPrice: 0,
75
- minimumTotalSupply: 1,
76
- maximumTotalSupply: 100,
77
- maximumSplitPercent: 0,
78
- allowedAddresses: new address[](0)
79
- });
80
-
81
- vm.prank(hookOwner);
82
- publisher.configurePostingCriteriaFor(posts);
83
- }
84
-
85
- function _setupMintMocks(uint256 maxTierId) internal {
86
- vm.mockCall(
87
- hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.maxTierIdOf.selector), abi.encode(maxTierId)
88
- );
89
- vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.adjustTiers.selector), abi.encode());
90
- vm.mockCall(hookAddr, abi.encodeWithSelector(bytes4(keccak256("METADATA_ID_TARGET()"))), abi.encode(address(0)));
91
- }
92
-
93
- /// @notice Test that fee is still charged when post.price = 0 for an existing tier.
94
- /// Before the fix, the attacker could set post.price = 0 and pay exactly 0 ETH
95
- /// for the fee. After the fix, the actual tier price is read from the store.
96
- function test_feeChargedForExistingTierEvenWithZeroPostPrice() public {
97
- _configureCategory();
98
-
99
- // First mint: create tier 1 with TIER_PRICE.
100
- _setupMintMocks(0);
101
-
102
- // Mock tierOf for tier 1 to return a tier with TIER_PRICE.
103
- JB721Tier memory tier = JB721Tier({
104
- id: 1,
105
- price: TIER_PRICE,
106
- remainingSupply: 9,
107
- initialSupply: 10,
108
- votingUnits: 0,
109
- reserveFrequency: 0,
110
- reserveBeneficiary: address(0),
111
- encodedIPFSUri: TEST_URI,
112
- category: 5,
113
- discountPercent: 0,
114
- flags: JB721TierFlags({
115
- allowOwnerMint: false,
116
- transfersPausable: false,
117
- cantBeRemoved: false,
118
- cantIncreaseDiscountPercent: false,
119
- cantBuyWithCredits: false
120
- }),
121
- splitPercent: 0,
122
- resolvedUri: ""
123
- });
124
- vm.mockCall(
125
- hookStoreAddr,
126
- abi.encodeWithSelector(IJB721TiersHookStore.tierOf.selector, hookAddr, 1, false),
127
- abi.encode(tier)
128
- );
129
-
130
- // Mock terminals.
131
- vm.mockCall(
132
- address(directory),
133
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, hookProjectId),
134
- abi.encode(terminalAddr)
135
- );
136
- vm.mockCall(
137
- address(directory),
138
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, feeProjectId),
139
- abi.encode(feeTerminalAddr)
140
- );
141
-
142
- // Mock terminal.pay() to succeed and record the value sent.
143
- vm.mockCall(terminalAddr, "", abi.encode(uint256(0)));
144
- vm.mockCall(feeTerminalAddr, "", abi.encode(uint256(0)));
145
-
146
- CTPost[] memory posts = new CTPost[](1);
147
- posts[0] = CTPost({
148
- encodedIPFSUri: TEST_URI,
149
- totalSupply: 10,
150
- price: TIER_PRICE,
151
- category: 5,
152
- splitPercent: 0,
153
- splits: new JBSplit[](0)
154
- });
155
-
156
- // First mint to create the tier and populate the mapping.
157
- vm.prank(poster);
158
- publisher.mintFrom{value: 2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
159
-
160
- // Verify the mapping was set.
161
- assertEq(publisher.tierIdForEncodedIPFSUriOf(hookAddr, TEST_URI), 1, "tier ID should be stored");
162
-
163
- // Now the attack: existing tier, but attacker sets post.price = 0.
164
- // Update mocks for the second mint (maxTierId is now 1).
165
- _setupMintMocks(1);
166
-
167
- CTPost[] memory attackPosts = new CTPost[](1);
168
- attackPosts[0] = CTPost({
169
- encodedIPFSUri: TEST_URI,
170
- totalSupply: 10,
171
- price: 0, // Attacker tries to evade fee by setting price = 0.
172
- category: 5,
173
- splitPercent: 0,
174
- splits: new JBSplit[](0)
175
- });
176
-
177
- // The fee is TIER_PRICE / FEE_DIVISOR = 1 ether / 20 = 0.05 ether.
178
- // The project payment is TIER_PRICE - fee = 1 ether - 0.05 ether = 0.95 ether.
179
- // Total required: TIER_PRICE = 1 ether (project gets 0.95 ether, fee is 0.05 ether).
180
- // With the fix, the actual tier price (1 ether) is used, so the full msg.value is needed.
181
-
182
- // Sending 0 ETH should revert because totalPrice is now the actual tier price (1 ether),
183
- // not the attacker's 0.
184
- vm.prank(poster);
185
- vm.expectRevert();
186
- publisher.mintFrom{value: 0}(IJB721TiersHook(hookAddr), attackPosts, poster, poster, "", "");
187
-
188
- // Sending the correct amount should succeed.
189
- vm.prank(poster);
190
- publisher.mintFrom{value: 2 ether}(IJB721TiersHook(hookAddr), attackPosts, poster, poster, "", "");
191
- }
192
-
193
- /// @notice Test that the correct fee amount is deducted for existing tier mints.
194
- /// The fee should be based on the actual tier price, not post.price.
195
- function test_correctFeeDeductedForExistingTier() public {
196
- _configureCategory();
197
-
198
- // Create tier 1 with TIER_PRICE.
199
- _setupMintMocks(0);
200
-
201
- // Mock tierOf for tier 1.
202
- JB721Tier memory tier = JB721Tier({
203
- id: 1,
204
- price: TIER_PRICE,
205
- remainingSupply: 9,
206
- initialSupply: 10,
207
- votingUnits: 0,
208
- reserveFrequency: 0,
209
- reserveBeneficiary: address(0),
210
- encodedIPFSUri: TEST_URI,
211
- category: 5,
212
- discountPercent: 0,
213
- flags: JB721TierFlags({
214
- allowOwnerMint: false,
215
- transfersPausable: false,
216
- cantBeRemoved: false,
217
- cantIncreaseDiscountPercent: false,
218
- cantBuyWithCredits: false
219
- }),
220
- splitPercent: 0,
221
- resolvedUri: ""
222
- });
223
- vm.mockCall(
224
- hookStoreAddr,
225
- abi.encodeWithSelector(IJB721TiersHookStore.tierOf.selector, hookAddr, 1, false),
226
- abi.encode(tier)
227
- );
228
-
229
- // Mock terminals.
230
- vm.mockCall(
231
- address(directory),
232
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, hookProjectId),
233
- abi.encode(terminalAddr)
234
- );
235
- vm.mockCall(
236
- address(directory),
237
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, feeProjectId),
238
- abi.encode(feeTerminalAddr)
239
- );
240
- vm.mockCall(terminalAddr, "", abi.encode(uint256(0)));
241
- vm.mockCall(feeTerminalAddr, "", abi.encode(uint256(0)));
242
-
243
- // First mint to create the tier.
244
- CTPost[] memory posts = new CTPost[](1);
245
- posts[0] = CTPost({
246
- encodedIPFSUri: TEST_URI,
247
- totalSupply: 10,
248
- price: TIER_PRICE,
249
- category: 5,
250
- splitPercent: 0,
251
- splits: new JBSplit[](0)
252
- });
253
-
254
- vm.prank(poster);
255
- publisher.mintFrom{value: 2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
256
-
257
- // Second mint with the existing tier. Even with post.price = 0, the fee
258
- // should be based on the actual price (1 ether).
259
- _setupMintMocks(1);
260
-
261
- CTPost[] memory existingPosts = new CTPost[](1);
262
- existingPosts[0] = CTPost({
263
- encodedIPFSUri: TEST_URI,
264
- totalSupply: 10,
265
- price: 0, // Attacker sets price to 0.
266
- category: 5,
267
- splitPercent: 0,
268
- splits: new JBSplit[](0)
269
- });
270
-
271
- // Fee = 1 ether / 20 = 0.05 ether
272
- // payValue = msg.value - fee = msg.value - 0.05 ether
273
- // totalPrice = 1 ether (from the store, not post.price)
274
- // Need: totalPrice <= payValue, i.e., 1 ether <= msg.value - 0.05 ether
275
- // So msg.value >= 1.05 ether
276
-
277
- // Sending exactly 1.05 ether should succeed.
278
- vm.prank(poster);
279
- publisher.mintFrom{value: 1.05 ether}(IJB721TiersHook(hookAddr), existingPosts, poster, poster, "", "");
280
-
281
- // Sending 1.04 ether should fail (1.04 - 0.05 = 0.99 < 1 ether totalPrice).
282
- vm.prank(poster);
283
- vm.expectRevert();
284
- publisher.mintFrom{value: 1.04 ether}(IJB721TiersHook(hookAddr), existingPosts, poster, poster, "", "");
285
- }
286
- }