@bananapus/721-hook-v6 0.0.42 → 0.0.45

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.
Files changed (86) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +20 -9
  4. package/script/Deploy.s.sol +2 -2
  5. package/src/JB721Checkpoints.sol +61 -19
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +66 -53
  8. package/src/JB721TiersHookDeployer.sol +8 -5
  9. package/src/JB721TiersHookProjectDeployer.sol +87 -46
  10. package/src/JB721TiersHookStore.sol +137 -107
  11. package/src/abstract/JB721Hook.sol +8 -6
  12. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  13. package/src/interfaces/IJB721CheckpointsDeployer.sol +7 -3
  14. package/src/interfaces/IJB721TiersHook.sol +3 -3
  15. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +4 -2
  16. package/src/interfaces/IJB721TiersHookStore.sol +11 -11
  17. package/src/libraries/JB721TiersHookLib.sol +1 -1
  18. package/src/structs/JB721TiersHookFlags.sol +1 -1
  19. package/src/structs/JBPayDataHookRulesetMetadata.sol +1 -1
  20. package/test/utils/AccessJBLib.sol +49 -0
  21. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  22. package/test/utils/TestBaseWorkflow.sol +213 -0
  23. package/test/utils/UnitTestSetup.sol +805 -0
  24. package/.gas-snapshot +0 -152
  25. package/ADMINISTRATION.md +0 -87
  26. package/ARCHITECTURE.md +0 -98
  27. package/AUDIT_INSTRUCTIONS.md +0 -77
  28. package/RISKS.md +0 -118
  29. package/SKILLS.md +0 -43
  30. package/STYLE_GUIDE.md +0 -610
  31. package/USER_JOURNEYS.md +0 -121
  32. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  33. package/slither-ci.config.json +0 -10
  34. package/test/721HookAttacks.t.sol +0 -408
  35. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  36. package/test/Fork.t.sol +0 -2346
  37. package/test/TestAuditGaps.sol +0 -1075
  38. package/test/TestCheckpoints.t.sol +0 -341
  39. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  40. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  41. package/test/audit/AuditRegressions.t.sol +0 -83
  42. package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
  43. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  44. package/test/audit/FreshAudit.t.sol +0 -197
  45. package/test/audit/FutureTierPoC.t.sol +0 -39
  46. package/test/audit/FutureTierRemoval.t.sol +0 -47
  47. package/test/audit/Pass12L18.t.sol +0 -80
  48. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  49. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  50. package/test/audit/RepoFindings.t.sol +0 -195
  51. package/test/audit/ReserveActivation.t.sol +0 -87
  52. package/test/audit/ReserveSlotProtection.t.sol +0 -273
  53. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  54. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  55. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  56. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  57. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  58. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  59. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  60. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  61. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  62. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  63. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  64. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  65. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  66. package/test/regression/CacheTierLookup.t.sol +0 -190
  67. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  68. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  69. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  70. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  71. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  72. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  73. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  74. package/test/unit/JBBitmap.t.sol +0 -170
  75. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  76. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  77. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  78. package/test/unit/deployer_Unit.t.sol +0 -114
  79. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  80. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  81. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  82. package/test/unit/pay_Unit.t.sol +0 -1661
  83. package/test/unit/redeem_Unit.t.sol +0 -473
  84. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  85. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  86. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
@@ -16,7 +16,11 @@ import {JBBitmapWord} from "./structs/JBBitmapWord.sol";
16
16
  import {JBStored721Tier} from "./structs/JBStored721Tier.sol";
17
17
 
18
18
  /// @title JB721TiersHookStore
19
- /// @notice This contract stores and manages data for many `IJB721TiersHook`s and their NFTs.
19
+ /// @notice The shared data store for all `JB721TiersHook` instances. Stores tier definitions, mint counts, reserve
20
+ /// tracking, voting units, and removal bitmaps. Each hook registers its own tiers here; the store handles
21
+ /// tier validation, minting logic (including reserve accounting), and cash-out weight calculations.
22
+ /// @dev One store is shared across all hooks. Functions are keyed by hook address so each hook reads/writes
23
+ /// only its own data. Tier IDs are sequential per hook and 1-indexed.
20
24
  contract JB721TiersHookStore is IJB721TiersHookStore {
21
25
  using JBBitmap for mapping(uint256 => uint256);
22
26
  using JBBitmap for JBBitmapWord;
@@ -155,48 +159,50 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
155
159
  // ------------------------- external views -------------------------- //
156
160
  //*********************************************************************//
157
161
 
158
- /// @notice Resolves the encoded IPFS URI for the tier of the 721 with the provided token ID from the provided 721
159
- /// contract.
160
- /// @param hook The 721 contract that the encoded IPFS URI belongs to.
161
- /// @param tokenId The token ID of the 721 to get the encoded tier IPFS URI of.
162
- /// @return The encoded IPFS URI.
162
+ /// @notice Get the encoded IPFS URI for the tier that a specific NFT belongs to. Used to resolve the NFT's
163
+ /// metadata when no custom `tokenUriResolver` is set.
164
+ /// @param hook The 721 hook contract the NFT belongs to.
165
+ /// @param tokenId The token ID of the NFT.
166
+ /// @return The encoded IPFS URI for the NFT's tier.
163
167
  // forge-lint: disable-next-line(mixed-case-function)
164
168
  function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) {
165
169
  return encodedIPFSUriOf[hook][tierIdOfToken(tokenId)];
166
170
  }
