@bananapus/721-hook-v6 0.0.40 → 0.0.42

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.40",
3
+ "version": "0.0.42",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,7 +7,6 @@ import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
7
7
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
8
8
  import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
9
9
  import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
10
- import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
11
10
  import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
12
11
  import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
13
12
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
@@ -24,7 +23,6 @@ import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.
24
23
  import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
25
24
  import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
26
25
  import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
27
- import {JB721Constants} from "./libraries/JB721Constants.sol";
28
26
  import {JB721TiersHookLib} from "./libraries/JB721TiersHookLib.sol";
29
27
  import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
30
28
  import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol";
@@ -206,19 +204,21 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
206
204
  uint256 totalSplitAmount;
207
205
  bytes memory splitMetadata;
208
206
  address beneficiary;
209
- (weight, totalSplitAmount, splitMetadata, beneficiary) = JB721TiersHookLib.computeSplitsAndWeight({
210
- store: STORE,
211
- metadataIdTarget: METADATA_ID_TARGET,
212
- packedPricingContext: _packedPricingContext,
213
- prices: PRICES,
214
- context: context
215
- });
207
+ uint256 splitCreditWeight;
208
+ (weight, totalSplitAmount, splitMetadata, beneficiary, splitCreditWeight) =
209
+ JB721TiersHookLib.computeSplitsAndWeight({
210
+ store: STORE,
211
+ metadataIdTarget: METADATA_ID_TARGET,
212
+ packedPricingContext: _packedPricingContext,
213
+ prices: PRICES,
214
+ context: context
215
+ });
216
216
 
217
217
  hookSpecifications[0] = JBPayHookSpecification({
218
218
  hook: this,
219
219
  noop: false,
220
220
  amount: totalSplitAmount,
221
- metadata: abi.encode(beneficiary, context.payer, splitMetadata)
221
+ metadata: abi.encode(beneficiary, context.payer, splitMetadata, splitCreditWeight)
222
222
  });
223
223
  }
224
224
 
@@ -36,6 +36,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
36
36
  error JB721TiersHookStore_InvalidQuantity(uint256 quantity, uint256 limit);
37
37
  error JB721TiersHookStore_ManualMintingNotAllowed(uint256 tierId);
38
38
  error JB721TiersHookStore_MaxTiersExceeded(uint256 numberOfTiers, uint256 limit);
39
+ error JB721TiersHookStore_MissingReserveBeneficiary(uint256 tierId);
39
40
  error JB721TiersHookStore_PriceExceedsAmount(uint256 price, uint256 leftoverAmount);
40
41
  error JB721TiersHookStore_ReserveFrequencyNotAllowed(uint256 tierId);
41
42
  error JB721TiersHookStore_SplitPercentExceedsBounds(uint256 percent, uint256 limit);
@@ -976,6 +977,15 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
976
977
  revert JB721TiersHookStore_DeadlockedReserve();
977
978
  }
978
979
 
980
+ // A tier with reserves must have a beneficiary — either tier-specific or a previously set default.
981
+ // Without one, minted reserves would be sent to address(0).
982
+ if (
983
+ tierToAdd.reserveFrequency > 0 && tierToAdd.reserveBeneficiary == address(0)
984
+ && defaultReserveBeneficiaryOf[msg.sender] == address(0)
985
+ ) {
986
+ revert JB721TiersHookStore_MissingReserveBeneficiary(tierId);
987
+ }
988
+
979
989
  // Store the tier with that ID.
