@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.
- package/README.md +2 -2
- package/foundry.toml +2 -1
- package/package.json +25 -13
- package/script/ConfigureFeeProject.s.sol +8 -5
- package/src/CTDeployer.sol +52 -51
- package/src/CTPublisher.sol +20 -5
- package/src/interfaces/ICTDeployer.sol +2 -2
- package/ADMINISTRATION.md +0 -94
- package/ARCHITECTURE.md +0 -96
- package/AUDIT_INSTRUCTIONS.md +0 -88
- package/RISKS.md +0 -78
- package/SKILLS.md +0 -46
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -134
- package/foundry.lock +0 -11
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -507
- package/test/CTDeployer.t.sol +0 -616
- package/test/CTProjectOwner.t.sol +0 -185
- package/test/CTPublisher.t.sol +0 -869
- package/test/ClaimCollectionOwnership.t.sol +0 -315
- package/test/CroptopAttacks.t.sol +0 -437
- package/test/Fork.t.sol +0 -227
- package/test/TestAuditGaps.sol +0 -696
- package/test/Test_MetadataGeneration.t.sol +0 -79
- package/test/audit/CodexNemesisCroptopPublisherBoundary.t.sol +0 -329
- package/test/audit/CodexNemesisCurrencyPoCs.t.sol +0 -371
- package/test/audit/CodexNemesisFreshRound.t.sol +0 -395
- package/test/audit/CodexNemesisMetadataShadow.t.sol +0 -203
- package/test/audit/CodexNemesisPoCs.t.sol +0 -263
- package/test/audit/CodexNemesisPolicyReuse.t.sol +0 -168
- package/test/audit/CodexNemesisUriDrift.t.sol +0 -252
- package/test/audit/DeployerPermissionBypass.t.sol +0 -213
- package/test/audit/EmptyPostFeeBypass.t.sol +0 -53
- package/test/audit/FeeBeneficiaryReentrancy.t.sol +0 -247
- package/test/audit/FeeFallbackBlackhole.t.sol +0 -263
- package/test/fork/PublishFork.t.sol +0 -440
- package/test/regression/DuplicateUriFeeEvasion.t.sol +0 -312
- package/test/regression/FeeEvasion.t.sol +0 -286
- 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
|
-
}
|