167
171
 
168
- /// @notice Get the flags that dictate the behavior of the provided 721 contract.
169
- /// @param hook The 721 contract to get the flags of.
170
- /// @return The flags.
172
+ /// @notice Get the behavioral flags for a hook such as whether transfers are pausable, whether NFT holders can
173
+ /// cash out, and whether token issuance occurs for split-routed payments.
174
+ /// @param hook The 721 hook contract to get the flags of.
175
+ /// @return The hook's flags.
171
176
  function flagsOf(address hook) external view override returns (JB721TiersHookFlags memory) {
172
177
  return _flagsOf[hook];
173
178
  }
174
179
 
175
- /// @notice Check if the provided tier has been removed from the provided 721 contract.
176
- /// @param hook The 721 contract the tier belongs to.
177
- /// @param tierId The ID of the tier to check the removal status of.
178
- /// @return A bool which is `true` if the tier has been removed, and `false` otherwise.
180
+ /// @notice Check whether a tier has been removed. Removed tiers can no longer be minted from, but existing NFTs
181
+ /// from that tier remain valid and can still be cashed out.
182
+ /// @param hook The 721 hook contract the tier belongs to.
183
+ /// @param tierId The ID of the tier to check.
184
+ /// @return `true` if the tier has been removed, `false` otherwise.
179
185
  function isTierRemoved(address hook, uint256 tierId) external view override returns (bool) {
180
186
  JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[hook].readId(tierId);
181
187
 
182
188
  return bitmapWord.isTierIdRemoved(tierId);
183
189
  }
184
190
 
185
- /// @notice Get the number of pending reserve NFTs for the provided tier ID of the provided 721 contract.
186
- /// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
187
- /// @param hook The 721 contract to check for pending reserved NFTs.
188
- /// @param tierId The ID of the tier to get the number of pending reserves for.
189
- /// @return The number of pending reserved NFTs.
191
+ /// @notice How many reserved NFTs are waiting to be minted for a tier. Reserves accumulate automatically as
192
+ /// non-reserve NFTs are minted (based on the tier's `reserveFrequency`). Anyone can mint them via
193
+ /// `mintPendingReservesFor`.
194
+ /// @param hook The 721 hook contract to check.
195
+ /// @param tierId The ID of the tier to check.
196
+ /// @return The number of pending reserved NFTs that can be minted.
190
197
  function numberOfPendingReservesFor(address hook, uint256 tierId) external view override returns (uint256) {
191
198
  return _numberOfPendingReservesFor({hook: hook, tierId: tierId, storedTier: _storedTierOf[hook][tierId]});
192
199
  }
193
200
 
194
- /// @notice Get the tier of the 721 with the provided token ID in the provided 721 contract.
195
- /// @param hook The 721 contract that the tier belongs to.
196
- /// @param tokenId The token ID of the 721 to get the tier of.
197
- /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
198
- /// resolved and included.
199
- /// @return The tier.
201
+ /// @notice Look up which tier an NFT belongs to and return the full tier details (price, supply, metadata, etc.).
202
+ /// @param hook The 721 hook contract the NFT belongs to.
203
+ /// @param tokenId The token ID of the NFT.
204
+ /// @param includeResolvedUri If `true` and the hook has a `tokenUriResolver`, resolves the URI and includes it.
205
+ /// @return The tier that the NFT belongs to.
200
206
  function tierOfTokenId(
201
207
  address hook,
202
208
  uint256 tokenId,
@@ -255,14 +261,14 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
255
261
  transfersPausable = (storedTier.packedBools & 0x2) != 0;
256
262
  }
257
263
 
258
- /// @notice Gets an array of currently active 721 tiers for the provided 721 contract.
259
- /// @param hook The 721 contract to get the tiers of.
260
- /// @param categories An array tier categories to get tiers from. Send an empty array to get all categories.
261
- /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
262
- /// resolved and included.
263
- /// @param startingId The ID of the first tier to get (sorted by category). Send 0 to get all active tiers.
264
- /// @param size The number of tiers to include.
265
- /// @return tiers An array of active 721 tiers.
264
+ /// @notice Get all active (non-removed) tiers for a hook, with optional filtering by category and pagination.
265
+ /// Tiers are returned sorted by category.
266
+ /// @param hook The 721 hook contract to get tiers from.
267
+ /// @param categories Filter to specific categories. Pass an empty array to include all categories.
268
+ /// @param includeResolvedUri If `true` and the hook has a `tokenUriResolver`, resolves URIs and includes them.
269
+ /// @param startingId Start from this tier ID (for pagination). Pass 0 to start from the beginning.
270
+ /// @param size The maximum number of tiers to return.
271
+ /// @return tiers An array of active tiers.
266
272
  function tiersOf(
267
273
  address hook,
268
274
  uint256[] calldata categories,
@@ -345,14 +351,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
345
351
  }
