@bananapus/721-hook-v6 0.0.38 → 0.0.40

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,10 +17,10 @@
17
17
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/address-registry-v6": "^0.0.17",
21
- "@bananapus/core-v6": "^0.0.34",
22
- "@bananapus/ownable-v6": "^0.0.17",
23
- "@bananapus/permission-ids-v6": "^0.0.17",
20
+ "@bananapus/address-registry-v6": "^0.0.20",
21
+ "@bananapus/core-v6": "^0.0.36",
22
+ "@bananapus/ownable-v6": "^0.0.20",
23
+ "@bananapus/permission-ids-v6": "^0.0.19",
24
24
  "@openzeppelin/contracts": "^5.6.1",
25
25
  "@prb/math": "^4.1.0",
26
26
  "solady": "^0.1.8"
@@ -1291,6 +1291,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1291
1291
  // Set the tier being iterated upon (0-indexed).
1292
1292
  uint256 tierId = tierIds[i];
1293
1293
 
1294
+ // Reject tier IDs that don't exist yet — removing a future tier would cause it
1295
+ // to be born already removed when later added.
1296
+ if (tierId == 0 || tierId > maxTierIdOf[msg.sender]) {
1297
+ revert JB721TiersHookStore_UnrecognizedTier(tierId);
1298
+ }
1299
+
1294
1300
  // Get a reference to the stored tier.
1295
1301
  JBStored721Tier storage storedTier = _storedTierOf[msg.sender][tierId];
