@bananapus/721-hook-v6 0.0.40 → 0.0.41

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.41",
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);
@@ -724,6 +725,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
724
725
  || reserveBeneficiaryOf({hook: hook, tierId: tierId}) == address(0)
725
726
  ) return 0;
726
727
 
728
+ // A sold-out tier cannot have mintable pending reserves — minting would underflow remainingSupply.
729
+ if (storedTier.remainingSupply == 0) return 0;
730
+
727
731
  // The number of reserve NFTs which have already been minted from the tier.
728
732
  uint256 numberOfReserveMints = numberOfReservesMintedFor[hook][tierId];
729
733
 
@@ -976,6 +980,15 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
976
980
  revert JB721TiersHookStore_DeadlockedReserve();
977
981
  }
978
982
 
983
+ // A tier with reserves must have a beneficiary — either tier-specific or a previously set default.
984
+ // Without one, minted reserves would be sent to address(0).
985
+ if (
986
+ tierToAdd.reserveFrequency > 0 && tierToAdd.reserveBeneficiary == address(0)
987
+ && defaultReserveBeneficiaryOf[msg.sender] == address(0)
988
+ ) {
989
+ revert JB721TiersHookStore_MissingReserveBeneficiary(tierId);
990
+ }
991
+
979
992
  // Store the tier with that ID.