346
352
  }
347
353
 
348
- /// @notice Returns the number of voting units an addresses has within the specified tier of the specified 721
349
- /// contract.
350
- /// @dev NFTs have a tier-specific number of voting units. If the tier does not have a custom number of voting
351
- /// units, the price is used.
352
- /// @param hook The 721 contract that the tier belongs to.
353
- /// @param account The address to get the voting units of within the tier.
354
- /// @param tierId The ID of the tier to get voting units within.
355
- /// @return The address' voting units within the tier.
354
+ /// @notice Get an address's voting power from a specific tier. Each NFT in the tier contributes either the tier's
355
+ /// custom `votingUnits` (if configured) or the tier's price. Multiply by the holder's balance in the tier.
356
+ /// @param hook The 721 hook contract that the tier belongs to.
357
+ /// @param account The address to get voting units for.
358
+ /// @param tierId The ID of the tier.
359
+ /// @return The address's total voting units within the tier.
356
360
  function tierVotingUnitsOf(
357
361
  address hook,
358
362
  address account,
@@ -379,9 +383,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
379
383
  return balance * (useVotingUnits ? _tierVotingUnitsOf[hook][tierId] : storedTier.price);
380
384
  }
381
385
 
382
- /// @notice Get the number of NFTs which have been minted from the provided 721 contract (across all tiers).
383
- /// @param hook The 721 contract to get a total supply of.
384
- /// @return supply The total number of NFTs minted from all tiers on the contract.
386
+ /// @notice The total number of NFTs currently in circulation for a hook (minted minus burned, across all tiers).
387
+ /// @param hook The 721 hook contract to get the total supply of.
388
+ /// @return supply The total number of outstanding NFTs.
385
389
  function totalSupplyOf(address hook) external view override returns (uint256 supply) {
386
390
  // Keep a reference to the greatest tier ID.
387
391
  uint256 maxTierId = maxTierIdOf[hook];
@@ -402,13 +406,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
402
406
  }
403
407
  }
404
408
 
405
- /// @notice Get the number of voting units the provided address has for the provided 721 contract (across all
406
- /// tiers).
407
- /// @dev NFTs have a tier-specific number of voting units. If the tier does not have a custom number of voting
408
- /// units, the price is used.
409
- /// @param hook The 721 contract to get the voting units within.
409
+ /// @notice Get an address's total voting power across all tiers of a hook. Sums up the voting units from every
410
+ /// tier where the address holds NFTs.
411
+ /// @dev Each tier contributes: `balance * (customVotingUnits || tierPrice)`.
412
+ /// @param hook The 721 hook contract to get voting units within.
410
413
  /// @param account The address to get the voting unit total of.
411
- /// @return units The total voting units the address has within the 721 contract.
414
+ /// @return units The total voting units the address holds across all tiers.
412
415
  function votingUnitsOf(address hook, address account) external view virtual override returns (uint256 units) {
413
416
  // Keep a reference to the greatest tier ID.
414
417
  uint256 maxTierId = maxTierIdOf[hook];
@@ -446,11 +449,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
446
449
  // -------------------------- public views --------------------------- //
447
450
  //*********************************************************************//
448
451
 
449
- /// @notice Get the number of NFTs that the specified address has from the specified 721 contract (across all
450
- /// tiers).
451
- /// @param hook The 721 contract to get the balance within.
452
+ /// @notice How many NFTs an address owns from a hook, totaled across all tiers.
453
+ /// @param hook The 721 hook contract to check.
452
454
  /// @param owner The address to check the balance of.
453
- /// @return balance The number of NFTs the owner has from the 721 contract.
455
+ /// @return balance The total number of NFTs the owner holds.
454
456
  function balanceOf(address hook, address owner) public view override returns (uint256 balance) {
455
457
  // Keep a reference to the greatest tier ID.
456
458
  uint256 maxTierId = maxTierIdOf[hook];
@@ -466,13 +468,13 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
466
468
  }
467
469
  }
468
470
 
469
- /// @notice The combined cash out weight of the NFTs with the provided token IDs.
470
- /// @dev Cash out weight is based on 721 price.
471
- /// @dev Divide this result by the `totalCashOutWeight` to get the portion of funds that can be reclaimed by
472
- /// cashing out these NFTs.
473
- /// @param hook The 721 contract that the NFTs belong to.
474
- /// @param tokenIds The token IDs of the NFTs to get the cash out weight of.
475
- /// @return weight The cash out weight.
471
+ /// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeight` to get the fraction of
472
+ /// the project's surplus that cashing out these NFTs would reclaim.
473
+ /// @dev Weight is based on each NFT's original tier price (not the discounted price paid). Discounts are
474
+ /// transient purchase incentives and don't affect an NFT's share of the cash-out pool.
475
+ /// @param hook The 721 hook contract the NFTs belong to.
476
+ /// @param tokenIds The token IDs to get the combined cash-out weight of.
477
+ /// @return weight The combined cash-out weight.
476
478
  function cashOutWeightOf(address hook, uint256[] calldata tokenIds) public view override returns (uint256 weight) {
477
479
  // Add each 721's original price (from its tier) to the weight.
478
480
  // Uses the full tier price, not the discounted price — by design. Discounts are transient incentives
@@ -488,10 +490,11 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
488
490
  }