1296
1302
 
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
6
+
7
+ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
+ import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
9
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
10
+
11
+ contract Test_20260425CodexNemesisFutureTierRemoval is Test {
12
+ JB721TiersHookStore internal store;
13
+
14
+ function setUp() external {
15
+ store = new JB721TiersHookStore();
16
+ }
17
+
18
+ function test_futureRemovedTierIdIsBornRemovedAndCannotMint() external {
19
+ JB721TierConfig[] memory firstTier = new JB721TierConfig[](1);
20
+ firstTier[0] = _tier(1);
21
+ uint256[] memory firstIds = store.recordAddTiers(firstTier);
22
+ assertEq(firstIds[0], 1);
23
+
24
+ // Attempting to remove a future (nonexistent) tier ID should now revert
25
+ // thanks to the L-18 fix, preventing the "born removed" bug.
26
+ uint256[] memory futureIds = new uint256[](1);
27
+ futureIds[0] = 2;
28
+ vm.expectRevert(abi.encodeWithSignature("JB721TiersHookStore_UnrecognizedTier(uint256)", 2));
29
+ store.recordRemoveTierIds(futureIds);
30
+ }
31
+
32
+ function _tier(uint24 category) internal pure returns (JB721TierConfig memory tier) {
33
+ tier.price = 1;
34
+ tier.initialSupply = 10;
35
+ tier.category = category;
36
+ tier.flags = JB721TierConfigFlags({
37
+ allowOwnerMint: false,
38
+ useReserveBeneficiaryAsDefault: false,
39
+ transfersPausable: false,
40
+ useVotingUnits: false,
41
+ cantBeRemoved: false,
42
+ cantIncreaseDiscountPercent: false,
43
+ cantBuyWithCredits: false
44
+ });
45
+ tier.splits = new JBSplit[](0);
46
+ }
47
+ }
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
6
+
7
+ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
+ import {JB721Tier} from "../../src/structs/JB721Tier.sol";
9
+ import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
10
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
11
+
12
+ contract Test_20260425CodexNemesisReserveActivation is Test {
13
+ JB721TiersHookStore internal store;
14
+
15
+ function setUp() external {
16
+ store = new JB721TiersHookStore();
17
+ }
18
+
19
+ function test_retroactiveDefaultReserveBeneficiaryCreatesUnmintablePendingReserves() external {
20
+ JB721TierConfig[] memory initialTiers = new JB721TierConfig[](1);
21
+ initialTiers[0] = _tier({
22
+ price: 1,
23
+ initialSupply: 10,
24
+ reserveFrequency: 2,
25
+ reserveBeneficiary: address(0),
26
+ useReserveBeneficiaryAsDefault: false,
27
+ category: 1
28
+ });
29
+ store.recordAddTiers(initialTiers);
30
+
31
+ uint16[] memory tierIds = new uint16[](10);
32
+ for (uint256 i; i < tierIds.length; i++) {
33
+ tierIds[i] = 1;
34
+ }
35
+ store.recordMint({amount: 10, tierIds: tierIds, isOwnerMint: false});
36
+
37
+ JB721Tier memory beforeDefault = store.tierOf({hook: address(this), id: 1, includeResolvedUri: false});
38
+ assertEq(beforeDefault.remainingSupply, 0);
39
+ assertEq(beforeDefault.reserveFrequency, 0);
40
+ assertEq(store.numberOfPendingReservesFor({hook: address(this), tierId: 1}), 0);
41
+ assertEq(store.totalCashOutWeight(address(this)), 10);
42
+
43
+ JB721TierConfig[] memory laterTiers = new JB721TierConfig[](1);
44
+ laterTiers[0] = _tier({
45
+ price: 1,
46
+ initialSupply: 10,
47
+ reserveFrequency: 2,
48
+ reserveBeneficiary: address(0xBEEF),
49
+ useReserveBeneficiaryAsDefault: true,
50
+ category: 2
51
+ });
52
+ store.recordAddTiers(laterTiers);
53
+
54
+ JB721Tier memory afterDefault = store.tierOf({hook: address(this), id: 1, includeResolvedUri: false});
55
+ assertEq(afterDefault.remainingSupply, 0);
56
+ assertEq(afterDefault.reserveFrequency, 2);
57
+ assertEq(afterDefault.reserveBeneficiary, address(0xBEEF));
58
+ assertEq(store.numberOfPendingReservesFor({hook: address(this), tierId: 1}), 5);
59
+ assertEq(store.totalCashOutWeight(address(this)), 15);
60
+
61
+ vm.expectRevert();
62
+ store.recordMintReservesFor({tierId: 1, count: 1});
63
+ }
64
+
65
+ function _tier(
66
+ uint104 price,
67
+ uint32 initialSupply,
68
+ uint16 reserveFrequency,
69
+ address reserveBeneficiary,
70
+ bool useReserveBeneficiaryAsDefault,
71
+ uint24 category
72
+ )
73
+ internal
74
+ pure
75
+ returns (JB721TierConfig memory tier)
76
+ {
77
+ tier.price = price;
78
+ tier.initialSupply = initialSupply;
79
+ tier.reserveFrequency = reserveFrequency;
80
+ tier.reserveBeneficiary = reserveBeneficiary;
81
+ tier.category = category;
82
+ tier.flags = JB721TierConfigFlags({
83
+ allowOwnerMint: false,
84
+ useReserveBeneficiaryAsDefault: useReserveBeneficiaryAsDefault,
85
+ transfersPausable: false,
86
+ useVotingUnits: false,
87
+ cantBeRemoved: false,
88
+ cantIncreaseDiscountPercent: false,
89
+ cantBuyWithCredits: false
90
+ });
91
+ tier.splits = new JBSplit[](0);
92
+ }
93
+ }
@@ -0,0 +1,272 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
5
+ import {ForTest_JB721TiersHook} from "../utils/ForTest_JB721TiersHook.sol";
6
+ import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
7
+ import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
8
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
9
+ import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
10
+ import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
11
+ import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
12
+ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
13
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
14
+ import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
15
+ import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
16
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
17
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
18
+
19
+ contract CodexNemesisFreshAudit is UnitTestSetup {
20
+ function _buildPayMetadata(
21
+ address hookAddress,
22
+ bool allowOverspending,
23
+ uint16[] memory tierIdsToMint
24
+ )
25
+ internal
26
+ view
27
+ returns (bytes memory)
28
+ {
29
+ bytes[] memory data = new bytes[](1);
30
+ data[0] = abi.encode(allowOverspending, tierIdsToMint);
31
+
32
+ bytes4[] memory ids = new bytes4[](1);
33
+ ids[0] = metadataHelper.getId("pay", hookAddress);
34
+
35
+ return metadataHelper.createMetadata(ids, data);
36
+ }
37
+
38
+ function _nativeAmount(uint256 value) internal pure returns (JBTokenAmount memory) {
39
+ return JBTokenAmount({
40
+ token: JBConstants.NATIVE_TOKEN,
41
+ value: value,
42
+ decimals: 18,
43
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
44
+ });
45
+ }
46
+
47
+ function test_payCredits_can_underfund_split_bearing_tier_mints() public {
48
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
49
+ IJB721TiersHookStore hookStore = testHook.STORE();
50
+ address splitReceiver = makeAddr("splitReceiver");
51
+
52
+ JB721TierConfig[] memory tiersToAdd = new JB721TierConfig[](1);
53
+ tiersToAdd[0] = JB721TierConfig({
54
+ price: uint104(1 ether),
55
+ initialSupply: uint32(10),
56
+ votingUnits: 0,
57
+ reserveFrequency: 0,
58
+ reserveBeneficiary: address(0),
59
+ encodedIPFSUri: bytes32(uint256(1)),
60
+ category: uint24(1),
61
+ discountPercent: 0,
62
+ flags: JB721TierConfigFlags({
63
+ allowOwnerMint: false,
64
+ useReserveBeneficiaryAsDefault: false,
65
+ transfersPausable: false,
66
+ useVotingUnits: false,
67
+ cantBeRemoved: false,
68
+ cantIncreaseDiscountPercent: false,
69
+ cantBuyWithCredits: false
70
+ }),
71
+ splitPercent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
72
+ splits: new JBSplit[](0)
73
+ });
74
+
75
+ vm.prank(owner);
76
+ testHook.adjustTiers(tiersToAdd, new uint256[](0));
77
+
78
+ JBSplit[] memory splits = new JBSplit[](1);
79
+ splits[0] = JBSplit({
80
+ percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
81
+ projectId: 0,
82
+ beneficiary: payable(splitReceiver),
83
+ preferAddToBalance: false,
84
+ lockedUntil: 0,
85
+ hook: IJBSplitHook(address(0))
86
+ });
87
+
88
+ uint256 groupId = uint256(uint160(address(testHook))) | (uint256(1) << 160);
89
+ vm.mockCall(
90
+ mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
91
+ );
92
+ vm.mockCall(
93
+ mockJBDirectory,
94
+ abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
95
+ abi.encode(true)
96
+ );
97
+
98
+ JBAfterPayRecordedContext memory seedCredits = JBAfterPayRecordedContext({
99
+ payer: beneficiary,
100
+ projectId: projectId,
101
+ rulesetId: 0,
102
+ amount: _nativeAmount(1 ether),
103
+ forwardedAmount: _nativeAmount(0),
104
+ weight: 10e18,
105
+ newlyIssuedTokenCount: 0,
106
+ beneficiary: beneficiary,
107
+ hookMetadata: bytes(""),
108
+ payerMetadata: bytes("")
109
+ });
110
+
111
+ vm.prank(mockTerminalAddress);
112
+ testHook.afterPayRecordedWith(seedCredits);
113
+ assertEq(testHook.payCreditsOf(beneficiary), 1 ether, "setup: credits should be seeded");
114
+
115
+ uint16[] memory tierIds = new uint16[](1);
116
+ tierIds[0] = 1;
117
+ bytes memory payerMetadata = _buildPayMetadata(address(testHook), true, tierIds);
118
+
119
+ JBBeforePayRecordedContext memory beforeContext = JBBeforePayRecordedContext({
120
+ terminal: mockTerminalAddress,
121
+ payer: beneficiary,
122
+ amount: _nativeAmount(1),
123
+ projectId: projectId,
124
+ rulesetId: 0,
125
+ beneficiary: beneficiary,
126
+ weight: 10e18,
127
+ reservedPercent: 0,
128
+ metadata: payerMetadata
129
+ });
130
+
131
+ (uint256 weight, JBPayHookSpecification[] memory hookSpecifications) =
132
+ testHook.beforePayRecordedWith(beforeContext);
133
+
134
+ assertEq(weight, 0, "all fresh payment value is treated as split-routed");
135
+ assertEq(hookSpecifications.length, 1, "expected single pay hook spec");
136
+ assertEq(hookSpecifications[0].amount, 1, "split forwarding is capped to the fresh payment only");
137
+
138
+ JBAfterPayRecordedContext memory mintWithCredits = JBAfterPayRecordedContext({
139
+ payer: beneficiary,
140
+ projectId: projectId,
141
+ rulesetId: 0,
142
+ amount: _nativeAmount(1),
143
+ forwardedAmount: _nativeAmount(hookSpecifications[0].amount),
144
+ weight: weight,
145
+ newlyIssuedTokenCount: 0,
146
+ beneficiary: beneficiary,
147
+ hookMetadata: hookSpecifications[0].metadata,
148
+ payerMetadata: payerMetadata
149
+ });
150
+
151
+ vm.deal(mockTerminalAddress, 1);
152
+ vm.prank(mockTerminalAddress);
153
+ testHook.afterPayRecordedWith{value: 1}(mintWithCredits);
154
+
155
+ assertEq(testHook.balanceOf(beneficiary), 1, "beneficiary still mints the split-bearing NFT");
156
+ assertEq(testHook.payCreditsOf(beneficiary), 1, "stored credits fund essentially the entire mint");
157
+ assertEq(splitReceiver.balance, 1, "split receiver only receives the fresh 1 wei payment");
158
+ assertEq(hookStore.totalCashOutWeight(address(testHook)), 1 ether, "full-price NFT still enters cash-out math");
159
+ }
160
+
161
+ function test_new_default_reserve_beneficiary_retroactively_dilutes_existing_tiers() public {
162
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
163
+ IJB721TiersHookStore hookStore = testHook.STORE();
164
+ address retroReserveBeneficiary = makeAddr("retroReserveBeneficiary");
165
+
166
+ vm.mockCall(
167
+ mockJBDirectory,
168
+ abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
169
+ abi.encode(true)
170
+ );
171
+
172
+ JB721TierConfig[] memory initialTier = new JB721TierConfig[](1);
173
+ initialTier[0] = JB721TierConfig({
174
+ price: uint104(1 ether),
175
+ initialSupply: uint32(5),
176
+ votingUnits: 0,
177
+ reserveFrequency: uint16(2),
178
+ reserveBeneficiary: address(0),
179
+ encodedIPFSUri: bytes32(uint256(2)),
180
+ category: uint24(1),
181
+ discountPercent: 0,
182
+ flags: JB721TierConfigFlags({
183
+ allowOwnerMint: false,
184
+ useReserveBeneficiaryAsDefault: false,
185
+ transfersPausable: false,
186
+ useVotingUnits: false,
187
+ cantBeRemoved: false,
188
+ cantIncreaseDiscountPercent: false,
189
+ cantBuyWithCredits: false
190
+ }),
191
+ splitPercent: 0,
192
+ splits: new JBSplit[](0)
193
+ });
194
+
195
+ vm.prank(owner);
196
+ testHook.adjustTiers(initialTier, new uint256[](0));
197
+
198
+ uint16[] memory tierIds = new uint16[](2);
199
+ tierIds[0] = 1;
200
+ tierIds[1] = 1;
201
+
202
+ JBAfterPayRecordedContext memory initialMint = JBAfterPayRecordedContext({
203
+ payer: beneficiary,
204
+ projectId: projectId,
205
+ rulesetId: 0,
206
+ amount: _nativeAmount(2 ether),
207
+ forwardedAmount: _nativeAmount(0),
208
+ weight: 10e18,
209
+ newlyIssuedTokenCount: 0,
210
+ beneficiary: beneficiary,
211
+ hookMetadata: bytes(""),
212
+ payerMetadata: _buildPayMetadata(address(testHook), false, tierIds)
213
+ });
214
+
215
+ vm.prank(mockTerminalAddress);
216
+ testHook.afterPayRecordedWith(initialMint);
217
+
218
+ assertEq(
219
+ hookStore.numberOfPendingReservesFor(address(testHook), 1), 0, "no reserve obligation before default set"
220
+ );
221
+ assertEq(testHook.totalCashOutWeight(), 2 ether, "cash-out denominator initially matches sold NFTs");
222
+
223
+ JB721TierConfig[] memory activatingTier = new JB721TierConfig[](1);
224
+ activatingTier[0] = JB721TierConfig({
225
+ price: uint104(2 ether),
226
+ initialSupply: uint32(5),
227
+ votingUnits: 0,
228
+ reserveFrequency: uint16(1),
229
+ reserveBeneficiary: retroReserveBeneficiary,
230
+ encodedIPFSUri: bytes32(uint256(3)),
231
+ category: uint24(2),
232
+ discountPercent: 0,
233
+ flags: JB721TierConfigFlags({
234
+ allowOwnerMint: false,
235
+ useReserveBeneficiaryAsDefault: true,
236
+ transfersPausable: false,
237
+ useVotingUnits: false,
238
+ cantBeRemoved: false,
239
+ cantIncreaseDiscountPercent: false,
240
+ cantBuyWithCredits: false
241
+ }),
242
+ splitPercent: 0,
243
+ splits: new JBSplit[](0)
244
+ });
245
+
246
+ vm.prank(owner);
247
+ testHook.adjustTiers(activatingTier, new uint256[](0));
248
+
249
+ assertEq(
250
+ hookStore.defaultReserveBeneficiaryOf(address(testHook)),
251
+ retroReserveBeneficiary,
252
+ "new tier overwrites the hook-wide default reserve beneficiary"
253
+ );
254
+ assertEq(
255
+ hookStore.numberOfPendingReservesFor(address(testHook), 1),
256
+ 1,
257
+ "historic mints on the older tier now retroactively create reserve debt"
258
+ );
259
+ assertEq(
260
+ testHook.totalCashOutWeight(),
261
+ 3 ether,
262
+ "cash-out denominator dilutes existing holders before any new payment occurs"
263
+ );
264
+
265
+ testHook.mintPendingReservesFor(1, 1);
266
+
267
+ assertEq(
268
+ testHook.balanceOf(retroReserveBeneficiary), 1, "the new default beneficiary can mint retroactive reserves"
269
+ );
270
+ assertEq(hookStore.numberOfPendingReservesFor(address(testHook), 1), 0, "reserve debt is realized after mint");
271
+ }
272
+ }
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
5
+ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
6
+ import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
7
+ import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
8
+
9
+ contract CodexNemesisFutureTierPoC is UnitTestSetup {
10
+ function test_futureTierRemovalPersistsIntoNewTierAndBricksMint() external {
11
+ hook = _initHookDefaultTiers(0);
12
+
13
+ uint256[] memory futureTierIds = new uint256[](1);
14
+ futureTierIds[0] = 1;
15
+
16
+ // L-18 FIX: Removing a future (nonexistent) tier ID now reverts,
17
+ // preventing the "born removed" bug entirely.
18
+ vm.prank(owner);
19
+ vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_UnrecognizedTier.selector, 1));
20
+ hook.adjustTiers(new JB721TierConfig[](0), futureTierIds);
21
+ }
22
+
23
+ function test_futureTierUriCanBePoisonedBeforeTierExists() external {
24
+ hook = _initHookDefaultTiers(0);
25
+
26
+ bytes32 poisonedUri = bytes32(uint256(0x1234));
27
+
28
+ vm.prank(owner);
29
+ hook.setMetadata("", "", "", "", IJB721TokenUriResolver(address(hook)), 1, poisonedUri);
30
+
31
+ (JB721TierConfig[] memory tiersToAdd,) = _createTiers(defaultTierConfig, 1);
32
+ tiersToAdd[0].encodedIPFSUri = bytes32(0);
33
+
34
+ vm.prank(owner);
35
+ hook.adjustTiers(tiersToAdd, new uint256[](0));
36
+
37
+ assertEq(hook.STORE().encodedIPFSUriOf(address(hook), 1), poisonedUri, "future tier inherited stale uri");
38
+ }
39
+ }
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
6
+
7
+ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
+ import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
9
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
10
+
11
+ /// @notice L-18: Prevent future tier pre-removal.
12
+ /// Removing a tier ID that does not yet exist should revert with `UnrecognizedTier`.
13
+ contract Pass12L18 is Test {
14
+ JB721TiersHookStore internal store;
15
+
16
+ function setUp() external {
17
+ store = new JB721TiersHookStore();
18
+ }
19
+
20
+ /// @notice Removing a future tier ID (beyond maxTierIdOf) must revert.
21
+ function test_L18_fix_reverts_future_removal() external {
22
+ // Add 5 tiers so maxTierIdOf == 5.
23
+ JB721TierConfig[] memory tiers = new JB721TierConfig[](5);
24
+ for (uint256 i; i < 5; i++) {
25
+ tiers[i] = _tier(uint24(i + 1));
26
+ }
27
+ store.recordAddTiers(tiers);
28
+ assertEq(store.maxTierIdOf(address(this)), 5);
29
+
30
+ // Attempt to remove tier 10 (future) — should revert.
31
+ uint256[] memory futureTierIds = new uint256[](1);
32
+ futureTierIds[0] = 10;
33
+ vm.expectRevert(abi.encodeWithSignature("JB721TiersHookStore_UnrecognizedTier(uint256)", 10));
34
+ store.recordRemoveTierIds(futureTierIds);
35
+
36
+ // Attempt to remove tier 0 (invalid) — should also revert.
37
+ uint256[] memory zeroTierIds = new uint256[](1);
38
+ zeroTierIds[0] = 0;
39
+ vm.expectRevert(abi.encodeWithSignature("JB721TiersHookStore_UnrecognizedTier(uint256)", 0));
40
+ store.recordRemoveTierIds(zeroTierIds);
41
+ }
42
+
43
+ /// @notice Removing an existing tier still works as before.
44
+ function test_L18_existing_tier_removal_works() external {
45
+ // Add 5 tiers.
46
+ JB721TierConfig[] memory tiers = new JB721TierConfig[](5);
47
+ for (uint256 i; i < 5; i++) {
48
+ tiers[i] = _tier(uint24(i + 1));
49
+ }
50
+ store.recordAddTiers(tiers);
51
+
52
+ // Remove tier 3 — should succeed.
53
+ uint256[] memory removeTierIds = new uint256[](1);
54
+ removeTierIds[0] = 3;
55
+ store.recordRemoveTierIds(removeTierIds);
56
+ assertTrue(store.isTierRemoved({hook: address(this), tierId: 3}));
57
+
58
+ // Other tiers should not be affected.
59
+ assertFalse(store.isTierRemoved({hook: address(this), tierId: 1}));
60
+ assertFalse(store.isTierRemoved({hook: address(this), tierId: 2}));
61
+ assertFalse(store.isTierRemoved({hook: address(this), tierId: 4}));
62
+ assertFalse(store.isTierRemoved({hook: address(this), tierId: 5}));
63
+ }
64
+
65
+ function _tier(uint24 category) internal pure returns (JB721TierConfig memory tier) {
66
+ tier.price = 1;
67
+ tier.initialSupply = 10;
68
+ tier.category = category;
69
+ tier.flags = JB721TierConfigFlags({
70
+ allowOwnerMint: false,
71
+ useReserveBeneficiaryAsDefault: false,
72
+ transfersPausable: false,
73
+ useVotingUnits: false,
74
+ cantBeRemoved: false,
75
+ cantIncreaseDiscountPercent: false,
76
+ cantBuyWithCredits: false
77
+ });
78
+ tier.splits = new JBSplit[](0);
79
+ }
80
+ }
@@ -160,7 +160,7 @@ contract ERC20CashOutFork is Test {
160
160
  receive() external payable {}
161
161
 
162
162
  function setUp() public {
163
- vm.createSelectFork("ethereum");
163
+ vm.createSelectFork("ethereum", 24_971_900);
164
164
 
165
165
  _deployJBCore();
166
166
  _deploy721Hook();