@croptop/core-v6 0.0.36 → 0.0.37
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/package.json +8 -8
- package/test/audit/CodexNemesisCroptopPublisherBoundary.t.sol +329 -0
- package/test/audit/CodexNemesisCurrencyPoCs.t.sol +371 -0
- package/test/audit/CodexNemesisFreshRound.t.sol +395 -0
- package/test/audit/CodexNemesisMetadataShadow.t.sol +203 -0
- package/test/audit/CodexNemesisPolicyReuse.t.sol +168 -0
- package/test/audit/CodexNemesisUriDrift.t.sol +252 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@croptop/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v5'"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
20
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
22
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
23
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
24
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
25
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
19
|
+
"@bananapus/721-hook-v6": "^0.0.38",
|
|
20
|
+
"@bananapus/buyback-hook-v6": "^0.0.30",
|
|
21
|
+
"@bananapus/core-v6": "^0.0.36",
|
|
22
|
+
"@bananapus/ownable-v6": "^0.0.20",
|
|
23
|
+
"@bananapus/permission-ids-v6": "^0.0.19",
|
|
24
|
+
"@bananapus/router-terminal-v6": "^0.0.30",
|
|
25
|
+
"@bananapus/suckers-v6": "^0.0.28",
|
|
26
26
|
"@openzeppelin/contracts": "^5.6.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
7
|
+
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
8
|
+
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
9
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
10
|
+
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
|
|
11
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
12
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
13
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
14
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
15
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
16
|
+
|
|
17
|
+
import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
|
|
18
|
+
import {CTPost} from "../../src/structs/CTPost.sol";
|
|
19
|
+
import {CTPublisher} from "../../src/CTPublisher.sol";
|
|
20
|
+
|
|
21
|
+
contract NemesisMockPermissions is IJBPermissions {
|
|
22
|
+
function WILDCARD_PROJECT_ID() external pure returns (uint256) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasPermission(address, address, uint256, uint256, bool, bool) external pure returns (bool) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasPermissions(address, address, uint256, uint256[] calldata, bool, bool) external pure returns (bool) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function permissionsOf(address, address, uint256) external pure returns (uint256) {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setPermissionsFor(address, JBPermissionsData calldata) external {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
contract NemesisMockTerminal {
|
|
42
|
+
mapping(uint256 projectId => uint256 amount) public paidToProject;
|
|
43
|
+
|
|
44
|
+
function pay(
|
|
45
|
+
uint256 projectId,
|
|
46
|
+
address,
|
|
47
|
+
uint256,
|
|
48
|
+
address,
|
|
49
|
+
uint256,
|
|
50
|
+
string calldata,
|
|
51
|
+
bytes calldata
|
|
52
|
+
)
|
|
53
|
+
external
|
|
54
|
+
payable
|
|
55
|
+
returns (uint256)
|
|
56
|
+
{
|
|
57
|
+
paidToProject[projectId] += msg.value;
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
contract NemesisMockDirectory {
|
|
63
|
+
mapping(uint256 projectId => address terminal) public terminalOf;
|
|
64
|
+
|
|
65
|
+
function setTerminal(uint256 projectId, address terminal) external {
|
|
66
|
+
terminalOf[projectId] = terminal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function primaryTerminalOf(uint256 projectId, address) external view returns (IJBTerminal) {
|
|
70
|
+
return IJBTerminal(terminalOf[projectId]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
contract NemesisMockStore {
|
|
75
|
+
struct StoredTier {
|
|
76
|
+
uint104 price;
|
|
77
|
+
uint32 initialSupply;
|
|
78
|
+
uint32 remainingSupply;
|
|
79
|
+
bytes32 encodedIPFSUri;
|
|
80
|
+
bool removed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
uint256 public maxTierId;
|
|
84
|
+
mapping(uint256 tierId => StoredTier) public tierData;
|
|
85
|
+
|
|
86
|
+
function encodedUriOf(uint256 tierId) external view returns (bytes32) {
|
|
87
|
+
return tierData[tierId].encodedIPFSUri;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function addTier(JB721TierConfig memory config) external returns (uint256 tierId) {
|
|
91
|
+
tierId = ++maxTierId;
|
|
92
|
+
tierData[tierId] = StoredTier({
|
|
93
|
+
price: config.price,
|
|
94
|
+
initialSupply: config.initialSupply,
|
|
95
|
+
remainingSupply: config.initialSupply,
|
|
96
|
+
encodedIPFSUri: config.encodedIPFSUri,
|
|
97
|
+
removed: false
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setEncodedUri(uint256 tierId, bytes32 encodedIPFSUri) external {
|
|
102
|
+
tierData[tierId].encodedIPFSUri = encodedIPFSUri;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function maxTierIdOf(address) external view returns (uint256) {
|
|
106
|
+
return maxTierId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isTierRemoved(address, uint256 tierId) external view returns (bool) {
|
|
110
|
+
return tierData[tierId].removed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function tierOf(address, uint256 tierId, bool) external view returns (JB721Tier memory tier) {
|
|
114
|
+
StoredTier memory stored = tierData[tierId];
|
|
115
|
+
tier = JB721Tier({
|
|
116
|
+
id: uint32(tierId),
|
|
117
|
+
price: stored.price,
|
|
118
|
+
remainingSupply: stored.remainingSupply,
|
|
119
|
+
initialSupply: stored.initialSupply,
|
|
120
|
+
votingUnits: 0,
|
|
121
|
+
reserveFrequency: 0,
|
|
122
|
+
reserveBeneficiary: address(0),
|
|
123
|
+
encodedIPFSUri: stored.encodedIPFSUri,
|
|
124
|
+
category: 0,
|
|
125
|
+
discountPercent: 0,
|
|
126
|
+
flags: JB721TierFlags({
|
|
127
|
+
allowOwnerMint: false,
|
|
128
|
+
transfersPausable: false,
|
|
129
|
+
cantBeRemoved: false,
|
|
130
|
+
cantIncreaseDiscountPercent: false,
|
|
131
|
+
cantBuyWithCredits: false
|
|
132
|
+
}),
|
|
133
|
+
splitPercent: 0,
|
|
134
|
+
resolvedUri: ""
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
contract NemesisMutableHook {
|
|
140
|
+
uint256 public immutable PROJECT_ID;
|
|
141
|
+
IJB721TiersHookStore public immutable STORE;
|
|
142
|
+
address public ownerAddress;
|
|
143
|
+
|
|
144
|
+
constructor(uint256 projectId, IJB721TiersHookStore store_, address owner_) {
|
|
145
|
+
PROJECT_ID = projectId;
|
|
146
|
+
STORE = store_;
|
|
147
|
+
ownerAddress = owner_;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function owner() external view returns (address) {
|
|
151
|
+
return ownerAddress;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function METADATA_ID_TARGET() external view returns (address) {
|
|
155
|
+
return address(this);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata) external {
|
|
159
|
+
for (uint256 i; i < tiersToAdd.length; i++) {
|
|
160
|
+
NemesisMockStore(address(STORE)).addTier(tiersToAdd[i]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setMetadata(
|
|
165
|
+
string calldata,
|
|
166
|
+
string calldata,
|
|
167
|
+
string calldata,
|
|
168
|
+
string calldata,
|
|
169
|
+
address,
|
|
170
|
+
uint256 encodedIPFSUriTierId,
|
|
171
|
+
bytes32 encodedIPFSUri
|
|
172
|
+
)
|
|
173
|
+
external
|
|
174
|
+
{
|
|
175
|
+
NemesisMockStore(address(STORE)).setEncodedUri(encodedIPFSUriTierId, encodedIPFSUri);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
contract CodexNemesisCroptopPublisherBoundaryTest is Test {
|
|
180
|
+
uint256 internal constant FEE_PROJECT_ID = 1;
|
|
181
|
+
uint256 internal constant PROJECT_ID = 2;
|
|
182
|
+
|
|
183
|
+
bytes32 internal constant URI_A = keccak256("uri-a");
|
|
184
|
+
bytes32 internal constant URI_B = keccak256("uri-b");
|
|
185
|
+
|
|
186
|
+
address internal hookOwner = makeAddr("hookOwner");
|
|
187
|
+
address internal unrestrictedPoster = makeAddr("unrestrictedPoster");
|
|
188
|
+
address internal restrictedPoster = makeAddr("restrictedPoster");
|
|
189
|
+
address internal outsider = makeAddr("outsider");
|
|
190
|
+
|
|
191
|
+
NemesisMockPermissions internal permissions;
|
|
192
|
+
NemesisMockDirectory internal directory;
|
|
193
|
+
NemesisMockStore internal store;
|
|
194
|
+
NemesisMutableHook internal hook;
|
|
195
|
+
NemesisMockTerminal internal projectTerminal;
|
|
196
|
+
NemesisMockTerminal internal feeTerminal;
|
|
197
|
+
CTPublisher internal publisher;
|
|
198
|
+
|
|
199
|
+
function setUp() public {
|
|
200
|
+
permissions = new NemesisMockPermissions();
|
|
201
|
+
directory = new NemesisMockDirectory();
|
|
202
|
+
store = new NemesisMockStore();
|
|
203
|
+
hook = new NemesisMutableHook(PROJECT_ID, IJB721TiersHookStore(address(store)), hookOwner);
|
|
204
|
+
projectTerminal = new NemesisMockTerminal();
|
|
205
|
+
feeTerminal = new NemesisMockTerminal();
|
|
206
|
+
publisher = new CTPublisher(IJBDirectory(address(directory)), permissions, FEE_PROJECT_ID, address(0));
|
|
207
|
+
|
|
208
|
+
directory.setTerminal(PROJECT_ID, address(projectTerminal));
|
|
209
|
+
directory.setTerminal(FEE_PROJECT_ID, address(feeTerminal));
|
|
210
|
+
|
|
211
|
+
vm.deal(unrestrictedPoster, 100 ether);
|
|
212
|
+
vm.deal(restrictedPoster, 100 ether);
|
|
213
|
+
vm.deal(outsider, 100 ether);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function test_existingTierReuseBypassesUpdatedAllowlistAndPriceFloor() external {
|
|
217
|
+
_configureCategory(1, 1 ether, _singletonArray(unrestrictedPoster));
|
|
218
|
+
|
|
219
|
+
vm.prank(unrestrictedPoster);
|
|
220
|
+
publisher.mintFrom{value: 2 ether}(
|
|
221
|
+
IJB721TiersHook(address(hook)),
|
|
222
|
+
_singlePost({uri: URI_A, price: 1 ether, category: 1}),
|
|
223
|
+
unrestrictedPoster,
|
|
224
|
+
unrestrictedPoster,
|
|
225
|
+
"",
|
|
226
|
+
""
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "initial publish should cache tier 1");
|
|
230
|
+
|
|
231
|
+
// Tighten the policy so only `restrictedPoster` can publish, and only at >= 5 ether.
|
|
232
|
+
_configureCategory(1, 5 ether, _singletonArray(restrictedPoster));
|
|
233
|
+
|
|
234
|
+
vm.prank(outsider);
|
|
235
|
+
publisher.mintFrom{value: 2 ether}(
|
|
236
|
+
IJB721TiersHook(address(hook)), _singlePost({uri: URI_A, price: 0, category: 1}), outsider, outsider, "", ""
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// The outsider's second call succeeds because existing-tier reuse skips the allowlist and price checks.
|
|
240
|
+
assertEq(store.maxTierId(), 1, "reuse path should mint from the old tier instead of creating a new one");
|
|
241
|
+
assertEq(
|
|
242
|
+
projectTerminal.paidToProject(PROJECT_ID),
|
|
243
|
+
3.9 ether,
|
|
244
|
+
"both mints should settle against the stale reused tier price"
|
|
245
|
+
);
|
|
246
|
+
assertEq(
|
|
247
|
+
feeTerminal.paidToProject(FEE_PROJECT_ID),
|
|
248
|
+
0.1 ether,
|
|
249
|
+
"fee routing still uses the stale reused tier price instead of the new stricter floor"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function test_hookMetadataMutationDesyncsPublisherCacheAndAllowsDuplicateTier() external {
|
|
254
|
+
_configureCategory(1, 1 ether, new address[](0));
|
|
255
|
+
|
|
256
|
+
vm.prank(unrestrictedPoster);
|
|
257
|
+
publisher.mintFrom{value: 2 ether}(
|
|
258
|
+
IJB721TiersHook(address(hook)),
|
|
259
|
+
_singlePost({uri: URI_A, price: 1 ether, category: 1}),
|
|
260
|
+
unrestrictedPoster,
|
|
261
|
+
unrestrictedPoster,
|
|
262
|
+
"",
|
|
263
|
+
""
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "publisher cache should point at tier 1");
|
|
267
|
+
assertEq(store.encodedUriOf(1), URI_A, "canonical hook metadata should start at uri A");
|
|
268
|
+
|
|
269
|
+
// The hook owner changes the canonical tier URI through the underlying 721 hook.
|
|
270
|
+
vm.prank(hookOwner);
|
|
271
|
+
hook.setMetadata("", "", "", "", address(0), 1, URI_B);
|
|
272
|
+
|
|
273
|
+
assertEq(store.encodedUriOf(1), URI_B, "hook metadata now says tier 1 is uri B");
|
|
274
|
+
assertEq(
|
|
275
|
+
publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A),
|
|
276
|
+
1,
|
|
277
|
+
"publisher cache is stale and still thinks uri A owns tier 1"
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
vm.prank(unrestrictedPoster);
|
|
281
|
+
publisher.mintFrom{value: 2 ether}(
|
|
282
|
+
IJB721TiersHook(address(hook)),
|
|
283
|
+
_singlePost({uri: URI_B, price: 1 ether, category: 1}),
|
|
284
|
+
unrestrictedPoster,
|
|
285
|
+
unrestrictedPoster,
|
|
286
|
+
"",
|
|
287
|
+
""
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Croptop creates a second tier for the same canonical URI because it never re-syncs against hook metadata.
|
|
291
|
+
assertEq(store.maxTierId(), 2, "publisher should have created a duplicate tier after the metadata drift");
|
|
292
|
+
assertEq(store.encodedUriOf(1), URI_B, "tier 1 still resolves to uri B");
|
|
293
|
+
assertEq(store.encodedUriOf(2), URI_B, "tier 2 now also resolves to uri B");
|
|
294
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_B), 2, "cache now points uri B at tier 2");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function _configureCategory(uint24 category, uint104 minimumPrice, address[] memory allowedAddresses) internal {
|
|
298
|
+
CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
|
|
299
|
+
allowedPosts[0] = CTAllowedPost({
|
|
300
|
+
hook: address(hook),
|
|
301
|
+
category: category,
|
|
302
|
+
minimumPrice: minimumPrice,
|
|
303
|
+
minimumTotalSupply: 1,
|
|
304
|
+
maximumTotalSupply: 100,
|
|
305
|
+
maximumSplitPercent: 0,
|
|
306
|
+
allowedAddresses: allowedAddresses
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
vm.prank(hookOwner);
|
|
310
|
+
publisher.configurePostingCriteriaFor(allowedPosts);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function _singlePost(bytes32 uri, uint104 price, uint24 category) internal pure returns (CTPost[] memory posts) {
|
|
314
|
+
posts = new CTPost[](1);
|
|
315
|
+
posts[0] = CTPost({
|
|
316
|
+
encodedIPFSUri: uri,
|
|
317
|
+
totalSupply: 10,
|
|
318
|
+
price: price,
|
|
319
|
+
category: category,
|
|
320
|
+
splitPercent: 0,
|
|
321
|
+
splits: new JBSplit[](0)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function _singletonArray(address account) internal pure returns (address[] memory addrs) {
|
|
326
|
+
addrs = new address[](1);
|
|
327
|
+
addrs[0] = account;
|
|
328
|
+
}
|
|
329
|
+
}
|