489
491
  }
490
492
 
491
- /// @notice The reserve beneficiary for the provided tier ID on the provided 721 contract.
492
- /// @param hook The 721 contract that the tier belongs to.
493
- /// @param tierId The ID of the tier to get the reserve beneficiary of.
494
- /// @return The reserve beneficiary for the tier.
493
+ /// @notice The address that receives reserved NFTs for a tier. Falls back to the hook's default reserve
494
+ /// beneficiary if no tier-specific beneficiary is set.
495
+ /// @param hook The 721 hook contract.
496
+ /// @param tierId The ID of the tier.
497
+ /// @return The reserve beneficiary address for the tier.
495
498
  function reserveBeneficiaryOf(address hook, uint256 tierId) public view override returns (address) {
496
499
  // Get the stored reserve beneficiary.
497
500
  address storedReserveBeneficiaryOfTier = _reserveBeneficiaryOf[hook][tierId];
@@ -505,19 +508,18 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
505
508
  return defaultReserveBeneficiaryOf[hook];
506
509
  }
507
510
 
508
- /// @notice The tier ID for the 721 with the provided token ID.
509
- /// @dev Tiers are 1-indexed from the `tiers` array, meaning the 0th element of the array is tier 1.
510
- /// @param tokenId The token ID of the 721 to get the tier ID of.
511
- /// @return The ID of the 721's tier.
511
+ /// @notice Derive which tier an NFT belongs to from its token ID. Token IDs encode the tier: `tokenId / 1e9`
512
+ /// gives the tier ID.
513
+ /// @param tokenId The token ID of the NFT.
514
+ /// @return The tier ID.
512
515
  function tierIdOfToken(uint256 tokenId) public pure override returns (uint256) {
513
516
  return tokenId / _ONE_BILLION;
514
517
  }
515
518
 
516
- /// @notice Get the tier with the provided ID from the provided 721 contract.
517
- /// @param hook The 721 contract to get the tier from.
519
+ /// @notice Get the full details of a specific tier by its ID price, supply, reserve info, metadata, etc.
520
+ /// @param hook The 721 hook contract to get the tier from.
518
521
  /// @param id The ID of the tier to get.
519
- /// @param includeResolvedUri If set to `true`, if the contract has a token URI resolver, its content will be
520
- /// resolved and included.
522
+ /// @param includeResolvedUri If `true` and the hook has a `tokenUriResolver`, resolves the URI and includes it.
521
523
  /// @return The tier.
522
524
  function tierOf(address hook, uint256 id, bool includeResolvedUri) public view override returns (JB721Tier memory) {
523
525
  return _getTierFrom({
@@ -525,9 +527,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
525
527
  });
526
528
  }
527
529
 
528
- /// @notice The combined cash out weight for all NFTs from the provided 721 contract.
529
- /// @param hook The 721 contract to get the total cash out weight of.
530
- /// @return weight The total cash out weight.
530
+ /// @notice The total cash-out weight across all outstanding NFTs (including pending reserves). This is the
531
+ /// denominator used to determine what fraction of a project's surplus each NFT can reclaim on cash out.
532
+ /// @param hook The 721 hook contract to get the total cash-out weight of.
533
+ /// @return weight The total cash-out weight.
531
534
  // Changing defaultReserveBeneficiary retroactively affects totalCashOutWeight. By design —
532
535
  // cashOutWeight is calculated dynamically, not snapshotted. The defaultReserveBeneficiary determines which
533
536
  // tiers have pending reserves (via _numberOfPendingReservesFor), affecting the denominator. Changing it is
@@ -752,13 +755,15 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
752
755
  }
753
756
  }
754
757
 
755
- /// @notice Pack five bools into a single uint8.
756
- /// @param allowOwnerMint Whether or not owner minting is allowed in new tiers.
757
- /// @param transfersPausable Whether or not 721 transfers can be paused.
758
- /// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers.
759
- /// @param cantBeRemoved Whether or not attempts to remove the tier will revert.
760
- /// @param cantIncreaseDiscountPercent Whether or not attempts to increase the discount percent will revert.
761
- /// @param cantBuyWithCredits Whether or not the tier cannot be purchased using accumulated pay credits.
758
+ /// @notice Pack six tier-level boolean flags into a single uint8 for compact storage in `JBStored721Tier`.
759
+ /// @dev Bit layout: 0=allowOwnerMint, 1=transfersPausable, 2=useVotingUnits, 3=cantBeRemoved,
760
+ /// 4=cantIncreaseDiscountPercent, 5=cantBuyWithCredits.
761
+ /// @param allowOwnerMint Whether the project owner can mint from this tier directly (without paying).
762
+ /// @param transfersPausable Whether transfers of NFTs from this tier can be paused by the ruleset.
763
+ /// @param useVotingUnits Whether this tier uses a custom voting power value instead of defaulting to its price.
764
+ /// @param cantBeRemoved Whether this tier is permanently locked and cannot be removed once added.
765
+ /// @param cantIncreaseDiscountPercent Whether the discount percent can only stay the same or decrease.
766
+ /// @param cantBuyWithCredits Whether this tier cannot be purchased using accumulated pay credits.
762
767
  /// @return packed The packed bools.