980
993
  _storedTierOf[msg.sender][tierId] = JBStored721Tier({
981
994
  price: uint104(tierToAdd.price),
@@ -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
  {
@@ -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
  }
@@ -9,14 +9,17 @@ import {JB721Tier} from "../../src/structs/JB721Tier.sol";
9
9
  import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
10
10
  import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
11
11
 
12
- contract Test_20260425CodexNemesisReserveActivation is Test {
12
+ contract Test_ReserveActivation is Test {
13
13
  JB721TiersHookStore internal store;
14
14
 
15
15
  function setUp() external {
16
16
  store = new JB721TiersHookStore();
17
17
  }
18
18
 
19
- function test_retroactiveDefaultReserveBeneficiaryCreatesUnmintablePendingReserves() external {
19
+ /// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
20
+ /// is now rejected at creation time. This prevents the phantom-reserves scenario entirely.
21
+ function test_soldOutTier_noPhantomReserves_afterDefaultBeneficiaryChange() external {
22
+ // Attempt to add a tier with reserve frequency but no beneficiary — should revert.
20
23
  JB721TierConfig[] memory initialTiers = new JB721TierConfig[](1);
21
24
  initialTiers[0] = _tier({
22
25
  price: 1,
@@ -26,40 +29,31 @@ contract Test_20260425CodexNemesisReserveActivation is Test {
26
29
  useReserveBeneficiaryAsDefault: false,
27
30
  category: 1
28
31
  });
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
32
 
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);
33
+ vm.expectRevert(
34
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
35
+ );
36
+ store.recordAddTiers(initialTiers);
37
+ }
42
38
 
43
- JB721TierConfig[] memory laterTiers = new JB721TierConfig[](1);
44
- laterTiers[0] = _tier({
39
+ /// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
40
+ /// is now rejected at creation time. This prevents the retroactive reserve activation scenario entirely.
41
+ function test_nonSoldOutTier_reservesStillWork_afterDefaultBeneficiaryChange() external {
42
+ // Attempt to add a tier with reserve frequency but no beneficiary — should revert.
43
+ JB721TierConfig[] memory initialTiers = new JB721TierConfig[](1);
44
+ initialTiers[0] = _tier({
45
45
  price: 1,
46
- initialSupply: 10,
47
- reserveFrequency: 2,
48
- reserveBeneficiary: address(0xBEEF),
49
- useReserveBeneficiaryAsDefault: true,
50
- category: 2
46
+ initialSupply: 100,
47
+ reserveFrequency: 5,
48
+ reserveBeneficiary: address(0),
49
+ useReserveBeneficiaryAsDefault: false,
50
+ category: 1
51
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
52
 
61
- vm.expectRevert();
62
- store.recordMintReservesFor({tierId: 1, count: 1});
53
+ vm.expectRevert(
54
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
55
+ );
56
+ store.recordAddTiers(initialTiers);
63
57
  }
64
58
 
65
59
  function _tier(
@@ -0,0 +1,149 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "../utils/UnitTestSetup.sol";
6
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
+ import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
8
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
9
+ import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
10
+
11
+ contract RetroactiveReserveBeneficiaryDilution is UnitTestSetup {
12
+ /// @notice Creating a tier with reserveFrequency > 0 but no beneficiary (tier-specific or default) now reverts,
13
+ /// preventing the phantom reserve scenario where a later default beneficiary retroactively inflates
14
+ /// totalCashOutWeight and dilutes existing holders.
15
+ function test_adjustTier_reverts_when_reserve_has_no_beneficiary() public {
16
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
17
+
18
+ JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
19
+ tier1[0] = JB721TierConfig({
20
+ price: 1 ether,
21
+ initialSupply: 100,
22
+ votingUnits: 0,
23
+ reserveFrequency: 2,
24
+ reserveBeneficiary: address(0),
25
+ encodedIPFSUri: bytes32(uint256(0x1234)),
26
+ category: 1,
27
+ discountPercent: 0,
28
+ flags: JB721TierConfigFlags({
29
+ allowOwnerMint: false,
30
+ useReserveBeneficiaryAsDefault: false,
31
+ transfersPausable: false,
32
+ useVotingUnits: false,
33
+ cantBeRemoved: false,
34
+ cantIncreaseDiscountPercent: false,
35
+ cantBuyWithCredits: false
36
+ }),
37
+ splitPercent: 0,
38
+ splits: new JBSplit[](0)
39
+ });
40
+
41
+ vm.prank(owner);
42
+ vm.expectRevert(
43
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
44
+ );
45
+ testHook.adjustTiers(tier1, new uint256[](0));
46
+ }
47
+
48
+ /// @notice A tier with reserveFrequency > 0 succeeds when an explicit beneficiary is provided.
49
+ function test_adjustTier_succeeds_with_explicit_reserve_beneficiary() public {
50
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
51
+
52
+ JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
53
+ tier1[0] = JB721TierConfig({
54
+ price: 1 ether,
55
+ initialSupply: 100,
56
+ votingUnits: 0,
57
+ reserveFrequency: 2,
58
+ reserveBeneficiary: owner,
59
+ encodedIPFSUri: bytes32(uint256(0x1234)),
60
+ category: 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: 0,
72
+ splits: new JBSplit[](0)
73
+ });
74
+
75
+ vm.prank(owner);
76
+ testHook.adjustTiers(tier1, new uint256[](0));
77
+
78
+ assertEq(
79
+ testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
80
+ owner,
81
+ "tier-specific beneficiary should be set"
82
+ );
83
+ }
84
+
85
+ /// @notice A tier with reserveFrequency > 0 and no explicit beneficiary succeeds when a default beneficiary
86
+ /// was previously set by an earlier tier.
87
+ function test_adjustTier_succeeds_with_default_reserve_beneficiary() public {
88
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
89
+
90
+ // Add two tiers: first sets the default beneficiary, second relies on it.
91
+ JB721TierConfig[] memory tiers = new JB721TierConfig[](2);
92
+
93
+ // Tier 1: sets default beneficiary via useReserveBeneficiaryAsDefault.
94
+ tiers[0] = JB721TierConfig({
95
+ price: 1 ether,
96
+ initialSupply: 100,
97
+ votingUnits: 0,
98
+ reserveFrequency: 2,
99
+ reserveBeneficiary: owner,
100
+ encodedIPFSUri: bytes32(uint256(0x1234)),
101
+ category: 1,
102
+ discountPercent: 0,
103
+ flags: JB721TierConfigFlags({
104
+ allowOwnerMint: false,
105
+ useReserveBeneficiaryAsDefault: true,
106
+ transfersPausable: false,
107
+ useVotingUnits: false,
108
+ cantBeRemoved: false,
109
+ cantIncreaseDiscountPercent: false,
110
+ cantBuyWithCredits: false
111
+ }),
112
+ splitPercent: 0,
113
+ splits: new JBSplit[](0)
114
+ });
115
+
116
+ // Tier 2: relies on the default beneficiary (no explicit one).
117
+ tiers[1] = JB721TierConfig({
118
+ price: 2 ether,
119
+ initialSupply: 100,
120
+ votingUnits: 0,
121
+ reserveFrequency: 3,
122
+ reserveBeneficiary: address(0),
123
+ encodedIPFSUri: bytes32(uint256(0x5678)),
124
+ category: 2,
125
+ discountPercent: 0,
126
+ flags: JB721TierConfigFlags({
127
+ allowOwnerMint: false,
128
+ useReserveBeneficiaryAsDefault: false,
129
+ transfersPausable: false,
130
+ useVotingUnits: false,
131
+ cantBeRemoved: false,
132
+ cantIncreaseDiscountPercent: false,
133
+ cantBuyWithCredits: false
134
+ }),
135
+ splitPercent: 0,
136
+ splits: new JBSplit[](0)
137
+ });
138
+
139
+ vm.prank(owner);
140
+ testHook.adjustTiers(tiers, new uint256[](0));
141
+
142
+ // Tier 2 should inherit the default beneficiary set by tier 1.
143
+ assertEq(
144
+ testHook.STORE().reserveBeneficiaryOf(address(testHook), 2),
145
+ owner,
146
+ "tier 2 should inherit default beneficiary"
147
+ );
148
+ }
149
+ }
@@ -15,7 +15,7 @@ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
15
15
  /// @notice Regression test: split metadata is proportionally scaled when credits fund a split-bearing tier mint.
16
16
  /// @dev Previously (pre-fix), the per-tier split amounts were left at the uncapped value, trapping forwarded ETH.
17
17
  /// After the F-2 fix, split amounts are scaled down to match the actual forwarded amount.
18
- contract CodexSplitCreditsMismatch is UnitTestSetup {
18
+ contract SplitCreditsMismatch is UnitTestSetup {
19
19
  address internal splitBeneficiary = makeAddr("splitBeneficiary");
20
20
 
21
21
  function setUp() public override {
@@ -360,55 +360,39 @@ contract Test_mintFor_mintReservesFor_Unit is UnitTestSetup {
360
360
  }
361
361
  }
362
362
 
363
+ /// @notice Tiers with reserveFrequency > 0 and no beneficiary are now rejected at creation time.
364
+ /// This test verifies that the creation-time check prevents such tiers from existing.
363
365
  function test_numberOfPendingReservesFor_noReservesIfNoBeneficiarySet() public {
364
- uint256 initialSupply = 200; // The number of NFTs available for each tier.
365
- uint256 totalMinted = 120; // The number of NFTs already minted for each tier (out of `initialSupply`).
366
- uint256 reservedMinted = 10; // The number of reserve NFTs already minted (out of `totalMinted`).
367
- uint256 reserveFrequency = 9; // The frequency at which NFTs are reserved.
368
- // (For every 9 NFTs minted, 1 is reserved).
369
-
370
- reserveBeneficiary = address(0);
371
- ForTest_JB721TiersHook hook = _initializeForTestHook(10);
372
-
373
- // Initialize `numberOfTiers` tiers, and set the number of reserve NFTs already minted for each tier.
374
- // Although the `reserveFrequency` is set, it should be ignored since there is no reserve beneficiary.
375
- for (uint256 i; i < 10; i++) {
376
- hook.test_store()
377
- .ForTest_setTier(
378
- address(hook),
379
- i + 1,
380
- JBStored721Tier({
381
- price: uint104((i + 1) * 10),
382
- // forge-lint: disable-next-line(unsafe-typecast)
383
- remainingSupply: uint32(initialSupply - totalMinted),
384
- // forge-lint: disable-next-line(unsafe-typecast)
385
- initialSupply: uint32(initialSupply),
386
- // forge-lint: disable-next-line(unsafe-typecast)
387
- reserveFrequency: uint16(reserveFrequency),
388
- category: uint24(100),
389
- discountPercent: uint8(0),
390
- packedBools: hook.test_store().ForTest_packBools(false, false, true, false, false, false),
391
- splitPercent: 0
392
- })
393
- );
394
- hook.test_store().ForTest_setReservesMintedFor(address(hook), i + 1, reservedMinted);
395
- }
396
-
397
- // Fetch the stored tiers.
398
- JB721Tier[] memory storedTiers = hook.test_store().tiersOf(address(hook), new uint256[](0), false, 0, 10);
399
-
400
- // Check: did the reserve frequency default to 0 for all tiers?
401
- for (uint256 i; i < 10; i++) {
402
- assertEq(storedTiers[i].reserveFrequency, 0, "Reserve frequency should be zero (no beneficiary set).");
403
- }
404
- // Check: are we sure there are no pending reserves for all tiers?
405
- for (uint256 i; i < 10; i++) {
406
- assertEq(
407
- hook.test_store().numberOfPendingReservesFor(address(hook), i + 1),
408
- 0,
409
- "There should not be any pending reserves (no beneficiary set)."
410
- );
411
- }
366
+ // Build tier configs with reserveFrequency > 0 but reserveBeneficiary == address(0).
367
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
368
+ tierConfigs[0] = JB721TierConfig({
369
+ price: uint104(10),
370
+ initialSupply: uint32(200),
371
+ votingUnits: 0,
372
+ reserveFrequency: uint16(9),
373
+ reserveBeneficiary: address(0),
374
+ encodedIPFSUri: bytes32(uint256(1)),
375
+ category: uint24(100),
376
+ discountPercent: 0,
377
+ flags: JB721TierConfigFlags({
378
+ allowOwnerMint: false,
379
+ useReserveBeneficiaryAsDefault: false,
380
+ transfersPausable: false,
381
+ useVotingUnits: true,
382
+ cantBeRemoved: false,
383
+ cantIncreaseDiscountPercent: false,
384
+ cantBuyWithCredits: false
385
+ }),
386
+ splitPercent: 0,
387
+ splits: new JBSplit[](0)
388
+ });
389
+
390
+ // The creation-time check should reject a tier with reserves but no beneficiary.
391
+ ForTest_JB721TiersHookStore hookStore = new ForTest_JB721TiersHookStore();
392
+ vm.expectRevert(
393
+ abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
394
+ );
395
+ hookStore.recordAddTiers(tierConfigs);
412
396
  }
413
397
 
414
398
  function test_mintFor_mintArrayOfTiers() public {
@@ -1,161 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "../utils/UnitTestSetup.sol";
6
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
- import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
8
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
9
-
10
- contract CodexRetroactiveReserveBeneficiaryDilution is UnitTestSetup {
11
- function _buildPayMetadata(address hookAddress, uint16[] memory tierIdsToMint)
12
- internal
13
- view
14
- returns (bytes memory)
15
- {
16
- bytes[] memory data = new bytes[](1);
17
- data[0] = abi.encode(true, tierIdsToMint);
18
- bytes4[] memory ids = new bytes4[](1);
19
- ids[0] = metadataHelper.getId("pay", hookAddress);
20
- return metadataHelper.createMetadata(ids, data);
21
- }
22
-
23
- function _afterPayContext(
24
- address hookAddress,
25
- uint256 amountValue,
26
- uint16[] memory tierIdsToMint
27
- )
28
- internal
29
- view
30
- returns (JBAfterPayRecordedContext memory)
31
- {
32
- return JBAfterPayRecordedContext({
33
- payer: beneficiary,
34
- projectId: projectId,
35
- rulesetId: 0,
36
- amount: JBTokenAmount({
37
- token: JBConstants.NATIVE_TOKEN,
38
- value: amountValue,
39
- decimals: 18,
40
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
41
- }),
42
- forwardedAmount: JBTokenAmount({
43
- token: JBConstants.NATIVE_TOKEN,
44
- value: 0,
45
- decimals: 18,
46
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
47
- }),
48
- weight: 10e18,
49
- newlyIssuedTokenCount: 0,
50
- beneficiary: beneficiary,
51
- hookMetadata: "",
52
- payerMetadata: _buildPayMetadata(hookAddress, tierIdsToMint)
53
- });
54
- }
55
-
56
- function test_adjustTier_can_retroactively_create_reserves_for_existing_supply() public {
57
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
58
-
59
- JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
60
- tier1[0] = JB721TierConfig({
61
- price: 1 ether,
62
- initialSupply: 100,
63
- votingUnits: 0,
64
- reserveFrequency: 2,
65
- reserveBeneficiary: address(0),
66
- encodedIPFSUri: bytes32(uint256(0x1234)),
67
- category: 1,
68
- discountPercent: 0,
69
- 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
- splitPercent: 0,
79
- splits: new JBSplit[](0)
80
- });
81
-
82
- vm.prank(owner);
83
- testHook.adjustTiers(tier1, new uint256[](0));
84
-
85
- assertEq(
86
- testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
87
- 0,
88
- "no beneficiary means no reserves are pending"
89
- );
90
-
91
- vm.mockCall(
92
- mockJBDirectory,
93
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
94
- abi.encode(true)
95
- );
96
-
97
- uint16[] memory mintIds = new uint16[](3);
98
- mintIds[0] = 1;
99
- mintIds[1] = 1;
100
- mintIds[2] = 1;
101
-
102
- JBAfterPayRecordedContext memory payContext = _afterPayContext(address(testHook), 3 ether, mintIds);
103
- vm.prank(mockTerminalAddress);
104
- testHook.afterPayRecordedWith(payContext);
105
-
106
- assertEq(testHook.balanceOf(beneficiary), 3, "beneficiary should own the three paid NFTs");
107
- assertEq(testHook.totalCashOutWeight(), 3 ether, "denominator initially reflects only sold NFTs");
108
-
109
- JB721TierConfig[] memory tier2 = new JB721TierConfig[](1);
110
- tier2[0] = JB721TierConfig({
111
- price: 2 ether,
112
- initialSupply: 100,
113
- votingUnits: 0,
114
- reserveFrequency: 1,
115
- reserveBeneficiary: owner,
116
- encodedIPFSUri: bytes32(uint256(0x5678)),
117
- category: 2,
118
- discountPercent: 0,
119
- flags: JB721TierConfigFlags({
120
- allowOwnerMint: false,
121
- useReserveBeneficiaryAsDefault: true,
122
- transfersPausable: false,
123
- useVotingUnits: false,
124
- cantBeRemoved: false,
125
- cantIncreaseDiscountPercent: false,
126
- cantBuyWithCredits: false
127
- }),
128
- splitPercent: 0,
129
- splits: new JBSplit[](0)
130
- });
131
-
132
- vm.prank(owner);
133
- testHook.adjustTiers(tier2, new uint256[](0));
134
-
135
- assertEq(
136
- testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
137
- owner,
138
- "new default beneficiary retroactively applies to the old tier"
139
- );
140
- assertEq(
141
- testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
142
- 2,
143
- "historical sales now generate retroactive pending reserves"
144
- );
145
- assertEq(
146
- testHook.totalCashOutWeight(),
147
- 5 ether,
148
- "existing holders are diluted before any new payment enters the system"
149
- );
150
-
151
- vm.prank(address(0xBEEF));
152
- testHook.mintPendingReservesFor(1, 2);
153
-
154
- assertEq(testHook.balanceOf(owner), 2, "retroactive reserves mint directly to the new default beneficiary");
155
- assertEq(
156
- testHook.STORE().numberOfPendingReservesFor(address(testHook), 1),
157
- 0,
158
- "all retroactive reserve entitlement can be extracted"
159
- );
160
- }
161
- }