@croptop/core-v6 0.0.1
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/LICENSE +21 -0
- package/README.md +45 -0
- package/SKILLS.md +88 -0
- package/deployments/croptop-core-v5/arbitrum/CTDeployer.json +1896 -0
- package/deployments/croptop-core-v5/arbitrum/CTProjectOwner.json +186 -0
- package/deployments/croptop-core-v5/arbitrum/CTPublisher.json +738 -0
- package/deployments/croptop-core-v5/arbitrum_sepolia/CTDeployer.json +1883 -0
- package/deployments/croptop-core-v5/arbitrum_sepolia/CTProjectOwner.json +186 -0
- package/deployments/croptop-core-v5/arbitrum_sepolia/CTPublisher.json +738 -0
- package/deployments/croptop-core-v5/base/CTDeployer.json +1908 -0
- package/deployments/croptop-core-v5/base/CTProjectOwner.json +190 -0
- package/deployments/croptop-core-v5/base/CTPublisher.json +741 -0
- package/deployments/croptop-core-v5/base_sepolia/CTDeployer.json +1894 -0
- package/deployments/croptop-core-v5/base_sepolia/CTProjectOwner.json +190 -0
- package/deployments/croptop-core-v5/base_sepolia/CTPublisher.json +741 -0
- package/deployments/croptop-core-v5/ethereum/CTDeployer.json +1894 -0
- package/deployments/croptop-core-v5/ethereum/CTProjectOwner.json +190 -0
- package/deployments/croptop-core-v5/ethereum/CTPublisher.json +741 -0
- package/deployments/croptop-core-v5/optimism/CTDeployer.json +1894 -0
- package/deployments/croptop-core-v5/optimism/CTProjectOwner.json +190 -0
- package/deployments/croptop-core-v5/optimism/CTPublisher.json +741 -0
- package/deployments/croptop-core-v5/optimism_sepolia/CTDeployer.json +1894 -0
- package/deployments/croptop-core-v5/optimism_sepolia/CTProjectOwner.json +190 -0
- package/deployments/croptop-core-v5/optimism_sepolia/CTPublisher.json +741 -0
- package/deployments/croptop-core-v5/sepolia/CTDeployer.json +1894 -0
- package/deployments/croptop-core-v5/sepolia/CTProjectOwner.json +190 -0
- package/deployments/croptop-core-v5/sepolia/CTPublisher.json +741 -0
- package/foundry.toml +25 -0
- package/package.json +31 -0
- package/remappings.txt +2 -0
- package/script/ConfigureFeeProject.s.sol +386 -0
- package/script/Deploy.s.sol +138 -0
- package/script/helpers/CroptopDeploymentLib.sol +75 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +507 -0
- package/src/CTDeployer.sol +425 -0
- package/src/CTProjectOwner.sol +78 -0
- package/src/CTPublisher.sol +540 -0
- package/src/interfaces/ICTDeployer.sol +56 -0
- package/src/interfaces/ICTProjectOwner.sol +24 -0
- package/src/interfaces/ICTPublisher.sol +91 -0
- package/src/structs/CTAllowedPost.sol +22 -0
- package/src/structs/CTDeployerAllowedPost.sol +20 -0
- package/src/structs/CTPost.sol +22 -0
- package/src/structs/CTProjectConfig.sol +22 -0
- package/src/structs/CTSuckerDeploymentConfig.sol +11 -0
- package/test/CTPublisher.t.sol +672 -0
- package/test/CroptopAttacks.t.sol +439 -0
- package/test/Fork.t.sol +114 -0
- package/test/Test_MetadataGeneration.t.sol +70 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.23;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJBPermissions} from "@bananapus/core-v5/src/interfaces/IJBPermissions.sol";
|
|
7
|
+
import {IJBDirectory} from "@bananapus/core-v5/src/interfaces/IJBDirectory.sol";
|
|
8
|
+
import {IJBTerminal} from "@bananapus/core-v5/src/interfaces/IJBTerminal.sol";
|
|
9
|
+
import {IJBSplitHook} from "@bananapus/core-v5/src/interfaces/IJBSplitHook.sol";
|
|
10
|
+
import {IJBOwnable} from "@bananapus/ownable-v5/src/interfaces/IJBOwnable.sol";
|
|
11
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHook.sol";
|
|
12
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v5/src/interfaces/IJB721TiersHookStore.sol";
|
|
13
|
+
import {IJB721Hook} from "@bananapus/721-hook-v5/src/interfaces/IJB721Hook.sol";
|
|
14
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v5/src/structs/JB721TierConfig.sol";
|
|
15
|
+
import {JBConstants} from "@bananapus/core-v5/src/libraries/JBConstants.sol";
|
|
16
|
+
import {JBSplit} from "@bananapus/core-v5/src/structs/JBSplit.sol";
|
|
17
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v5/src/JBPermissionIds.sol";
|
|
18
|
+
|
|
19
|
+
import {CTPublisher} from "../src/CTPublisher.sol";
|
|
20
|
+
import {CTAllowedPost} from "../src/structs/CTAllowedPost.sol";
|
|
21
|
+
import {CTPost} from "../src/structs/CTPost.sol";
|
|
22
|
+
|
|
23
|
+
/// @title CroptopAttacks
|
|
24
|
+
/// @notice Adversarial security tests for CTPublisher focusing on mintFrom edge cases,
|
|
25
|
+
/// allowlist bypasses, input validation, and split percent enforcement.
|
|
26
|
+
contract CroptopAttacks 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 poster = makeAddr("poster");
|
|
37
|
+
address unauthorized = makeAddr("unauthorized");
|
|
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 test accounts so they can send ETH with mintFrom.
|
|
58
|
+
vm.deal(poster, 100 ether);
|
|
59
|
+
vm.deal(unauthorized, 100 ether);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// @dev Configure a standard category for testing.
|
|
63
|
+
function _configureCategory(uint24 category, uint104 minPrice, uint32 minSupply, uint32 maxSupply) internal {
|
|
64
|
+
CTAllowedPost[] memory posts = new CTAllowedPost[](1);
|
|
65
|
+
posts[0] = CTAllowedPost({
|
|
66
|
+
hook: hookAddr,
|
|
67
|
+
category: category,
|
|
68
|
+
minimumPrice: minPrice,
|
|
69
|
+
minimumTotalSupply: minSupply,
|
|
70
|
+
maximumTotalSupply: maxSupply,
|
|
71
|
+
maximumSplitPercent: 0,
|
|
72
|
+
allowedAddresses: new address[](0)
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vm.prank(hookOwner);
|
|
76
|
+
publisher.configurePostingCriteriaFor(posts);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// @dev Configure a category with an allowlist.
|
|
80
|
+
function _configureCategoryWithAllowlist(uint24 category, address[] memory allowed) internal {
|
|
81
|
+
CTAllowedPost[] memory posts = new CTAllowedPost[](1);
|
|
82
|
+
posts[0] = CTAllowedPost({
|
|
83
|
+
hook: hookAddr,
|
|
84
|
+
category: category,
|
|
85
|
+
minimumPrice: 0,
|
|
86
|
+
minimumTotalSupply: 1,
|
|
87
|
+
maximumTotalSupply: 100,
|
|
88
|
+
maximumSplitPercent: 0,
|
|
89
|
+
allowedAddresses: allowed
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
vm.prank(hookOwner);
|
|
93
|
+
publisher.configurePostingCriteriaFor(posts);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// @dev Configure a category with a maximum split percent.
|
|
97
|
+
function _configureCategoryWithSplits(
|
|
98
|
+
uint24 category,
|
|
99
|
+
uint104 minPrice,
|
|
100
|
+
uint32 minSupply,
|
|
101
|
+
uint32 maxSupply,
|
|
102
|
+
uint32 maxSplitPercent
|
|
103
|
+
)
|
|
104
|
+
internal
|
|
105
|
+
{
|
|
106
|
+
CTAllowedPost[] memory posts = new CTAllowedPost[](1);
|
|
107
|
+
posts[0] = CTAllowedPost({
|
|
108
|
+
hook: hookAddr,
|
|
109
|
+
category: category,
|
|
110
|
+
minimumPrice: minPrice,
|
|
111
|
+
minimumTotalSupply: minSupply,
|
|
112
|
+
maximumTotalSupply: maxSupply,
|
|
113
|
+
maximumSplitPercent: maxSplitPercent,
|
|
114
|
+
allowedAddresses: new address[](0)
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
vm.prank(hookOwner);
|
|
118
|
+
publisher.configurePostingCriteriaFor(posts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// @dev Set up mocks for mintFrom path.
|
|
122
|
+
function _setupMintMocks() internal {
|
|
123
|
+
vm.mockCall(
|
|
124
|
+
hookStoreAddr, abi.encodeWithSelector(IJB721TiersHookStore.maxTierIdOf.selector), abi.encode(uint256(0))
|
|
125
|
+
);
|
|
126
|
+
vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721TiersHook.adjustTiers.selector), abi.encode());
|
|
127
|
+
// METADATA_ID_TARGET() selector.
|
|
128
|
+
vm.mockCall(hookAddr, abi.encodeWithSelector(bytes4(keccak256("METADATA_ID_TARGET()"))), abi.encode(address(0)));
|
|
129
|
+
vm.mockCall(
|
|
130
|
+
address(directory),
|
|
131
|
+
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector),
|
|
132
|
+
abi.encode(terminalAddr)
|
|
133
|
+
);
|
|
134
|
+
vm.mockCall(terminalAddr, "", abi.encode(uint256(0)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =========================================================================
|
|
138
|
+
// Test 1: Post to unconfigured category — should revert
|
|
139
|
+
// =========================================================================
|
|
140
|
+
function test_mintFrom_unconfiguredCategory_reverts() public {
|
|
141
|
+
_setupMintMocks();
|
|
142
|
+
|
|
143
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
144
|
+
posts[0] = CTPost({
|
|
145
|
+
encodedIPFSUri: keccak256("test-content"),
|
|
146
|
+
totalSupply: 10,
|
|
147
|
+
price: 0.1 ether,
|
|
148
|
+
category: 999,
|
|
149
|
+
splitPercent: 0,
|
|
150
|
+
splits: new JBSplit[](0)
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
vm.prank(poster);
|
|
154
|
+
vm.expectRevert();
|
|
155
|
+
publisher.mintFrom{value: 0.1 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// =========================================================================
|
|
159
|
+
// Test 2: Post with price below minimum — should revert
|
|
160
|
+
// =========================================================================
|
|
161
|
+
function test_mintFrom_belowMinPrice_reverts() public {
|
|
162
|
+
_configureCategory(5, 0.1 ether, 1, 100);
|
|
163
|
+
_setupMintMocks();
|
|
164
|
+
|
|
165
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
166
|
+
posts[0] = CTPost({
|
|
167
|
+
encodedIPFSUri: keccak256("cheap-content"),
|
|
168
|
+
totalSupply: 10,
|
|
169
|
+
price: 0.01 ether,
|
|
170
|
+
category: 5,
|
|
171
|
+
splitPercent: 0,
|
|
172
|
+
splits: new JBSplit[](0)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
vm.prank(poster);
|
|
176
|
+
vm.expectRevert();
|
|
177
|
+
publisher.mintFrom{value: 0.01 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// =========================================================================
|
|
181
|
+
// Test 3: Post with supply above maximum — should revert
|
|
182
|
+
// =========================================================================
|
|
183
|
+
function test_mintFrom_exceedsMaxSupply_reverts() public {
|
|
184
|
+
_configureCategory(5, 0, 1, 50);
|
|
185
|
+
_setupMintMocks();
|
|
186
|
+
|
|
187
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
188
|
+
posts[0] = CTPost({
|
|
189
|
+
encodedIPFSUri: keccak256("big-supply"),
|
|
190
|
+
totalSupply: 100,
|
|
191
|
+
price: 0.01 ether,
|
|
192
|
+
category: 5,
|
|
193
|
+
splitPercent: 0,
|
|
194
|
+
splits: new JBSplit[](0)
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
vm.prank(poster);
|
|
198
|
+
vm.expectRevert();
|
|
199
|
+
publisher.mintFrom{value: 0.01 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =========================================================================
|
|
203
|
+
// Test 4: Post with supply below minimum — should revert
|
|
204
|
+
// =========================================================================
|
|
205
|
+
function test_mintFrom_belowMinSupply_reverts() public {
|
|
206
|
+
_configureCategory(5, 0, 10, 100);
|
|
207
|
+
_setupMintMocks();
|
|
208
|
+
|
|
209
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
210
|
+
posts[0] = CTPost({
|
|
211
|
+
encodedIPFSUri: keccak256("small-supply"),
|
|
212
|
+
totalSupply: 5,
|
|
213
|
+
price: 0.01 ether,
|
|
214
|
+
category: 5,
|
|
215
|
+
splitPercent: 0,
|
|
216
|
+
splits: new JBSplit[](0)
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
vm.prank(poster);
|
|
220
|
+
vm.expectRevert();
|
|
221
|
+
publisher.mintFrom{value: 0.01 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// =========================================================================
|
|
225
|
+
// Test 5: Allowlist bypass — non-allowed address posts to restricted category
|
|
226
|
+
// =========================================================================
|
|
227
|
+
function test_mintFrom_allowlistBypass_reverts() public {
|
|
228
|
+
address[] memory allowed = new address[](1);
|
|
229
|
+
allowed[0] = poster;
|
|
230
|
+
|
|
231
|
+
_configureCategoryWithAllowlist(7, allowed);
|
|
232
|
+
_setupMintMocks();
|
|
233
|
+
|
|
234
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
235
|
+
posts[0] = CTPost({
|
|
236
|
+
encodedIPFSUri: keccak256("sneaky-content"),
|
|
237
|
+
totalSupply: 10,
|
|
238
|
+
price: 0.01 ether,
|
|
239
|
+
category: 7,
|
|
240
|
+
splitPercent: 0,
|
|
241
|
+
splits: new JBSplit[](0)
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
vm.prank(unauthorized);
|
|
245
|
+
vm.expectRevert();
|
|
246
|
+
publisher.mintFrom{value: 0.01 ether}(IJB721TiersHook(hookAddr), posts, unauthorized, unauthorized, "", "");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// =========================================================================
|
|
250
|
+
// Test 6: Zero IPFS URI — should revert
|
|
251
|
+
// =========================================================================
|
|
252
|
+
function test_mintFrom_zeroIPFSUri_reverts() public {
|
|
253
|
+
_configureCategory(5, 0, 1, 100);
|
|
254
|
+
_setupMintMocks();
|
|
255
|
+
|
|
256
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
257
|
+
posts[0] = CTPost({
|
|
258
|
+
encodedIPFSUri: bytes32(0),
|
|
259
|
+
totalSupply: 10,
|
|
260
|
+
price: 0.01 ether,
|
|
261
|
+
category: 5,
|
|
262
|
+
splitPercent: 0,
|
|
263
|
+
splits: new JBSplit[](0)
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
vm.prank(poster);
|
|
267
|
+
vm.expectRevert();
|
|
268
|
+
publisher.mintFrom{value: 0.01 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// =========================================================================
|
|
272
|
+
// Test 7: Configure with zero minSupply reverts
|
|
273
|
+
// =========================================================================
|
|
274
|
+
function test_configure_zeroMinSupply_reverts() public {
|
|
275
|
+
CTAllowedPost[] memory posts = new CTAllowedPost[](1);
|
|
276
|
+
posts[0] = CTAllowedPost({
|
|
277
|
+
hook: hookAddr,
|
|
278
|
+
category: 5,
|
|
279
|
+
minimumPrice: 0,
|
|
280
|
+
minimumTotalSupply: 0,
|
|
281
|
+
maximumTotalSupply: 100,
|
|
282
|
+
maximumSplitPercent: 0,
|
|
283
|
+
allowedAddresses: new address[](0)
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
vm.prank(hookOwner);
|
|
287
|
+
vm.expectRevert(CTPublisher.CTPublisher_ZeroTotalSupply.selector);
|
|
288
|
+
publisher.configurePostingCriteriaFor(posts);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// =========================================================================
|
|
292
|
+
// Test 8: Configure without permission — should revert
|
|
293
|
+
// =========================================================================
|
|
294
|
+
function test_configure_noPermission_reverts() public {
|
|
295
|
+
vm.mockCall(
|
|
296
|
+
address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false)
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
CTAllowedPost[] memory posts = new CTAllowedPost[](1);
|
|
300
|
+
posts[0] = CTAllowedPost({
|
|
301
|
+
hook: hookAddr,
|
|
302
|
+
category: 1,
|
|
303
|
+
minimumPrice: 0,
|
|
304
|
+
minimumTotalSupply: 1,
|
|
305
|
+
maximumTotalSupply: 100,
|
|
306
|
+
maximumSplitPercent: 0,
|
|
307
|
+
allowedAddresses: new address[](0)
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
vm.prank(unauthorized);
|
|
311
|
+
vm.expectRevert();
|
|
312
|
+
publisher.configurePostingCriteriaFor(posts);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// =========================================================================
|
|
316
|
+
// Test 9: Split percent exceeds maximum — should revert
|
|
317
|
+
// =========================================================================
|
|
318
|
+
function test_mintFrom_splitPercentExceedsMaximum_reverts() public {
|
|
319
|
+
_configureCategoryWithSplits(5, 0, 1, 100, 500_000_000); // 50% max
|
|
320
|
+
_setupMintMocks();
|
|
321
|
+
|
|
322
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
323
|
+
posts[0] = CTPost({
|
|
324
|
+
encodedIPFSUri: keccak256("greedy-split"),
|
|
325
|
+
totalSupply: 10,
|
|
326
|
+
price: 0.1 ether,
|
|
327
|
+
category: 5,
|
|
328
|
+
splitPercent: 750_000_000, // 75% exceeds 50%
|
|
329
|
+
splits: new JBSplit[](0)
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
vm.prank(poster);
|
|
333
|
+
vm.expectRevert(
|
|
334
|
+
abi.encodeWithSelector(
|
|
335
|
+
CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 750_000_000, 500_000_000
|
|
336
|
+
)
|
|
337
|
+
);
|
|
338
|
+
publisher.mintFrom{value: 0.2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// =========================================================================
|
|
342
|
+
// Test 10: Split percent when splits disabled (max=0) — should revert
|
|
343
|
+
// =========================================================================
|
|
344
|
+
function test_mintFrom_splitPercentWhenDisabled_reverts() public {
|
|
345
|
+
_configureCategoryWithSplits(5, 0, 1, 100, 0); // Splits disabled
|
|
346
|
+
_setupMintMocks();
|
|
347
|
+
|
|
348
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
349
|
+
splits[0] = JBSplit({
|
|
350
|
+
percent: 100_000_000,
|
|
351
|
+
projectId: 0,
|
|
352
|
+
beneficiary: payable(poster),
|
|
353
|
+
preferAddToBalance: false,
|
|
354
|
+
lockedUntil: 0,
|
|
355
|
+
hook: IJBSplitHook(address(0))
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
359
|
+
posts[0] = CTPost({
|
|
360
|
+
encodedIPFSUri: keccak256("sneaky-split"),
|
|
361
|
+
totalSupply: 10,
|
|
362
|
+
price: 0.1 ether,
|
|
363
|
+
category: 5,
|
|
364
|
+
splitPercent: 100_000_000, // Any amount when disabled
|
|
365
|
+
splits: splits
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
vm.prank(poster);
|
|
369
|
+
vm.expectRevert(
|
|
370
|
+
abi.encodeWithSelector(CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 100_000_000, 0)
|
|
371
|
+
);
|
|
372
|
+
publisher.mintFrom{value: 0.2 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// =========================================================================
|
|
376
|
+
// Test 11: Attacker re-configures to raise split percent
|
|
377
|
+
// =========================================================================
|
|
378
|
+
function test_reconfigure_raiseSplitPercent_requiresPermission() public {
|
|
379
|
+
// Owner configures with 50% max split.
|
|
380
|
+
_configureCategoryWithSplits(5, 0, 1, 100, 500_000_000);
|
|
381
|
+
|
|
382
|
+
// Mock permissions to return false for unauthorized.
|
|
383
|
+
vm.mockCall(
|
|
384
|
+
address(permissions),
|
|
385
|
+
abi.encodeWithSelector(IJBPermissions.hasPermission.selector, unauthorized),
|
|
386
|
+
abi.encode(false)
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Attacker tries to reconfigure with 100% max split.
|
|
390
|
+
CTAllowedPost[] memory posts = new CTAllowedPost[](1);
|
|
391
|
+
posts[0] = CTAllowedPost({
|
|
392
|
+
hook: hookAddr,
|
|
393
|
+
category: 5,
|
|
394
|
+
minimumPrice: 0,
|
|
395
|
+
minimumTotalSupply: 1,
|
|
396
|
+
maximumTotalSupply: 100,
|
|
397
|
+
maximumSplitPercent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
398
|
+
allowedAddresses: new address[](0)
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
vm.prank(unauthorized);
|
|
402
|
+
vm.expectRevert();
|
|
403
|
+
publisher.configurePostingCriteriaFor(posts);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// =========================================================================
|
|
407
|
+
// Test 12: Multiple posts — first valid, second exceeds split
|
|
408
|
+
// =========================================================================
|
|
409
|
+
function test_mintFrom_batchWithOneExceedingSplit_reverts() public {
|
|
410
|
+
_configureCategoryWithSplits(5, 0, 1, 100, 500_000_000);
|
|
411
|
+
_setupMintMocks();
|
|
412
|
+
|
|
413
|
+
CTPost[] memory posts = new CTPost[](2);
|
|
414
|
+
posts[0] = CTPost({
|
|
415
|
+
encodedIPFSUri: keccak256("post-ok"),
|
|
416
|
+
totalSupply: 10,
|
|
417
|
+
price: 0.1 ether,
|
|
418
|
+
category: 5,
|
|
419
|
+
splitPercent: 250_000_000, // 25% OK
|
|
420
|
+
splits: new JBSplit[](0)
|
|
421
|
+
});
|
|
422
|
+
posts[1] = CTPost({
|
|
423
|
+
encodedIPFSUri: keccak256("post-bad"),
|
|
424
|
+
totalSupply: 10,
|
|
425
|
+
price: 0.1 ether,
|
|
426
|
+
category: 5,
|
|
427
|
+
splitPercent: 999_000_000, // 99.9% exceeds
|
|
428
|
+
splits: new JBSplit[](0)
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
vm.prank(poster);
|
|
432
|
+
vm.expectRevert(
|
|
433
|
+
abi.encodeWithSelector(
|
|
434
|
+
CTPublisher.CTPublisher_SplitPercentExceedsMaximum.selector, 999_000_000, 500_000_000
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
publisher.mintFrom{value: 0.4 ether}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
438
|
+
}
|
|
439
|
+
}
|
package/test/Fork.t.sol
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.17;
|
|
3
|
+
|
|
4
|
+
import "@bananapus/721-hook-v5/script/helpers/Hook721DeploymentLib.sol";
|
|
5
|
+
import "@bananapus/core-v5/script/helpers/CoreDeploymentLib.sol";
|
|
6
|
+
import "@bananapus/suckers-v5/script/helpers/SuckerDeploymentLib.sol";
|
|
7
|
+
|
|
8
|
+
import "./../src/CTDeployer.sol";
|
|
9
|
+
import {JBConstants} from "@bananapus/core-v5/src/libraries/JBConstants.sol";
|
|
10
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v5/src/structs/JBSuckerDeployerConfig.sol";
|
|
11
|
+
import {JBTokenMapping} from "@bananapus/suckers-v5/src/structs/JBTokenMapping.sol";
|
|
12
|
+
import {CTProjectOwner} from "./../src/CTProjectOwner.sol";
|
|
13
|
+
import {CTPublisher} from "./../src/CTPublisher.sol";
|
|
14
|
+
|
|
15
|
+
import "forge-std/Test.sol";
|
|
16
|
+
|
|
17
|
+
contract ForkTest is Test {
|
|
18
|
+
/// @notice tracks the deployment of the core contracts for the chain we are deploying to.
|
|
19
|
+
CoreDeployment core;
|
|
20
|
+
/// @notice tracks the deployment of the 721 hook contracts for the chain we are deploying to.
|
|
21
|
+
Hook721Deployment hook;
|
|
22
|
+
/// @notice tracks the deployment of the sucker contracts for the chain we are deploying to.
|
|
23
|
+
SuckerDeployment suckers;
|
|
24
|
+
|
|
25
|
+
CTPublisher publisher;
|
|
26
|
+
CTDeployer deployer;
|
|
27
|
+
|
|
28
|
+
address TRUSTED_FORWARDER;
|
|
29
|
+
|
|
30
|
+
function setUp() public {
|
|
31
|
+
// Fork ETH mainnet.
|
|
32
|
+
vm.createSelectFork(vm.rpcUrl("ethereum"), 22_432_742);
|
|
33
|
+
|
|
34
|
+
// Get the deployment addresses for the nana CORE for this chain.
|
|
35
|
+
// We want to do this outside of the `sphinx` modifier.
|
|
36
|
+
core = CoreDeploymentLib.getDeployment(
|
|
37
|
+
vm.envOr("NANA_CORE_DEPLOYMENT_PATH", string("node_modules/@bananapus/core-v5/deployments/"))
|
|
38
|
+
);
|
|
39
|
+
// Get the deployment addresses for the 721 hook contracts for this chain.
|
|
40
|
+
hook = Hook721DeploymentLib.getDeployment(
|
|
41
|
+
vm.envOr("NANA_721_DEPLOYMENT_PATH", string("node_modules/@bananapus/721-hook-v5/deployments/"))
|
|
42
|
+
);
|
|
43
|
+
// Get the deployment addresses for the suckers contracts for this chain.
|
|
44
|
+
suckers = SuckerDeploymentLib.getDeployment(
|
|
45
|
+
vm.envOr("NANA_SUCKERS_DEPLOYMENT_PATH", string("node_modules/@bananapus/suckers-v5/deployments/"))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// We use the same trusted forwarder as the core deployment.
|
|
49
|
+
TRUSTED_FORWARDER = core.controller.trustedForwarder();
|
|
50
|
+
|
|
51
|
+
// Deploy the croptop contracts.
|
|
52
|
+
publisher = new CTPublisher(core.directory, core.permissions, 1, TRUSTED_FORWARDER);
|
|
53
|
+
deployer = new CTDeployer(
|
|
54
|
+
core.permissions, core.projects, hook.hook_deployer, publisher, suckers.registry, TRUSTED_FORWARDER
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function testDeployProject(address owner) public {
|
|
59
|
+
vm.assume(owner != address(0) && owner.code.length == 0);
|
|
60
|
+
|
|
61
|
+
// Create the project config.
|
|
62
|
+
CTProjectConfig memory config = CTProjectConfig({
|
|
63
|
+
terminalConfigurations: new JBTerminalConfig[](0),
|
|
64
|
+
projectUri: "https://croptop.eth.sucks/",
|
|
65
|
+
allowedPosts: new CTDeployerAllowedPost[](0),
|
|
66
|
+
contractUri: "https://croptop.eth.sucks/",
|
|
67
|
+
name: "Croptop",
|
|
68
|
+
symbol: "CROP",
|
|
69
|
+
salt: bytes32(0)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
CTSuckerDeploymentConfig memory suckerConfig =
|
|
73
|
+
CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
|
|
74
|
+
|
|
75
|
+
deployer.deployProjectFor(owner, config, suckerConfig, core.controller);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function testDeployProjectWithSuckers(address owner, bytes32 salt, bytes32 suckerSalt) public {
|
|
79
|
+
vm.assume(owner != address(0) && owner.code.length == 0);
|
|
80
|
+
vm.assume(suckerSalt != bytes32(0));
|
|
81
|
+
|
|
82
|
+
// Create the project config.
|
|
83
|
+
CTProjectConfig memory config = CTProjectConfig({
|
|
84
|
+
terminalConfigurations: new JBTerminalConfig[](0),
|
|
85
|
+
projectUri: "https://croptop.eth.sucks/",
|
|
86
|
+
allowedPosts: new CTDeployerAllowedPost[](0),
|
|
87
|
+
contractUri: "https://croptop.eth.sucks/",
|
|
88
|
+
name: "Croptop",
|
|
89
|
+
symbol: "CROP",
|
|
90
|
+
salt: salt
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Create the sucker config.
|
|
94
|
+
JBTokenMapping[] memory tokens = new JBTokenMapping[](1);
|
|
95
|
+
tokens[0] = JBTokenMapping({
|
|
96
|
+
localToken: address(JBConstants.NATIVE_TOKEN),
|
|
97
|
+
minGas: 200_000,
|
|
98
|
+
remoteToken: address(JBConstants.NATIVE_TOKEN),
|
|
99
|
+
minBridgeAmount: 0.001 ether
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
JBSuckerDeployerConfig[] memory deployerConfigurations = new JBSuckerDeployerConfig[](1);
|
|
103
|
+
deployerConfigurations[0] = JBSuckerDeployerConfig({deployer: suckers.optimismDeployer, mappings: tokens});
|
|
104
|
+
|
|
105
|
+
CTSuckerDeploymentConfig memory suckerConfig =
|
|
106
|
+
CTSuckerDeploymentConfig({deployerConfigurations: deployerConfigurations, salt: suckerSalt});
|
|
107
|
+
|
|
108
|
+
// Deploy the project.
|
|
109
|
+
(uint256 projectId,) = deployer.deployProjectFor(owner, config, suckerConfig, core.controller);
|
|
110
|
+
|
|
111
|
+
// Check that the projectId has a sucker.
|
|
112
|
+
assertEq(suckers.registry.suckersOf(projectId).length, deployerConfigurations.length);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.17;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import {JBMetadataResolver} from "@bananapus/core-v5/src/libraries/JBMetadataResolver.sol";
|
|
6
|
+
|
|
7
|
+
import {MetadataResolverHelper} from "@bananapus/core-v5/test/helpers/MetadataResolverHelper.sol";
|
|
8
|
+
|
|
9
|
+
/// @notice Quick test to assert the creation of metadata while minting
|
|
10
|
+
/// @dev This test is not meant to be exhaustive, but to ensure that the metadata is valid.
|
|
11
|
+
/// It uses a mock contract which only returns a metadata following the logic
|
|
12
|
+
/// of the CroptopPublisher contract during mint. This external contract is used to recreate the same
|
|
13
|
+
contract Test_MetadataGeneration_Unit is Test {
|
|
14
|
+
/// @notice Create a new metadata from the _additionalPayMetadata and the datahook metadata (containing the tiers to
|
|
15
|
+
/// mint).
|
|
16
|
+
/// @dev Naming follows CroptopPublisher contract.
|
|
17
|
+
function test_metadataBuilding() public {
|
|
18
|
+
MetadataResolverHelper _resolverHelper = new MetadataResolverHelper();
|
|
19
|
+
|
|
20
|
+
// The intial metadata passed to the terminal
|
|
21
|
+
bytes4[] memory _ids = new bytes4[](10);
|
|
22
|
+
bytes[] memory _datas = new bytes[](10);
|
|
23
|
+
|
|
24
|
+
for (uint256 _i; _i < _ids.length; _i++) {
|
|
25
|
+
_ids[_i] = bytes4(uint32(_i + 1 * 1000));
|
|
26
|
+
_datas[_i] = abi.encode(
|
|
27
|
+
bytes1(uint8(_i + 1)), uint32(69), bytes2(uint16(_i + 69)), bytes32(uint256(type(uint256).max))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
bytes memory _additionalPayMetadata = _resolverHelper.createMetadata(_ids, _datas);
|
|
32
|
+
|
|
33
|
+
// The referal to include in the first 32 bytes of the metadata
|
|
34
|
+
uint256 FEE_PROJECT_ID = 420;
|
|
35
|
+
|
|
36
|
+
// The additional metadata to include
|
|
37
|
+
bytes4 datahookId = bytes4(bytes20(address(0xdeadbeef)));
|
|
38
|
+
uint256[] memory tierIdsToMint = new uint256[](9);
|
|
39
|
+
|
|
40
|
+
for (uint256 i = 0; i < 9; i++) {
|
|
41
|
+
tierIdsToMint[i] = i + 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Test: create the new metadata:
|
|
45
|
+
bytes memory mintMetadata = JBMetadataResolver.addToMetadata({
|
|
46
|
+
originalMetadata: _additionalPayMetadata, idToAdd: datahookId, dataToAdd: abi.encode(true, tierIdsToMint)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Add the referal id in the first 32 bytes
|
|
50
|
+
assembly {
|
|
51
|
+
mstore(add(mintMetadata, 32), FEE_PROJECT_ID)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
bytes memory targetData;
|
|
55
|
+
bool found;
|
|
56
|
+
|
|
57
|
+
// Check: both data are present and correct?
|
|
58
|
+
for (uint256 i = 0; i < _ids.length; i++) {
|
|
59
|
+
(found, targetData) = JBMetadataResolver.getDataFor(_ids[i], mintMetadata);
|
|
60
|
+
assertTrue(found, "metadata not found");
|
|
61
|
+
assertEq(targetData, _datas[i], "metadata not equal");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
(found, targetData) = JBMetadataResolver.getDataFor(datahookId, mintMetadata);
|
|
65
|
+
assertTrue(found, "datahook metadata not found");
|
|
66
|
+
assertEq(targetData, abi.encode(true, tierIdsToMint), "datahook not equal");
|
|
67
|
+
|
|
68
|
+
assertEq(uint256(bytes32(mintMetadata)), FEE_PROJECT_ID, "referal id not equal");
|
|
69
|
+
}
|
|
70
|
+
}
|