763
768
  function _packBools(
764
769
  bool allowOwnerMint,
@@ -782,14 +787,16 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
782
787
  }
783
788
  }
784
789
 
785
- /// @notice Unpack six bools from a single uint8.
790
+ /// @notice Unpack six tier-level boolean flags from a single uint8 stored in `JBStored721Tier.packedBools`.
791
+ /// @dev Inverse of `_packBools`. Same bit layout: 0=allowOwnerMint, 1=transfersPausable, 2=useVotingUnits,
792
+ /// 3=cantBeRemoved, 4=cantIncreaseDiscountPercent, 5=cantBuyWithCredits.
786
793
  /// @param packed The packed bools.
787
- /// @param allowOwnerMint Whether or not owner minting is allowed in new tiers.
788
- /// @param transfersPausable Whether or not 721 transfers can be paused.
789
- /// @param useVotingUnits Whether or not custom voting unit amounts are allowed in new tiers.
790
- /// @param cantBeRemoved Whether or not the tier can be removed once added.
791
- /// @param cantIncreaseDiscountPercent Whether or not the discount percent cannot be increased.
792
- /// @param cantBuyWithCredits Whether or not the tier cannot be purchased using accumulated pay credits.
794
+ /// @param allowOwnerMint Whether the project owner can mint from this tier directly (without paying).
795
+ /// @param transfersPausable Whether transfers of NFTs from this tier can be paused by the ruleset.
796
+ /// @param useVotingUnits Whether this tier uses a custom voting power value instead of defaulting to its price.
797
+ /// @param cantBeRemoved Whether this tier is permanently locked and cannot be removed once added.
798
+ /// @param cantIncreaseDiscountPercent Whether the discount percent can only stay the same or decrease.
799
+ /// @param cantBuyWithCredits Whether this tier cannot be purchased using accumulated pay credits.
793
800
  function _unpackBools(uint8 packed)
794
801
  internal
795
802
  pure
@@ -816,7 +823,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
816
823
  // ---------------------- external transactions ---------------------- //
817
824
  //*********************************************************************//
818
825
 
819
- /// @notice Cleans an 721 contract's removed tiers from the tier sorting sequence.
826
+ /// @notice Walk through the tier sorting sequence and skip over any tiers that have been removed. This compacts
827
+ /// the linked list so that `tiersOf` iteration no longer visits removed tiers. Anyone can call this — it's a
828
+ /// maintenance operation with no access control.
829
+ /// @dev Call this after `recordRemoveTierIds` to keep tier iteration efficient. Without cleaning, removed tiers
830
+ /// remain in the linked list (they just can't be minted from). The function also updates
831
+ /// `_startingTierIdOfCategory` if the previous starting tier for a category was removed.
820
832
  /// @param hook The 721 contract to clean tiers for.
821
833
  function cleanTiers(address hook) external override {
822
834
  // Keep a reference to the last tier ID.
@@ -873,13 +885,16 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
873
885
  emit CleanTiers({hook: hook, caller: msg.sender});
874
886
  }
875
887
 
876
- /// @notice Record newly added tiers.
877
- /// @dev WARNING: If any tier in `tiersToAdd` has `useReserveBeneficiaryAsDefault` set to `true`, its
888
+ /// @notice Validate and store new tiers for the calling hook. Each tier is assigned a sequential ID, inserted
889
+ /// into the category-sorted linked list, and has its reserve beneficiary, voting units, IPFS URI, and flags
890
+ /// persisted. Tiers must be provided sorted by category (ascending).
891
+ /// @dev Only callable by hook contracts (msg.sender is treated as the hook address).
892
+ /// WARNING: If any tier in `tiersToAdd` has `useReserveBeneficiaryAsDefault` set to `true`, its
878
893
  /// `reserveBeneficiary` will overwrite the hook's global `defaultReserveBeneficiaryOf`. This affects ALL existing
879
894
  /// tiers that do not have a tier-specific reserve beneficiary set via `_reserveBeneficiaryOf`. Callers should be
880
895
  /// aware of this side effect when using `adjustTiers` to add new tiers.
881
896
  /// @param tiersToAdd The tiers to add.
882
- /// @return tierIds The IDs of the tiers being added.
897
+ /// @return tierIds The IDs of the tiers added.
883
898
  function recordAddTiers(JB721TierConfig[] calldata tiersToAdd)
884
899
  external
885
900
  override
@@ -1125,7 +1140,8 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1125
1140
  maxTierIdOf[msg.sender] = currentMaxTierIdOf + tiersToAdd.length;
1126
1141
  }
1127
1142
 
1128
- /// @notice Records 721 burns.
1143
+ /// @notice Increment the burn counter for each token's tier. Does NOT affect `remainingSupply` — burned NFTs
1144
+ /// cannot be re-minted. The burn count is used by `totalSupplyOf` to compute the circulating supply.
1129
1145
  /// @dev This function trusts `msg.sender` (the hook contract) to only call it after actually burning the