980
990
  _storedTierOf[msg.sender][tierId] = JBStored721Tier({
981
991
  price: uint104(tierToAdd.price),
@@ -1219,7 +1229,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1219
1229
  if (storedTier.remainingSupply == 0) revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
1220
1230
 
1221
1231
  // Mint the 721 — decrement remaining supply first so the reserve check below
1222
- // sees the post-mint state (this non-reserve mint may increase pending reserves).
1232
+ // sees the correct post-mint non-reserve-mint count.
1223
1233
  unchecked {
1224
1234
  // Keep a reference to its token ID.
1225
1235
  tokenIds[i] = _generateTokenId({
@@ -7,7 +7,6 @@ import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
7
7
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
8
8
 
9
9
  import {IJB721Checkpoints} from "./IJB721Checkpoints.sol";
10
- import {IJB721CheckpointsDeployer} from "./IJB721CheckpointsDeployer.sol";
11
10
  import {IJB721Hook} from "./IJB721Hook.sol";
12
11
  import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
13
12
  import {IJB721TokenUriResolver} from "./IJB721TokenUriResolver.sol";
@@ -309,6 +309,8 @@ library JB721TiersHookLib {
309
309
  /// @return totalSplitAmount The total amount to forward for splits.
310
310
  /// @return splitMetadata Encoded per-tier breakdown (tierIds, amounts).
311
311
  /// @return beneficiary The resolved beneficiary address.
312
+ /// @return splitCreditWeight The weight attributable to tier splits when `issueTokensForSplits` is true.
313
+ /// Zero when splits are absent or `issueTokensForSplits` is false.
312
314
  function computeSplitsAndWeight(
313
315
  IJB721TiersHookStore store,
314
316
  address metadataIdTarget,
@@ -318,7 +320,13 @@ library JB721TiersHookLib {
318
320
  )
319
321
  external
320
322
  view
321
- returns (uint256 weight, uint256 totalSplitAmount, bytes memory splitMetadata, address beneficiary)
323
+ returns (
324
+ uint256 weight,
325
+ uint256 totalSplitAmount,
326
+ bytes memory splitMetadata,
327
+ address beneficiary,
328
+ uint256 splitCreditWeight
329
+ )
322
330
  {
323
331
  // Calculate per-tier split amounts.
324
332
  (totalSplitAmount, splitMetadata) = _calculateSplitAmounts({
@@ -346,6 +354,13 @@ library JB721TiersHookLib {
346
354
  hook: address(this)
347
355
  });
348
356
 
357
+ // When issueTokensForSplits is true and there are splits, compute the weight portion
358
+ // attributable to tier splits. Downstream compositors (e.g. JBOmnichainDeployer) use this
359
+ // to preserve split credit when an extra hook (buyback) returns weight=0.
360
+ if (totalSplitAmount != 0 && context.amount.value != 0 && store.flagsOf(address(this)).issueTokensForSplits) {
361
+ splitCreditWeight = mulDiv(context.weight, totalSplitAmount, context.amount.value);
362
+ }
363
+
349
364
  // Resolve the effective beneficiary from payment metadata.
350
365
  beneficiary = context.beneficiary;
351
366
  {
@@ -275,15 +275,15 @@ contract TestVotingUnitsLifecycle is UnitTestSetup {
275
275
  address(testHook),
276
276
  2,
277
277
  JBStored721Tier({
278
- price: uint104(20),
279
- remainingSupply: uint32(100),
280
- initialSupply: uint32(100),
281
- reserveFrequency: uint16(0),
282
- category: uint24(100),
283
- discountPercent: uint8(0),
284
- packedBools: testHook.test_store().ForTest_packBools(true, false, false, false, false, false),
285
- splitPercent: 0
286
- })
278
+ price: uint104(20),
279
+ remainingSupply: uint32(100),
280
+ initialSupply: uint32(100),
281
+ reserveFrequency: uint16(0),
282
+ category: uint24(100),
283
+ discountPercent: uint8(0),
284
+ packedBools: testHook.test_store().ForTest_packBools(true, false, false, false, false, false),
285
+ splitPercent: 0
286
+ })
287
287
  );
288
288
  // Clear tier 2's custom voting units (so it falls back to price).
289
289
  testHook.test_store().ForTest_setTierVotingUnits(address(testHook), 2, 0);
@@ -0,0 +1,66 @@
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 CodexNemesisReserveSellout is Test {
13
+ JB721TiersHookStore internal store;
14
+
15
+ function setUp() public {
16
+ store = new JB721TiersHookStore();
17
+ }
18
+
19
+ /// @notice Verifies that a paid mint cannot consume the last slot when it is reserved.
20
+ /// @dev Previously this test demonstrated the bug (paid mint succeeded). Now it confirms the fix.
21
+ function test_paidMintCannotConsumeReservedFinalSlot() public {
22
+ JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
23
+ tiers[0] = JB721TierConfig({
24
+ price: 1 ether,
25
+ initialSupply: 2,
26
+ votingUnits: 0,
27
+ reserveFrequency: 1,
28
+ reserveBeneficiary: address(0xBEEF),
29
+ encodedIPFSUri: bytes32(0),
30
+ category: 0,
31
+ discountPercent: 0,
32
+ flags: JB721TierConfigFlags({
33
+ allowOwnerMint: false,
34
+ useReserveBeneficiaryAsDefault: false,
35
+ transfersPausable: false,
36
+ useVotingUnits: false,
37
+ cantBeRemoved: false,
38
+ cantIncreaseDiscountPercent: false,
39
+ cantBuyWithCredits: false
40
+ }),
41
+ splitPercent: 0,
42
+ splits: new JBSplit[](0)
43
+ });
44
+
45
+ store.recordAddTiers(tiers);
46
+
47
+ uint16[] memory tierIds = new uint16[](1);
48
+ tierIds[0] = 1;
49
+
50
+ store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
51
+
52
+ JB721Tier memory tier = store.tierOf(address(this), 1, false);
53
+ assertEq(tier.remainingSupply, 1, "one paid mint leaves one slot");
54
+ assertEq(store.numberOfPendingReservesFor(address(this), 1), 1, "one reserve is pending");
55
+
56
+ // With the fix, the second paid mint reverts because the remaining slot is reserved.
57
+ vm.expectRevert(
58
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
59
+ );
60
+ store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
61
+
62
+ // Reserve beneficiary can still claim their entitled mint.
63
+ store.recordMintReservesFor({tierId: 1, count: 1});
64
+ assertEq(store.numberOfReservesMintedFor(address(this), 1), 1, "reserve beneficiary got their token");
65
+ }
66
+ }
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
4
4
  import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
5
5
  import {ForTest_JB721TiersHook} from "../utils/ForTest_JB721TiersHook.sol";
6
6
  import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
7
+ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
7
8
  import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
8
9
  import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
9
10
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
@@ -16,7 +17,7 @@ import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
16
17
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
17
18
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
18
19
 
19
- contract CodexNemesisFreshAudit is UnitTestSetup {
20
+ contract FreshAudit is UnitTestSetup {
20
21
  function _buildPayMetadata(
21
22
  address hookAddress,
22
23
  bool allowOverspending,
@@ -158,16 +159,10 @@ contract CodexNemesisFreshAudit is UnitTestSetup {
158
159
  assertEq(hookStore.totalCashOutWeight(address(testHook)), 1 ether, "full-price NFT still enters cash-out math");
159
160
  }
160
161
 
162
+ /// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
163
+ /// is now rejected at creation time, preventing the retroactive dilution bug.
161
164
  function test_new_default_reserve_beneficiary_retroactively_dilutes_existing_tiers() public {
162
165
  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
166
 
172
167
  JB721TierConfig[] memory initialTier = new JB721TierConfig[](1);
173
168
  initialTier[0] = JB721TierConfig({
@@ -192,81 +187,11 @@ contract CodexNemesisFreshAudit is UnitTestSetup {
192
187
  splits: new JBSplit[](0)
193
188
  });
194
189
 
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"
190
+ // The new creation-time check prevents tiers with reserves but no beneficiary.
191
+ vm.expectRevert(
192
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
220
193
  );
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
194
  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");
195
+ testHook.adjustTiers(initialTier, new uint256[](0));
271
196
  }
272
197
  }
@@ -6,7 +6,7 @@ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
6
6
  import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
7
7
  import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
8
8
 
9
- contract CodexNemesisFutureTierPoC is UnitTestSetup {
9
+ contract FutureTierPoC is UnitTestSetup {
10
10
  function test_futureTierRemovalPersistsIntoNewTierAndBricksMint() external {
11
11
  hook = _initHookDefaultTiers(0);
12
12
 
@@ -8,7 +8,7 @@ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
8
  import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
9
9
  import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
10
10
 
11
- contract Test_20260425CodexNemesisFutureTierRemoval is Test {
11
+ contract Test_FutureTierRemoval is Test {
12
12
  JB721TiersHookStore internal store;
13
13
 
14
14
  function setUp() external {
@@ -13,7 +13,7 @@ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
13
13
  import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
14
14
  import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
15
15
 
16
- contract CodexPayCreditsBypassTierSplits is UnitTestSetup {
16
+ contract PayCreditsBypassTierSplits is UnitTestSetup {
17
17
  address internal splitBeneficiary = makeAddr("splitBeneficiary");
18
18
 
19
19
  function setUp() public override {
@@ -12,7 +12,6 @@ import {JBPayDataHookRulesetMetadata} from "../../src/structs/JBPayDataHookRules
12
12
  import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
13
13
  import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
14
14
  import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
15
- import {IJB721TiersHook} from "../../src/interfaces/IJB721TiersHook.sol";
16
15
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
17
16
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
18
17
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
@@ -24,7 +23,7 @@ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingCo
24
23
  import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
25
24
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
26
25
 
27
- contract MockProjectsForNemesis {
26
+ contract MockProjects {
28
27
  uint256 internal _count;
29
28
  address internal _owner;
30
29
 
@@ -44,7 +43,7 @@ contract MockProjectsForNemesis {
44
43
  fallback() external {}
45
44
  }
46
45
 
47
- contract StrictControllerForNemesis {
46
+ contract StrictController {
48
47
  address internal immutable EXPECTED_CALLER;
49
48
 
50
49
  error UnexpectedCaller(address caller);
@@ -62,7 +61,7 @@ contract StrictControllerForNemesis {
62
61
  }
63
62
  }
64
63
 
65
- contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
64
+ contract Test_ProjectDeployerAuth is UnitTestSetup {
66
65
  JB721TiersHookProjectDeployer internal projectDeployer;
67
66
  address internal operator = address(0xBEEF);
68
67
  uint256 internal testProjectId = 5;
@@ -70,9 +69,9 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
70
69
  function setUp() public override {
71
70
  super.setUp();
72
71
 
73
- MockProjectsForNemesis projects = new MockProjectsForNemesis();
72
+ MockProjects projects = new MockProjects();
74
73
  vm.etch(mockJBProjects, address(projects).code);
75
- MockProjectsForNemesis(mockJBProjects).setup(testProjectId, owner);
74
+ MockProjects(mockJBProjects).setup(testProjectId, owner);
76
75
 
77
76
  vm.mockCall(mockJBDirectory, abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(mockJBProjects));
78
77
 
@@ -85,12 +84,10 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
85
84
  (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
86
85
  _launchConfig(testProjectId);
87
86
 
88
- StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
87
+ StrictController controller = new StrictController(owner);
89
88
 
90
89
  vm.prank(owner);
91
- vm.expectRevert(
92
- abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
93
- );
90
+ vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
94
91
  projectDeployer.launchRulesetsFor(
95
92
  testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
96
93
  );
@@ -100,12 +97,10 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
100
97
  (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
101
98
  _queueConfig(testProjectId);
102
99
 
103
- StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
100
+ StrictController controller = new StrictController(owner);
104
101
 
105
102
  vm.prank(owner);
106
- vm.expectRevert(
107
- abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
108
- );
103
+ vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
109
104
  projectDeployer.queueRulesetsOf(
110
105
  testProjectId, hookConfig, queueConfig, IJBController(address(controller)), bytes32(0)
111
106
  );
@@ -116,7 +111,7 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
116
111
  _launchConfig(testProjectId);
117
112
 
118
113
  address account = owner;
119
- StrictControllerForNemesis controller = new StrictControllerForNemesis(owner);
114
+ StrictController controller = new StrictController(owner);
120
115
 
121
116
  // Grant LAUNCH_RULESETS and SET_TERMINALS, deny QUEUE_RULESETS.
122
117
  vm.mockCall(
@@ -162,9 +157,7 @@ contract Test_CodexNemesisProjectDeployerAuth is UnitTestSetup {
162
157
  // The permission check passes with LAUNCH_RULESETS. The call proceeds to the controller, which reverts
163
158
  // because it sees the deployer contract as the caller rather than the original operator.
164
159
  vm.prank(operator);
165
- vm.expectRevert(
166
- abi.encodeWithSelector(StrictControllerForNemesis.UnexpectedCaller.selector, address(projectDeployer))
167
- );
160
+ vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
168
161
  projectDeployer.launchRulesetsFor(
169
162
  testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
170
163
  );
@@ -3,11 +3,11 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import "../utils/UnitTestSetup.sol";
5
5
 
6
- import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
7
6
  import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
7
+ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
8
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
9
9
 
10
- contract CodexNemesisRepoFindings is UnitTestSetup {
10
+ contract RepoFindings is UnitTestSetup {
11
11
  address payable internal splitBeneficiary = payable(makeAddr("splitBeneficiary"));
12
12
 
13
13
  function _payMetadata(
@@ -157,6 +157,8 @@ contract CodexNemesisRepoFindings is UnitTestSetup {
157
157
  );
158
158
  }
159
159
 
160
+ /// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
161
+ /// is now rejected at creation time, preventing the retroactive dilution bug.
160
162
  function test_new_default_reserve_beneficiary_retroactively_dilutes_existing_tiers() public {
161
163
  ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
162
164
 
@@ -183,88 +185,11 @@ contract CodexNemesisRepoFindings is UnitTestSetup {
183
185
  splits: new JBSplit[](0)
184
186
  });
185
187
 
186
- vm.prank(owner);
187
- testHook.adjustTiers(initialTier, new uint256[](0));
188
-
189
- mockAndExpect(
190
- mockJBDirectory,
191
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
192
- abi.encode(true)
193
- );
194
-
195
- uint16[] memory tierIdsToMint = new uint16[](3);
196
- tierIdsToMint[0] = 1;
197
- tierIdsToMint[1] = 1;
198
- tierIdsToMint[2] = 1;
199
- bytes memory payerMetadata = _payMetadata(address(testHook), false, tierIdsToMint);
200
-
201
- vm.prank(mockTerminalAddress);
202
- testHook.afterPayRecordedWith(
203
- JBAfterPayRecordedContext({
204
- payer: beneficiary,
205
- projectId: projectId,
206
- rulesetId: 0,
207
- amount: _nativeTokenAmount(3 ether),
208
- forwardedAmount: _nativeTokenAmount(0),
209
- weight: 10e18,
210
- newlyIssuedTokenCount: 0,
211
- beneficiary: beneficiary,
212
- hookMetadata: bytes(""),
213
- payerMetadata: payerMetadata
214
- })
215
- );
216
-
217
- assertEq(testHook.totalCashOutWeight(), 3 ether, "denominator initially reflects only sold NFTs");
218
- assertEq(
219
- testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
220
- 0,
221
- "without a reserve beneficiary the sold tier has no pending reserves"
188
+ // The new creation-time check prevents tiers with reserves but no beneficiary.
189
+ vm.expectRevert(
190
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
222
191
  );
223
-
224
- JB721TierConfig[] memory defaultingTier = new JB721TierConfig[](1);
225
- defaultingTier[0] = JB721TierConfig({
226
- price: 2 ether,
227
- initialSupply: 10,
228
- votingUnits: 0,
229
- reserveFrequency: 1,
230
- reserveBeneficiary: owner,
231
- encodedIPFSUri: bytes32(uint256(0x2222)),
232
- category: 2,
233
- discountPercent: 0,
234
- flags: JB721TierConfigFlags({
235
- allowOwnerMint: false,
236
- useReserveBeneficiaryAsDefault: true,
237
- transfersPausable: false,
238
- useVotingUnits: false,
239
- cantBeRemoved: false,
240
- cantIncreaseDiscountPercent: false,
241
- cantBuyWithCredits: false
242
- }),
243
- splitPercent: 0,
244
- splits: new JBSplit[](0)
245
- });
246
-
247
192
  vm.prank(owner);
248
- testHook.adjustTiers(defaultingTier, new uint256[](0));
249
-
250
- assertEq(
251
- testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
252
- owner,
253
- "the new default reserve beneficiary retroactively applies to the older sold tier"
254
- );
255
- assertEq(
256
- testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
257
- 2,
258
- "the older tier now reports newly created pending reserves from past sales"
259
- );
260
- assertEq(
261
- testHook.totalCashOutWeight(),
262
- 5 ether,
263
- "cash-out denominator is diluted by retroactively created reserves on the existing tier"
264
- );
265
-
266
- testHook.mintPendingReservesFor(1, 2);
267
-
268
- assertEq(testHook.balanceOf(owner), 2, "the owner can mint those retroactive reserve NFTs to themselves");
193
+ testHook.adjustTiers(initialTier, new uint256[](0));
269
194
  }
270
195
  }