@croptop/core-v6 0.0.36 → 0.0.38
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/src/CTPublisher.sol +20 -5
- 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 +196 -0
- package/test/audit/CodexNemesisPolicyReuse.t.sol +168 -0
- package/test/audit/CodexNemesisUriDrift.t.sol +252 -0
- package/test/audit/Pass12Fixes.t.sol +388 -0
- package/test/fork/PublishFork.t.sol +2 -2
- package/test/regression/StaleTierIdMapping.t.sol +10 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "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 {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
15
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
16
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
17
|
+
|
|
18
|
+
import {CTPublisher} from "../../src/CTPublisher.sol";
|
|
19
|
+
import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
|
|
20
|
+
import {CTPost} from "../../src/structs/CTPost.sol";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Minimal mock contracts (reusable across both tests)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
contract P12MockTerminal {
|
|
27
|
+
function pay(
|
|
28
|
+
uint256,
|
|
29
|
+
address,
|
|
30
|
+
uint256,
|
|
31
|
+
address,
|
|
32
|
+
uint256,
|
|
33
|
+
string calldata,
|
|
34
|
+
bytes calldata
|
|
35
|
+
)
|
|
36
|
+
external
|
|
37
|
+
payable
|
|
38
|
+
returns (uint256)
|
|
39
|
+
{
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
contract P12MockDirectory {
|
|
45
|
+
// forge-lint: disable-next-line(screaming-snake-case-immutable)
|
|
46
|
+
IJBTerminal internal immutable _terminal;
|
|
47
|
+
|
|
48
|
+
constructor(IJBTerminal terminal_) {
|
|
49
|
+
_terminal = terminal_;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function primaryTerminalOf(uint256, address) external view returns (IJBTerminal) {
|
|
53
|
+
return _terminal;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
contract P12MockStore {
|
|
58
|
+
struct TierData {
|
|
59
|
+
bytes32 uri;
|
|
60
|
+
uint104 price;
|
|
61
|
+
uint24 category;
|
|
62
|
+
uint32 supply;
|
|
63
|
+
bool removed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
uint256 internal _maxTierId;
|
|
67
|
+
mapping(uint256 tierId => TierData) internal _tiers;
|
|
68
|
+
|
|
69
|
+
function maxTierIdOf(address) external view returns (uint256) {
|
|
70
|
+
return _maxTierId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isTierRemoved(address, uint256 tierId) external view returns (bool) {
|
|
74
|
+
return _tiers[tierId].removed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tierOf(address, uint256 tierId, bool) external view returns (JB721Tier memory) {
|
|
78
|
+
TierData memory tier = _tiers[tierId];
|
|
79
|
+
return JB721Tier({
|
|
80
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
81
|
+
id: uint32(tierId),
|
|
82
|
+
price: tier.price,
|
|
83
|
+
remainingSupply: tier.supply,
|
|
84
|
+
initialSupply: tier.supply,
|
|
85
|
+
votingUnits: 0,
|
|
86
|
+
reserveFrequency: 0,
|
|
87
|
+
reserveBeneficiary: address(0),
|
|
88
|
+
encodedIPFSUri: tier.uri,
|
|
89
|
+
category: tier.category,
|
|
90
|
+
discountPercent: 0,
|
|
91
|
+
flags: JB721TierFlags({
|
|
92
|
+
allowOwnerMint: false,
|
|
93
|
+
transfersPausable: false,
|
|
94
|
+
cantBeRemoved: false,
|
|
95
|
+
cantIncreaseDiscountPercent: false,
|
|
96
|
+
cantBuyWithCredits: false
|
|
97
|
+
}),
|
|
98
|
+
splitPercent: 0,
|
|
99
|
+
resolvedUri: ""
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function addTier(JB721TierConfig calldata config) external returns (uint256 tierId) {
|
|
104
|
+
tierId = ++_maxTierId;
|
|
105
|
+
_tiers[tierId] = TierData({
|
|
106
|
+
uri: config.encodedIPFSUri,
|
|
107
|
+
price: config.price,
|
|
108
|
+
category: config.category,
|
|
109
|
+
supply: config.initialSupply,
|
|
110
|
+
removed: false
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
115
|
+
function setEncodedIPFSUriOf(uint256 tierId, bytes32 uri) external {
|
|
116
|
+
_tiers[tierId].uri = uri;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tierUri(uint256 tierId) external view returns (bytes32) {
|
|
120
|
+
return _tiers[tierId].uri;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function maxTierId() external view returns (uint256) {
|
|
124
|
+
return _maxTierId;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
contract P12MockHook {
|
|
129
|
+
address internal _owner;
|
|
130
|
+
uint256 public immutable PROJECT_ID;
|
|
131
|
+
// forge-lint: disable-next-line(screaming-snake-case-immutable)
|
|
132
|
+
P12MockStore internal immutable _store;
|
|
133
|
+
|
|
134
|
+
constructor(address owner_, uint256 projectId_, P12MockStore store_) {
|
|
135
|
+
_owner = owner_;
|
|
136
|
+
PROJECT_ID = projectId_;
|
|
137
|
+
_store = store_;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function owner() external view returns (address) {
|
|
141
|
+
return _owner;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
145
|
+
function STORE() external view returns (IJB721TiersHookStore) {
|
|
146
|
+
return IJB721TiersHookStore(address(_store));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
150
|
+
function METADATA_ID_TARGET() external view returns (address) {
|
|
151
|
+
return address(this);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata) external {
|
|
155
|
+
for (uint256 i; i < tiersToAdd.length; i++) {
|
|
156
|
+
_store.addTier(tiersToAdd[i]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function setMetadata(
|
|
161
|
+
string calldata,
|
|
162
|
+
string calldata,
|
|
163
|
+
string calldata,
|
|
164
|
+
string calldata,
|
|
165
|
+
address,
|
|
166
|
+
uint256 encodedIPFSUriTierId,
|
|
167
|
+
bytes32 encodedIPFSUri
|
|
168
|
+
)
|
|
169
|
+
external
|
|
170
|
+
{
|
|
171
|
+
require(msg.sender == _owner, "not owner");
|
|
172
|
+
_store.setEncodedIPFSUriOf(encodedIPFSUriTierId, encodedIPFSUri);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Test contract
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
/// @notice Regression tests for Pass 12 audit fixes:
|
|
181
|
+
/// H-26: Metadata shadow — additionalPayMetadata with duplicate pay ID
|
|
182
|
+
/// M-42: URI cache desync — tier URI changed via setMetadata
|
|
183
|
+
contract Pass12FixesTest is Test {
|
|
184
|
+
bytes32 internal constant URI_A = keccak256("uri-a");
|
|
185
|
+
bytes32 internal constant URI_B = keccak256("uri-b");
|
|
186
|
+
|
|
187
|
+
JBPermissions internal permissions;
|
|
188
|
+
P12MockTerminal internal terminal;
|
|
189
|
+
P12MockDirectory internal directory;
|
|
190
|
+
P12MockStore internal store;
|
|
191
|
+
P12MockHook internal hook;
|
|
192
|
+
CTPublisher internal publisher;
|
|
193
|
+
|
|
194
|
+
address internal hookOwner = makeAddr("hookOwner");
|
|
195
|
+
address internal poster = makeAddr("poster");
|
|
196
|
+
uint256 internal constant FEE_PROJECT_ID = 1;
|
|
197
|
+
uint256 internal constant PROJECT_ID = 42;
|
|
198
|
+
|
|
199
|
+
function setUp() public {
|
|
200
|
+
permissions = new JBPermissions(address(0));
|
|
201
|
+
terminal = new P12MockTerminal();
|
|
202
|
+
directory = new P12MockDirectory(IJBTerminal(address(terminal)));
|
|
203
|
+
store = new P12MockStore();
|
|
204
|
+
hook = new P12MockHook(hookOwner, PROJECT_ID, store);
|
|
205
|
+
publisher = new CTPublisher(
|
|
206
|
+
IJBDirectory(address(directory)), IJBPermissions(address(permissions)), FEE_PROJECT_ID, address(0)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
vm.deal(poster, 100 ether);
|
|
210
|
+
|
|
211
|
+
// Configure a category that allows posting.
|
|
212
|
+
CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
|
|
213
|
+
allowedPosts[0] = CTAllowedPost({
|
|
214
|
+
hook: address(hook),
|
|
215
|
+
category: 7,
|
|
216
|
+
minimumPrice: 1 ether,
|
|
217
|
+
minimumTotalSupply: 1,
|
|
218
|
+
maximumTotalSupply: 100,
|
|
219
|
+
maximumSplitPercent: 0,
|
|
220
|
+
allowedAddresses: new address[](0)
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
vm.prank(hookOwner);
|
|
224
|
+
publisher.configurePostingCriteriaFor(allowedPosts);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
// H-26: Metadata shadow — duplicate pay ID in additionalPayMetadata
|
|
229
|
+
// -----------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
/// @notice When additionalPayMetadata already contains an entry for the pay ID,
|
|
232
|
+
/// the fix should revert with CTPublisher_DuplicatePayMetadata.
|
|
233
|
+
function test_H26_fix_reverts_duplicate_metadata() public {
|
|
234
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
235
|
+
posts[0] = CTPost({
|
|
236
|
+
encodedIPFSUri: URI_A,
|
|
237
|
+
totalSupply: 10,
|
|
238
|
+
price: 1 ether,
|
|
239
|
+
category: 7,
|
|
240
|
+
splitPercent: 0,
|
|
241
|
+
splits: new JBSplit[](0)
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Build metadata that already contains the pay ID for this hook.
|
|
245
|
+
address metadataIdTarget = address(hook); // hook.METADATA_ID_TARGET() returns address(hook)
|
|
246
|
+
uint16[] memory forgedTierIds = new uint16[](1);
|
|
247
|
+
forgedTierIds[0] = 999; // Attacker's desired tier
|
|
248
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
249
|
+
bytes[] memory datas = new bytes[](1);
|
|
250
|
+
ids[0] = JBMetadataResolver.getId({purpose: "pay", target: metadataIdTarget});
|
|
251
|
+
datas[0] = abi.encode(true, forgedTierIds);
|
|
252
|
+
bytes memory shadowingMetadata = JBMetadataResolver.createMetadata(ids, datas);
|
|
253
|
+
|
|
254
|
+
vm.prank(poster);
|
|
255
|
+
vm.expectRevert(CTPublisher.CTPublisher_DuplicatePayMetadata.selector);
|
|
256
|
+
publisher.mintFrom{value: 1.05 ether}(
|
|
257
|
+
IJB721TiersHook(address(hook)), posts, poster, poster, shadowingMetadata, ""
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// @notice Empty additionalPayMetadata should NOT revert.
|
|
262
|
+
function test_H26_fix_allows_empty_metadata() public {
|
|
263
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
264
|
+
posts[0] = CTPost({
|
|
265
|
+
encodedIPFSUri: URI_A,
|
|
266
|
+
totalSupply: 10,
|
|
267
|
+
price: 1 ether,
|
|
268
|
+
category: 7,
|
|
269
|
+
splitPercent: 0,
|
|
270
|
+
splits: new JBSplit[](0)
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Empty metadata — should succeed.
|
|
274
|
+
vm.prank(poster);
|
|
275
|
+
publisher.mintFrom{value: 1.05 ether}(IJB721TiersHook(address(hook)), posts, poster, poster, "", "");
|
|
276
|
+
|
|
277
|
+
assertEq(
|
|
278
|
+
publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "tier should be created with empty metadata"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// @notice additionalPayMetadata with a DIFFERENT ID (not the pay ID) should NOT revert.
|
|
283
|
+
function test_H26_fix_allows_unrelated_metadata() public {
|
|
284
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
285
|
+
posts[0] = CTPost({
|
|
286
|
+
encodedIPFSUri: URI_A,
|
|
287
|
+
totalSupply: 10,
|
|
288
|
+
price: 1 ether,
|
|
289
|
+
category: 7,
|
|
290
|
+
splitPercent: 0,
|
|
291
|
+
splits: new JBSplit[](0)
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Build metadata with a different purpose — should NOT trigger the check.
|
|
295
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
296
|
+
bytes[] memory datas = new bytes[](1);
|
|
297
|
+
ids[0] = JBMetadataResolver.getId({purpose: "unrelated", target: address(hook)});
|
|
298
|
+
datas[0] = abi.encode(uint256(42));
|
|
299
|
+
bytes memory unrelatedMetadata = JBMetadataResolver.createMetadata(ids, datas);
|
|
300
|
+
|
|
301
|
+
vm.prank(poster);
|
|
302
|
+
publisher.mintFrom{value: 1.05 ether}(
|
|
303
|
+
IJB721TiersHook(address(hook)), posts, poster, poster, unrelatedMetadata, ""
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
assertEq(
|
|
307
|
+
publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A),
|
|
308
|
+
1,
|
|
309
|
+
"tier should be created with unrelated metadata"
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// -----------------------------------------------------------------------
|
|
314
|
+
// M-42: URI cache desync — tier URI changed via setMetadata
|
|
315
|
+
// -----------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
/// @notice When a tier's URI is changed via setMetadata, the cache entry
|
|
318
|
+
/// (old URI -> tier ID) becomes stale. The fix should detect
|
|
319
|
+
/// the mismatch and clear the cache, creating a new tier.
|
|
320
|
+
function test_M42_fix_clears_stale_cache() public {
|
|
321
|
+
// Step 1: Publish URI_A — creates tier 1.
|
|
322
|
+
_publish(URI_A);
|
|
323
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A cached as tier 1");
|
|
324
|
+
|
|
325
|
+
// Step 2: Owner changes tier 1's URI from URI_A to URI_B via setMetadata.
|
|
326
|
+
vm.prank(hookOwner);
|
|
327
|
+
hook.setMetadata("", "", "", "", address(this), 1, URI_B);
|
|
328
|
+
|
|
329
|
+
// The publisher cache still maps URI_A -> tier 1, but tier 1 now has URI_B.
|
|
330
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "stale cache still maps URI_A -> tier 1");
|
|
331
|
+
|
|
332
|
+
// Step 3: Try to publish URI_A again. The fix should detect the mismatch
|
|
333
|
+
// (tier 1's actual URI is URI_B, not URI_A), clear the stale cache, and
|
|
334
|
+
// create a new tier 2 for URI_A.
|
|
335
|
+
_publish(URI_A);
|
|
336
|
+
|
|
337
|
+
assertEq(store.maxTierId(), 2, "new tier should be created for URI_A after cache invalidation");
|
|
338
|
+
assertEq(
|
|
339
|
+
publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 2, "URI_A should now map to tier 2 (fresh tier)"
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// @notice When the tier was removed, the cache should still be cleared (existing behavior preserved).
|
|
344
|
+
function test_M42_fix_still_handles_removed_tiers() public {
|
|
345
|
+
// Use vm.mockCall to simulate isTierRemoved returning true for tier 1.
|
|
346
|
+
_publish(URI_A);
|
|
347
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A cached as tier 1");
|
|
348
|
+
|
|
349
|
+
// Mock isTierRemoved to return true for tier 1.
|
|
350
|
+
vm.mockCall(
|
|
351
|
+
address(store),
|
|
352
|
+
abi.encodeWithSelector(IJB721TiersHookStore.isTierRemoved.selector, address(hook), uint256(1)),
|
|
353
|
+
abi.encode(true)
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Publish URI_A again — should clear stale mapping and create tier 2.
|
|
357
|
+
_publish(URI_A);
|
|
358
|
+
|
|
359
|
+
assertEq(store.maxTierId(), 2, "new tier should be created after tier removal");
|
|
360
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 2, "URI_A should map to new tier 2");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// @notice When a cached tier's URI still matches, it should be reused (no regression).
|
|
364
|
+
function test_M42_fix_reuses_valid_cache() public {
|
|
365
|
+
// Publish URI_A — creates tier 1.
|
|
366
|
+
_publish(URI_A);
|
|
367
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A cached as tier 1");
|
|
368
|
+
|
|
369
|
+
// Publish URI_A again — URI still matches, should reuse tier 1 (no new tier created).
|
|
370
|
+
_publish(URI_A);
|
|
371
|
+
assertEq(store.maxTierId(), 1, "no new tier should be created for matching URI");
|
|
372
|
+
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A still maps to tier 1");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// -----------------------------------------------------------------------
|
|
376
|
+
// Helper
|
|
377
|
+
// -----------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
function _publish(bytes32 uri) internal {
|
|
380
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
381
|
+
posts[0] = CTPost({
|
|
382
|
+
encodedIPFSUri: uri, totalSupply: 10, price: 1 ether, category: 7, splitPercent: 0, splits: new JBSplit[](0)
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
vm.prank(poster);
|
|
386
|
+
publisher.mintFrom{value: 1.05 ether}(IJB721TiersHook(address(hook)), posts, poster, poster, "", "");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -129,8 +129,8 @@ contract PublishForkTest is Test, DeployPermit2 {
|
|
|
129
129
|
// ─────────────────────────────────────
|
|
130
130
|
|
|
131
131
|
function setUp() public {
|
|
132
|
-
// Fork ETH mainnet.
|
|
133
|
-
vm.createSelectFork("ethereum");
|
|
132
|
+
// Fork ETH mainnet at a pinned block to avoid RPC tip-of-chain flakiness.
|
|
133
|
+
vm.createSelectFork("ethereum", 24_960_000);
|
|
134
134
|
|
|
135
135
|
// Deploy all JB core contracts fresh within the fork.
|
|
136
136
|
_deployJBCore();
|
|
@@ -129,6 +129,16 @@ contract L52_StaleTierIdMapping is Test {
|
|
|
129
129
|
abi.encode(true)
|
|
130
130
|
);
|
|
131
131
|
|
|
132
|
+
// Mock tierOf for the removed tier — the M-42 fix calls tierOf before checking isTierRemoved.
|
|
133
|
+
JB721Tier memory removedTier;
|
|
134
|
+
removedTier.id = 1;
|
|
135
|
+
removedTier.encodedIPFSUri = TEST_URI;
|
|
136
|
+
vm.mockCall(
|
|
137
|
+
hookStoreAddr,
|
|
138
|
+
abi.encodeWithSelector(IJB721TiersHookStore.tierOf.selector, hookAddr, 1, false),
|
|
139
|
+
abi.encode(removedTier)
|
|
140
|
+
);
|
|
141
|
+
|
|
132
142
|
// Update maxTierId to 1 so new tier gets ID 2.
|
|
133
143
|
_setupMintMocks(1);
|
|
134
144
|
|