1130
1146
  /// tokens. It does not verify ownership or existence of the token IDs — the hook is responsible for
1131
1147
  /// performing those checks before calling this function.
@@ -1147,16 +1163,22 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1147
1163
  }
1148
1164
  }
1149
1165
 
1150
- /// @notice Record newly set flags.
1166
+ /// @notice Store the behavioral flags for the calling hook. These flags govern whether new tiers can have voting
1167
+ /// units, reserves, or owner minting enabled.
1168
+ /// @dev Only callable by hook contracts. Overwrites any previously stored flags for the caller.
1151
1169
  /// @param flags The flags to set.
1152
1170
  function recordFlags(JB721TiersHookFlags calldata flags) external override {
1153
1171
  _flagsOf[msg.sender] = flags;
1154
1172
  }
1155
1173
 
1156
- /// @notice Record 721 mints from the provided tiers.
1157
- /// @param amount The amount being spent on NFTs. The total price must not exceed this amount.
1174
+ /// @notice Record paid mints: deduct each tier's (discounted) price from `amount`, decrement supply, generate
1175
+ /// token IDs, and enforce that enough supply remains to satisfy pending reserves. Returns the leftover amount
1176
+ /// and the total cost of credit-restricted tiers so the hook can enforce pay-credit rules.
1177
+ /// @dev Reverts if the tier is removed, unrecognized, sold out, or its price exceeds the remaining amount.
1178
+ /// For owner mints, the tier must have `allowOwnerMint` set.
1179
+ /// @param amount The amount to spend on NFTs. The total price must not exceed this amount.
1158
1180
  /// @param tierIds The IDs of the tiers to mint from.
1159
- /// @param isOwnerMint A flag indicating whether this function is being directly called by the 721 contract's owner.
1181
+ /// @param isOwnerMint A flag indicating whether the 721 contract's owner is directly calling this function.
1160
1182
  /// @return tokenIds The token IDs of the NFTs which were minted.
1161
1183
  /// @return leftoverAmount The `amount` remaining after minting.
1162
1184
  /// @return restrictedCost Total cost of tiers with `cantBuyWithCredits` set. The caller can use this to enforce
@@ -1252,7 +1274,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1252
1274
  }
1253
1275
  }
1254
1276
 
1255
- /// @notice Record reserve 721 minting for the provided tier ID on the provided 721 contract.
1277
+ /// @notice Generate token IDs for reserve NFT mints and decrement the tier's remaining supply. Reserves
1278
+ /// accumulate as non-reserve NFTs are minted (one reserve per `reserveFrequency` mints). Reverts if `count`
1279
+ /// exceeds the number of pending reserves.
1256
1280
  /// @param tierId The ID of the tier to mint reserves from.
1257
1281
  /// @param count The number of reserve NFTs to mint.
1258
1282
  /// @return tokenIds The token IDs of the reserve NFTs which were minted.
@@ -1292,10 +1316,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1292
1316
  }
1293
1317
  }
1294
1318
 
1295
- /// @notice Record tiers being removed.
1319
+ /// @notice Mark one or more tiers as removed. Removed tiers cannot be minted from, but existing NFTs from those
1320
+ /// tiers remain valid (can still be cashed out and transferred). Pending reserves can still be minted.
1296
1321
  /// @dev Removing a tier only marks it in a bitmap — it does not update the sorted tier linked list.
1297
1322
  /// Call `cleanTiers()` after removing tiers to update the sorting sequence and prevent stale tier iteration.
1298
- /// @param tierIds The IDs of the tiers being removed.
1323
+ /// Reverts if the tier has `cantBeRemoved` set, or if the tier ID is 0 or exceeds `maxTierIdOf`.
1324
+ /// @param tierIds The IDs of the tiers to remove.
1299
1325
  function recordRemoveTierIds(uint256[] calldata tierIds) external override {
1300
1326
  for (uint256 i; i < tierIds.length;) {
1301
1327
  // Set the tier being iterated upon (0-indexed).
@@ -1325,9 +1351,11 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1325
1351
  }
1326
1352
  }
1327
1353
 
1328
- /// @notice Records the setting of a discount for a tier.
1354
+ /// @notice Update the discount percentage for a tier. Discounts reduce the price payers pay without affecting the
1355
+ /// NFT's cash-out weight (which always uses the original price). Reverts if the tier is removed, if the percent
1356
+ /// exceeds the denominator, or if the tier has `cantIncreaseDiscountPercent` set and the new value is higher.
1329
1357
  /// @param tierId The ID of the tier to record a discount for.
1330
- /// @param discountPercent The new discount percent being applied.
1358
+ /// @param discountPercent The new discount percent to apply.
1331
1359
  function recordSetDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
1332
1360
  // Make sure the tier hasn't been removed.
1333
1361
  JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[msg.sender].readId(tierId);
@@ -1370,10 +1398,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1370
1398
  tokenUriResolverOf[msg.sender] = resolver;
1371
1399
  }
1372
1400
 
1373
- /// @notice Record an 721 transfer.
1374
- /// @param tierId The ID of the tier that the 721 being transferred belongs to.
1375
- /// @param from The address that the 721 is being transferred from.
1376
- /// @param to The address that the 721 is being transferred to.
1401
+ /// @notice Update tier balance accounting when an NFT is transferred. Decrements the sender's balance and
1402
+ /// increments the receiver's balance for the given tier. Handles mints (from == address(0)) and burns
1403
+ /// (to == address(0)) as one-sided updates.
1404
+ /// @param tierId The ID of the tier that the 721 to transfer belongs to.
1405
+ /// @param from The address to transfer the 721 from.
1406
+ /// @param to The address to transfer the 721 to.
1377
1407
  function recordTransferForTier(uint256 tierId, address from, address to) external override {
1378
1408
  // If this is not a mint,
1379
1409
  if (from != address(0)) {
@@ -20,10 +20,11 @@ import {IJB721Hook} from "../interfaces/IJB721Hook.sol";
20
20
  import {ERC721} from "./ERC721.sol";
21
21
 
22
22
  /// @title JB721Hook
23
- /// @notice When a project which uses this hook is paid, this hook may mint NFTs to the payer, depending on this hook's
24
- /// setup, the amount paid, and information specified by the payer. The project's owner can enable NFT cash outs
25
- /// through this hook, allowing the NFT holders to burn their NFTs to reclaim funds from the project (in proportion to
26
- /// the NFT's price).
23
+ /// @notice Abstract base for Juicebox 721 hooks. Implements the pay hook and cash-out hook interfaces: when a project
24
+ /// is paid through its terminal, this hook mints NFTs to the payer; when NFT holders cash out, this hook burns their
25
+ /// NFTs and lets the terminal send them their share of the project's surplus (proportional to the NFT's price).
26
+ /// @dev Subclasses (like `JB721TiersHook`) implement the actual minting logic via `_processPayment` and burn
27
+ /// tracking via `_didBurn`.
27
28
  abstract contract JB721Hook is ERC721, IJB721Hook {
28
29
  //*********************************************************************//
29
30
  // --------------------------- custom errors ------------------------- //
@@ -270,7 +271,8 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
270
271
  PROJECT_ID = projectId;
271
272
  }
272
273
 
273
- /// @notice Process a received payment.
274
- /// @param context The payment context passed in by the terminal.
274
+ /// @notice Process a received payment by minting NFTs and/or updating credits. Subclasses implement the
275
+ /// specific minting logic (e.g., tier selection, credit tracking, split distribution).
276
+ /// @param context The payment context passed in by the terminal (includes amount, payer, beneficiary, metadata).
275
277
  function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual;
276
278
  }
@@ -8,27 +8,34 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
8
8
  /// @dev Deployed as a clone via JB721CheckpointsDeployer during hook initialization. One module per hook.
9
9
  /// Pass this address to JBTokenDistributor as the IVotes token.
10
10
  interface IJB721Checkpoints is IERC5805 {
11
- /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
12
- /// @dev Looks up the token's tier voting units from the store internally.
13
- /// Auto-self-delegates on first receive so checkpoints work without manual delegation.
14
- /// @param from The previous owner (address(0) on mint).
15
- /// @param to The new owner (address(0) on burn).
16
- /// @param tokenId The token ID being transferred (used to look up tier voting units).
17
- function onTransfer(address from, address to, uint256 tokenId) external;
18
-
19
- /// @notice Initializes a cloned module with its hook and store references.
20
- /// @dev Can only be called once. Called by the deployer after cloning.
21
- /// @param hook The hook this module serves.
22
- /// @param store The store that holds tier data for the hook's NFTs.
23
- function initialize(address hook, IJB721TiersHookStore store) external;
24
-
25
11
  /// @notice The hook that this module tracks voting power for.
26
12
  /// @return The hook address.
27
13
  // forge-lint: disable-next-line(mixed-case-function)
28
14
  function HOOK() external view returns (address);
29
15
 
16
+ /// @notice The owner of an NFT at a past block.
17
+ /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
18
+ /// inferred from the hook's `firstOwnerOf`.
19
+ /// @param tokenId The token ID of the NFT to get the historical owner of.
20
+ /// @param blockNumber The block number to look up.
21
+ /// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
22
+ function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view returns (address);
23
+
30
24
  /// @notice The store that holds tier and voting data for the hook's NFTs.
31
25
  /// @return The store contract.
32
26
  // forge-lint: disable-next-line(mixed-case-function)
33
27
  function STORE() external view returns (IJB721TiersHookStore);
28
+
29
+ /// @notice Initializes a cloned module with its hook reference.
30
+ /// @dev Can only be called once. Called by the deployer after cloning.
31
+ /// @param hook The hook this module serves.
32
+ function initialize(address hook) external;
33
+
34
+ /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
35
+ /// @dev Looks up the token's tier voting units from the store internally.
36
+ /// Auto-self-delegates on first receive so checkpoints work without manual delegation.
37
+ /// @param from The previous owner (address(0) on mint).
38
+ /// @param to The new owner (address(0) on burn).
39
+ /// @param tokenId The token ID to transfer (used to look up tier voting units).
40
+ function onTransfer(address from, address to, uint256 tokenId) external;
34
41
  }
@@ -6,7 +6,7 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
6
6
 
7
7
  /// @notice Deploys JB721Checkpoints clones for JB721TiersHook instances.
8
8
  interface IJB721CheckpointsDeployer {
9
- /// @notice Thrown when the caller is not the hook that the checkpoint module is being deployed for.
9
+ /// @notice Thrown when the caller is not the hook that the checkpoint module is deployed for.
10
10
  error JB721CheckpointsDeployer_Unauthorized();
11
11
 
12
12
  /// @notice The implementation contract that clones are based on.
@@ -14,10 +14,14 @@ interface IJB721CheckpointsDeployer {
14
14
  // forge-lint: disable-next-line(mixed-case-function)
15
15
  function IMPLEMENTATION() external view returns (address);
16
16
 
17
+ /// @notice The store that holds tier and voting data for each hook's NFTs.
18
+ /// @return The store contract.
19
+ // forge-lint: disable-next-line(mixed-case-function)
20
+ function STORE() external view returns (IJB721TiersHookStore);
21
+
17
22
  /// @notice Deploys a new deterministic checkpoint clone for the given hook.
18
23
  /// @dev Uses CREATE2 with the hook address as salt so the clone address is the same across chains.
19
24
  /// @param hook The hook address the module will serve.
20
- /// @param store The store that holds tier data for the hook's NFTs.
21
25
  /// @return module The newly deployed and initialized checkpoint module.
22
- function deploy(address hook, IJB721TiersHookStore store) external returns (IJB721Checkpoints module);
26
+ function deploy(address hook) external returns (IJB721Checkpoints module);
23
27
  }
@@ -21,7 +21,7 @@ interface IJB721TiersHook is IJB721Hook {
21
21
  /// @notice Emitted when an `addToBalanceOf` call reverts during leftover distribution. The funds remain
22
22
  /// stranded in the hook contract.
23
23
  /// @param projectId The project ID whose terminal reverted.
24
- /// @param token The token being sent.
24
+ /// @param token The token to send.
25
25
  /// @param amount The amount that failed to send.
26
26
  /// @param reason The revert reason bytes.
27
27
  event AddToBalanceReverted(uint256 indexed projectId, address token, uint256 amount, bytes reason);
@@ -108,7 +108,7 @@ interface IJB721TiersHook is IJB721Hook {
108
108
  /// project's balance.
109
109
  /// @param projectId The project ID the split belongs to.
110
110
  /// @param split The split that reverted.
111
- /// @param amount The amount that was being paid out.
111
+ /// @param amount The amount that was paid out.
112
112
  /// @param reason The revert reason bytes.
113
113
  /// @param caller The address that called the function.
114
114
  event SplitPayoutReverted(uint256 indexed projectId, JBSplit split, uint256 amount, bytes reason, address caller);
@@ -142,7 +142,7 @@ interface IJB721TiersHook is IJB721Hook {
142
142
 
143
143
  /// @notice Context for the pricing of this hook's tiers.
144
144
  /// @return currency The currency used for tier prices.
145
- /// @return decimals The amount of decimals being used in tier prices.
145
+ /// @return decimals The number of decimals used in tier prices.
146
146
  function pricingContext() external view returns (uint256 currency, uint256 decimals);
147
147
 
148
148
  /// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
@@ -40,9 +40,10 @@ interface IJB721TiersHookProjectDeployer {
40
40
  returns (uint256 projectId, IJB721TiersHook hook);
41
41
 
42
42
  /// @notice Launches rulesets for a project with an attached 721 tiers hook.
43
- /// @param projectId The ID of the project that rulesets are being launched for.
43
+ /// @param projectId The ID of the project to launch rulesets for.
44
44
  /// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook.
45
45
  /// @param launchRulesetsConfig Configuration which dictates the project's new rulesets.
46
+ /// @param projectUri Metadata URI to associate with the project. Pass an empty string to leave it unchanged.
46
47
  /// @param controller The controller that the project's rulesets will be queued with.
47
48
  /// @param salt A salt to use for the deterministic deployment.
48
49
  /// @return rulesetId The ID of the successfully created ruleset.
@@ -51,6 +52,7 @@ interface IJB721TiersHookProjectDeployer {
51
52
  uint256 projectId,
52
53
  JBDeploy721TiersHookConfig memory deployTiersHookConfig,
53
54
  JBLaunchRulesetsConfig memory launchRulesetsConfig,
55
+ string memory projectUri,
54
56
  IJBController controller,
55
57
  bytes32 salt
56
58
  )
@@ -58,7 +60,7 @@ interface IJB721TiersHookProjectDeployer {
58
60
  returns (uint256 rulesetId, IJB721TiersHook hook);
59
61
 
60
62
  /// @notice Queues rulesets for a project with an attached 721 tiers hook.
61
- /// @param projectId The ID of the project that rulesets are being queued for.
63
+ /// @param projectId The ID of the project to queue rulesets for.
62
64
  /// @param deployTiersHookConfig Configuration which dictates the behavior of the 721 tiers hook.
63
65
  /// @param queueRulesetsConfig Configuration which dictates the project's newly queued rulesets.
64
66
  /// @param controller The controller that the project's rulesets will be queued with.