@bananapus/721-hook-v6 0.0.43 → 0.0.46

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/CHANGELOG.md CHANGED
@@ -20,7 +20,7 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
20
20
  - The repo now carries a dedicated helper library to keep the hook surface manageable and to support the larger v6 feature set.
21
21
  - The repo was upgraded from the v5 Solidity baseline to `0.8.28`.
22
22
 
23
- ## Local audit remediations
23
+ ## Local review remediations
24
24
 
25
25
  - `JB721TiersHookProjectDeployer.launchRulesetsFor` now checks `LAUNCH_RULESETS` instead of `QUEUE_RULESETS`. The previous check was semantically wrong — launching active rulesets should require the launch permission, not the queue permission.
26
26
 
@@ -43,7 +43,7 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
43
43
 
44
44
  ## Indexer impact
45
45
 
46
- - New events: `AddToBalanceReverted` (declared but no longer emitted -- replaced by `JB721TiersHookLib_SplitFallbackFailed` revert error), `SetName`, `SetSymbol`, `SplitPayoutReverted`.
46
+ - New events: `SetName`, `SetSymbol`, `SplitPayoutReverted`.
47
47
  - Tier config decoding changed because `JB721TierConfig` is no longer v5-compatible.
48
48
  - Collection metadata can now change after deployment, so one-time indexing of `name` and `symbol` is no longer sufficient.
49
49
 
@@ -62,7 +62,6 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
62
62
  - `pricingContext()`
63
63
  - `setMetadata(...)`
64
64
  - Added events
65
- - `AddToBalanceReverted` (declared in interface but no longer emitted; the library now reverts with `JB721TiersHookLib_SplitFallbackFailed` instead)
66
65
  - `SetName`
67
66
  - `SetSymbol`
68
67
  - `SplitPayoutReverted`
package/README.md CHANGED
@@ -73,7 +73,7 @@ That split is why UI bugs, economic bugs, and deployment bugs often land in diff
73
73
  1. `test/E2E/Pay_Mint_Redeem_E2E.t.sol`
74
74
  2. `test/invariants/TierLifecycleInvariant.t.sol`
75
75
  3. `test/invariants/TieredHookStoreInvariant.t.sol`
76
- 4. `test/audit/CodexSplitCreditsMismatch.t.sol`
76
+ 4. `test/regression/RegressionSplitCreditsMismatch.t.sol`
77
77
  5. `test/regression/ProjectDeployerRulesets.t.sol`
78
78
 
79
79
  ## Install
@@ -112,7 +112,7 @@ src/
112
112
  libraries/
113
113
  structs/
114
114
  test/
115
- unit, E2E, fork, invariant, audit, and regression coverage
115
+ unit, E2E, fork, invariant, review, and regression coverage
116
116
  script/
117
117
  Deploy.s.sol
118
118
  helpers/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.43",
3
+ "version": "0.0.46",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@bananapus/address-registry-v6": "0.0.25",
32
- "@bananapus/core-v6": "^0.0.39",
32
+ "@bananapus/core-v6": "0.0.44",
33
33
  "@bananapus/ownable-v6": "^0.0.24",
34
34
  "@bananapus/permission-ids-v6": "0.0.22",
35
35
  "@openzeppelin/contracts": "5.6.1",
@@ -25,8 +25,8 @@
25
25
  ## Useful Proof Points
26
26
 
27
27
  - [`test/Fork.t.sol`](../test/Fork.t.sol) for live-integration assumptions.
28
- - [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for known edge cases the repo authors considered worth pinning down.
28
+ - [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for known edge cases the repo authors considered worth pinning down.
29
29
  - [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) when you need a narrow function-level proof before editing a broad runtime path.
30
30
  - [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) when a local patch may have broken store-level relationships.
31
- - [`test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol`](../test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol) when reserve-beneficiary or pending-reserve behavior changes.
31
+ - [`test/regression/RetroactiveReserveBeneficiaryDilution.t.sol`](../test/regression/RetroactiveReserveBeneficiaryDilution.t.sol) when reserve-beneficiary or pending-reserve behavior changes.
32
32
  - [`script/Deploy.s.sol`](../script/Deploy.s.sol) when a deployment or launch question is really about config assembly rather than contract behavior.
@@ -30,4 +30,4 @@
30
30
  - [`test/TestVotingUnitsLifecycle.t.sol`](../test/TestVotingUnitsLifecycle.t.sol) for voting-unit lifecycle behavior.
31
31
  - [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) for checkpoint/module behavior.
32
32
  - [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) for store-level lifecycle invariants.
33
- - [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol), [`test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol`](../test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol), and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for reentrancy and attack-surface checks.
33
+ - [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol), [`test/regression/RetroactiveReserveBeneficiaryDilution.t.sol`](../test/regression/RetroactiveReserveBeneficiaryDilution.t.sol), and [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for reentrancy and attack-surface checks.
@@ -35,7 +35,7 @@ library Hook721DeploymentLib {
35
35
 
36
36
  for (uint256 _i; _i < networks.length; _i++) {
37
37
  if (networks[_i].chainId == chainId) {
38
- return getDeployment(path, networks[_i].name);
38
+ return getDeployment({path: path, network_name: networks[_i].name});
39
39
  }
40
40
  }
41
41
 
@@ -52,15 +52,31 @@ library Hook721DeploymentLib {
52
52
  returns (Hook721Deployment memory deployment)
53
53
  {
54
54
  deployment.hook_deployer = IJB721TiersHookDeployer(
55
- _getDeploymentAddress(path, "nana-721-hook-v6", network_name, "JB721TiersHookDeployer")
55
+ _getDeploymentAddress({
56
+ path: path,
57
+ project_name: "nana-721-hook-v6",
58
+ network_name: network_name,
59
+ contractName: "JB721TiersHookDeployer"
60
+ })
56
61
  );
57
62
 
58
63
  deployment.project_deployer = IJB721TiersHookProjectDeployer(
59
- _getDeploymentAddress(path, "nana-721-hook-v6", network_name, "JB721TiersHookProjectDeployer")
64
+ _getDeploymentAddress({
65
+ path: path,
66
+ project_name: "nana-721-hook-v6",
67
+ network_name: network_name,
68
+ contractName: "JB721TiersHookProjectDeployer"
69
+ })
60
70
  );
61
71
 
62
- deployment.store =
63
- IJB721TiersHookStore(_getDeploymentAddress(path, "nana-721-hook-v6", network_name, "JB721TiersHookStore"));
72
+ deployment.store = IJB721TiersHookStore(
73
+ _getDeploymentAddress({
74
+ path: path,
75
+ project_name: "nana-721-hook-v6",
76
+ network_name: network_name,
77
+ contractName: "JB721TiersHookStore"
78
+ })
79
+ );
64
80
  }
65
81
 
66
82
  /// @notice Get the address of a contract that was deployed by the Deploy script.
@@ -23,8 +23,8 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
23
23
  // --------------------------- custom errors ------------------------- //
24
24
  //*********************************************************************//
25
25
 
26
- error JB721Checkpoints_AlreadyInitialized();
27
- error JB721Checkpoints_Unauthorized();
26
+ error JB721Checkpoints_AlreadyInitialized(address hook);
27
+ error JB721Checkpoints_Unauthorized(address caller, address hook);
28
28
 
29
29
  //*********************************************************************//
30
30
  // --------------- public immutable stored properties ---------------- //
@@ -68,9 +68,8 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
68
68
  /// @dev Can only be called once. Called by the deployer after cloning.
69
69
  /// @param hook The hook this module serves.
70
70
  function initialize(address hook) external override {
71
- if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized();
71
+ if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized({hook: HOOK});
72
72
  // `hook` cannot be zero when called through the deployer because `msg.sender` must equal `hook`.
73
- // slither-disable-next-line missing-zero-check
74
73
  HOOK = hook;
75
74
  }
76
75
 
@@ -78,13 +77,12 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
78
77
  /// @dev Only callable by the HOOK. Looks up the token's tier voting units from the store.
79
78
  /// @param from The previous owner (address(0) on mint).
80
79
  /// @param to The new owner (address(0) on burn).
81
- /// @param tokenId The token ID being transferred.
80
+ /// @param tokenId The token ID to transfer.
82
81
  function onTransfer(address from, address to, uint256 tokenId) external override {
83
- if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized();
82
+ if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized({caller: msg.sender, hook: HOOK});
84
83
 
85
84
  if (from != address(0)) {
86
85
  // forge-lint: disable-next-line(unsafe-typecast)
87
- // slither-disable-next-line unused-return
88
86
  _ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(to)});
89
87
  }
90
88
 
@@ -13,6 +13,12 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
13
13
  /// @dev The implementation is deployed once in the constructor. Each `deploy()` call clones it (~45k gas) and
14
14
  /// initializes the clone with the hook and store references.
15
15
  contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
16
+ //*********************************************************************//
17
+ // --------------------------- custom errors ------------------------- //
18
+ //*********************************************************************//
19
+
20
+ error JB721CheckpointsDeployer_Unauthorized(address caller, address hook);
21
+
16
22
  //*********************************************************************//
17
23
  // --------------- public immutable stored properties ---------------- //
18
24
  //*********************************************************************//
@@ -42,7 +48,9 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
42
48
  /// @param hook The hook address the module will serve.
43
49
  /// @return module The newly deployed and initialized checkpoint module.
44
50
  function deploy(address hook) external override returns (IJB721Checkpoints module) {
45
- if (msg.sender != hook) revert JB721CheckpointsDeployer_Unauthorized();
51
+ if (msg.sender != hook) {
52
+ revert JB721CheckpointsDeployer_Unauthorized({caller: msg.sender, hook: hook});
53
+ }
46
54
 
47
55
  module = IJB721Checkpoints(
48
56
  LibClone.cloneDeterministic({implementation: IMPLEMENTATION, salt: bytes32(uint256(uint160(hook)))})
@@ -42,12 +42,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
42
42
  //*********************************************************************//
43
43
 
44
44
  error JB721TiersHook_AlreadyInitialized(uint256 projectId);
45
- error JB721TiersHook_CantBuyWithCredits();
46
45
  error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
47
- error JB721TiersHook_MintReserveNftsPaused();
48
- error JB721TiersHook_NoProjectId();
49
- error JB721TiersHook_Overspending(uint256 leftoverAmount);
50
- error JB721TiersHook_TierTransfersPaused();
46
+ error JB721TiersHook_MintReserveNftsPaused(uint256 projectId, uint256 tierId);
47
+ error JB721TiersHook_NoProjectId(uint256 projectId);
48
+ error JB721TiersHook_TierTransfersPaused(uint256 projectId, uint256 tokenId, address from, address to);
51
49
 
52
50
  //*********************************************************************//
53
51
  // --------------- public immutable stored properties ---------------- //
@@ -151,18 +149,19 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
151
149
  // ------------------------- external views -------------------------- //
152
150
  //*********************************************************************//
153
151
 
154
- /// @notice The first owner of an NFT.
155
- /// @dev This is generally the address which paid for the NFT.
156
- /// @param tokenId The token ID of the NFT to get the first owner of.
152
+ /// @notice The address that originally received an NFT (typically the payer). Tracked separately from the current
153
+ /// owner so it persists through transfers, useful for provenance and historical voting checkpoints.
154
+ /// @param tokenId The token ID of the NFT.
157
155
  /// @return The address of the NFT's first owner.
158
156
  function firstOwnerOf(uint256 tokenId) external view override returns (address) {
159
157
  address first = _firstOwnerOf[tokenId];
160
158
  return first != address(0) ? first : _ownerOf(tokenId);
161
159
  }
162
160
 
163
- /// @notice Context for the pricing of this hook's tiers.
161
+ /// @notice The currency and decimal precision used for this hook's tier prices. For example, if tiers are priced
162
+ /// in ETH with 18 decimals, `currency` would be the ETH currency ID and `decimals` would be 18.
164
163
  /// @return currency The currency used for tier prices.
165
- /// @return decimals The amount of decimals being used in tier prices.
164
+ /// @return decimals The number of decimals used in tier prices.
166
165
  function pricingContext() external view override returns (uint256 currency, uint256 decimals) {
167
166
  // Get a reference to the packed pricing context.
168
167
  uint256 packed = _packedPricingContext;
@@ -185,12 +184,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
185
184
  return STORE.balanceOf({hook: address(this), owner: owner});
186
185
  }
187
186
 
188
- /// @notice The data calculated before a payment is recorded in the terminal store.
189
- /// @dev Overrides the base to calculate the split amount to forward based on tier split percentages.
190
- /// @param context The payment context.
191
- /// @return weight The weight to use for token minting, adjusted down when tier splits route funds away from the
192
- /// project (unless `issueTokensForSplits` is set).
193
- /// @return hookSpecifications The hook specifications, with the split amount to forward.
187
+ /// @notice Called by the terminal before recording a payment. Calculates how much of the payment should be routed
188
+ /// to tier-based splits vs. kept by the project, and adjusts the minting weight accordingly.
189
+ /// @dev Overrides the base to compute tier split amounts from each tier's `splitPercent`.
190
+ /// @param context The payment context from the terminal.
191
+ /// @return weight The adjusted weight for project token minting (reduced when splits route funds away).
192
+ /// @return hookSpecifications Specifies this hook as the pay hook, with the split amount to forward.
194
193
  function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
195
194
  public
196
195
  view
@@ -222,17 +221,20 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
222
221
  });
223
222
  }
224
223
 
225
- /// @notice The combined cash out weight of the NFTs with the specified token IDs.
226
- /// @dev An NFT's cash out weight is its price.
227
- /// @dev To get their relative cash out weight, divide the result by the `totalCashOutWeight(...)`.
228
- /// @param tokenIds The token IDs of the NFTs to get the cumulative cash out weight of.
229
- /// @return weight The cash out weight of the tokenIds.
224
+ /// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeight()` to get the fraction of
225
+ /// surplus these NFTs can reclaim. Weight is based on the original tier price, not any discount paid.
226
+ /// @param tokenIds The token IDs of the NFTs to get the combined cash-out weight of.
227
+ /// @return weight The combined cash-out weight.
230
228
  function cashOutWeightOf(uint256[] memory tokenIds) public view virtual override returns (uint256) {
231
229
  return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
232
230
  }
233
231
 
234
- /// @notice Initializes a cloned copy of the original hook contract.
235
- /// @param projectId The ID of the project this this hook is associated with.
232
+ /// @notice Initialize a cloned copy of the hook. Sets the project association, ERC-721 name/symbol, pricing
233
+ /// context (currency + decimals), metadata URIs, initial tiers, and behavioral flags. Can only be called once
234
+ /// per clone — the implementation contract is pre-initialized in its constructor to prevent misuse.
235
+ /// @dev Called by `JB721TiersHookDeployer` immediately after cloning. Reverts with
236
+ /// `JB721TiersHook_AlreadyInitialized` if called more than once, or `JB721TiersHook_NoProjectId` if projectId is 0.
237
+ /// @param projectId The ID of the project this hook is associated with.
236
238
  /// @param name The name of the NFT collection.
237
239
  /// @param symbol The symbol representing the NFT collection.
238
240
  /// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
@@ -260,7 +262,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
260
262
  _initialized = true;
261
263
 
262
264
  // Make sure a projectId is provided.
263
- if (projectId == 0) revert JB721TiersHook_NoProjectId();
265
+ if (projectId == 0) revert JB721TiersHook_NoProjectId({projectId: projectId});
264
266
 
265
267
  // Initialize the superclass.
266
268
  JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
@@ -275,7 +277,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
275
277
  // pack the pricing decimals in bits 32-39 (8 bits).
276
278
  packed |= uint256(tiersConfig.decimals) << 32;
277
279
  // Store the packed value.
278
- // slither-disable-next-line events-maths
279
280
  _packedPricingContext = packed;
280
281
 
281
282
  // Store the base URI if provided.
@@ -328,9 +329,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
328
329
  return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
329
330
  }
330
331
 
331
- /// @notice The combined cash out weight of all outstanding NFTs.
332
- /// @dev An NFT's cash out weight is its price.
333
- /// @return weight The total cash out weight.
332
+ /// @notice The total cash-out weight across all outstanding NFTs and pending reserves. This is the denominator
333
+ /// for cash-out calculations — an NFT's share of the surplus is its weight divided by this total.
334
+ /// @return weight The total cash-out weight.
334
335
  function totalCashOutWeight() public view virtual override returns (uint256) {
335
336
  return STORE.totalCashOutWeight(address(this));
336
337
  }
@@ -339,12 +340,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
339
340
  // ---------------------- external transactions ---------------------- //
340
341
  //*********************************************************************//
341
342
 
342
- /// @notice Add or delete tiers.
343
- /// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the
344
- /// tiers.
345
- /// @dev Any added tiers must adhere to this hook's `JB721TiersHookFlags`.
346
- /// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs`.
347
- /// @param tierIdsToRemove The tiers to remove, as an array of tier IDs.
343
+ /// @notice Add new NFT tiers or remove existing ones. Added tiers get sequential IDs and must be sorted by
344
+ /// category. Removed tiers stop accepting new mints but existing NFTs remain valid.
345
+ /// @dev Only the collection owner or an operator with `ADJUST_721_TIERS` permission can call this.
346
+ /// @dev Added tiers must respect this hook's flags (e.g. `noNewTiersWithVotes`, `noNewTiersWithReserves`).
347
+ /// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs.
348
+ /// @param tierIdsToRemove The IDs of the tiers to remove.
348
349
  function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override {
349
350
  // Enforce permissions.
350
351
  _requirePermissionFrom({
@@ -363,9 +364,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
363
364
  });
364
365
  }
365
366
 
366
- /// @notice Manually mint NFTs from the provided tiers .
367
+ /// @notice Manually mint NFTs from specific tiers to a beneficiary, without requiring payment. Only tiers with
368
+ /// `allowOwnerMint` enabled can be minted this way.
369
+ /// @dev Only the collection owner or an operator with `MINT_721` permission can call this.
367
370
  /// @param tierIds The IDs of the tiers to mint from.
368
- /// @param beneficiary The address to mint to.
371
+ /// @param beneficiary The address to mint the NFTs to.
369
372
  /// @return tokenIds The IDs of the newly minted tokens.
370
373
  function mintFor(
371
374
  uint16[] calldata tierIds,
@@ -379,7 +382,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
379
382
  _requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.MINT_721});
380
383
 
381
384
  // Record the mint. The token IDs returned correspond to the tiers passed in.
382
- // slither-disable-next-line reentrancy-events,unused-return
383
385
  (tokenIds,,) = STORE.recordMint({
384
386
  amount: type(uint256).max, // force the mint.
385
387
  tierIds: tierIds,
@@ -389,9 +391,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
389
391
  _mintTokens({tokenIds: tokenIds, tierIds: tierIds, beneficiary: beneficiary, totalAmountPaid: 0});
390
392
  }
391
393
 
392
- /// @notice Mint pending reserved NFTs based on the provided information.
393
- /// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
394
- /// @param reserveMintConfigs Contains information about how many reserved tokens to mint for each tier.
394
+ /// @notice Mint pending reserved NFTs across multiple tiers in a single call. Reserves accumulate automatically
395
+ /// as NFTs are sold (based on each tier's `reserveFrequency`) and anyone can trigger their minting.
396
+ /// @param reserveMintConfigs The tier IDs and counts specifying how many reserves to mint from each tier.
395
397
  function mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs) external override {
396
398
  for (uint256 i; i < reserveMintConfigs.length;) {
397
399
  // Get a reference to the params being iterated upon.
@@ -406,12 +408,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
406
408
  }
407
409
  }
408
410
 
409
- /// @notice Allows the collection's owner to set the discount for a tier, if the tier allows it.
410
- /// @dev Only the contract's owner or an operator with the `SET_721_DISCOUNT_PERCENT` permission from the owner can
411
- /// adjust the
412
- /// tiers.
411
+ /// @notice Set a discount on a tier's price. Discounts reduce the price payers must pay, but don't affect the
412
+ /// NFT's cash-out weight (which always uses the original price). The tier must have `cannotIncreaseDiscountPercent`
413
+ /// set appropriately.
414
+ /// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
413
415
  /// @param tierId The ID of the tier to set the discount of.
414
- /// @param discountPercent The discount percent to set.
416
+ /// @param discountPercent The discount percent to set (0–100).
415
417
  function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
416
418
  // Enforce permissions.
417
419
  _requirePermissionFrom({
@@ -420,8 +422,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
420
422
  _setDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
421
423
  }
422
424
 
423
- /// @notice Allows the collection's owner to set the discount percent for multiple tiers.
424
- /// @param configs The configs to set the discount percent for.
425
+ /// @notice Set discount percentages for multiple tiers in a single call.
426
+ /// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
427
+ /// @param configs An array of tier ID + discount percent pairs to apply.
425
428
  function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override {
426
429
  // Enforce permissions.
427
430
  _requirePermissionFrom({
@@ -440,7 +443,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
440
443
  }
441
444
  }
442
445
 
443
- /// @notice Update this hook's metadata properties.
446
+ /// @notice Update any combination of this hook's metadata properties in a single call. Pass empty strings or
447
+ /// sentinel values for fields you don't want to change.
444
448
  /// @dev Only this contract's owner or an operator with the `SET_721_METADATA` permission can set the metadata.
445
449
  /// @param name The new collection name. Send empty to leave unchanged.
446
450
  /// @param symbol The new collection symbol. Send empty to leave unchanged.
@@ -495,7 +499,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
495
499
  // `address(this)` is the sentinel value meaning "leave unchanged" (since `address(0)` clears the resolver).
496
500
  if (tokenUriResolver != IJB721TokenUriResolver(address(this))) {
497
501
  // Store the new URI resolver.
498
- // slither-disable-next-line reentrancy-events
499
502
  _recordSetTokenUriResolver(tokenUriResolver);
500
503
  }
501
504
  if (encodedIPFSUriTierId != 0 && encodedIPFSUri != bytes32(0)) {
@@ -510,8 +513,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
510
513
  // ----------------------- public transactions ----------------------- //
511
514
  //*********************************************************************//
512
515
 
513
- /// @notice Mint reserved pending reserved NFTs within the provided tier.
514
- /// @dev "Pending" means that the NFTs have been reserved, but have not been minted yet.
516
+ /// @notice Mint pending reserved NFTs from a specific tier. Anyone can call this — reserves are minted to the
517
+ /// tier's reserve beneficiary (or the hook's default). Reverts if the ruleset has reserve minting paused.
515
518
  /// @param tierId The ID of the tier to mint reserved NFTs from.
516
519
  /// @param count The number of reserved NFTs to mint.
517
520
  function mintPendingReservesFor(uint256 tierId, uint256 count) public override {
@@ -521,15 +524,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
521
524
  // Pending reserve mints must not be paused.
522
525
  if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused((JBRulesetMetadataResolver.metadata(ruleset))))
523
526
  {
524
- revert JB721TiersHook_MintReserveNftsPaused();
527
+ revert JB721TiersHook_MintReserveNftsPaused({projectId: PROJECT_ID, tierId: tierId});
525
528
  }
526
529
 
527
530
  // Record the reserved mint for the tier.
528
- // slither-disable-next-line reentrancy-events,calls-loop
529
531
  uint256[] memory tokenIds = STORE.recordMintReservesFor({tierId: tierId, count: count});
530
532
 
531
533
  // Keep a reference to the beneficiary.
532
- // slither-disable-next-line calls-loop
533
534
  address reserveBeneficiary = STORE.reserveBeneficiaryOf({hook: address(this), tierId: tierId});
534
535
 
535
536
  // Cache _msgSender() before the loop to avoid repeated calls.
@@ -542,7 +543,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
542
543
  emit MintReservedNft({tokenId: tokenId, tierId: tierId, beneficiary: reserveBeneficiary, caller: caller});
543
544
 
544
545
  // Mint the NFT.
545
- // slither-disable-next-line reentrency-events
546
546
  _mint({to: reserveBeneficiary, tokenId: tokenId});
547
547
 
548
548
  unchecked {
@@ -564,7 +564,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
564
564
  /// @param projectId The ID of the project to check.
565
565
  /// @return The project's current ruleset.
566
566
  function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) {
567
- // slither-disable-next-line calls-loop
568
567
  return RULESETS.currentOf(projectId);
569
568
  }
570
569
 
@@ -591,11 +590,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
591
590
  STORE.recordBurn(tokenIds);
592
591
  }
593
592
 
594
- /// @notice Mints NFTs and emits events for each.
595
- /// @param tokenIds The token IDs to mint.
596
- /// @param tierIds The tier IDs corresponding to each token.
593
+ /// @notice Mint a batch of NFTs to the beneficiary and emit a `Mint` event for each. Called after the store has
594
+ /// recorded the mint and generated token IDs.
595
+ /// @param tokenIds The token IDs to mint (generated by the store based on tier and sequence number).
596
+ /// @param tierIds The tier IDs corresponding to each token (same length and order as `tokenIds`).
597
597
  /// @param beneficiary The address receiving the NFTs.
598
- /// @param totalAmountPaid The amount to report in the Mint event.
598
+ /// @param totalAmountPaid The total payment amount (including credits) to report in the Mint event for indexing.
599
599
  function _mintTokens(
600
600
  uint256[] memory tokenIds,
601
601
  uint16[] memory tierIds,
@@ -616,7 +616,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
616
616
  caller: caller
617
617
  });
618
618
 
619
- // slither-disable-next-line reentrancy-events
620
619
  _mint({to: beneficiary, tokenId: tokenIds[i]});
621
620
 
622
621
  unchecked {
@@ -656,7 +655,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
656
655
  if (tokenIds.length != 0) {
657
656
  // totalAmountPaid is the full amount available before recordMint deducted tier prices.
658
657
  uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
659
- // slither-disable-next-line reentrancy-events
660
658
  _mintTokens({
661
659
  tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
662
660
  });
@@ -680,7 +678,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
680
678
  });
681
679
  }
682
680
 
683
- // slither-disable-next-line reentrancy-no-eth
684
681
  payCreditsOf[beneficiary] = newPayCredits;
685
682
  }
686
683
  }
@@ -735,30 +732,29 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
735
732
  }
736
733
  }
737
734
 
738
- /// @notice Record the setting of a new token URI resolver.
739
- /// @param tokenUriResolver The new token URI resolver.
735
+ /// @notice Emit the `SetTokenUriResolver` event and persist the new resolver in the store. Pass `address(0)` to
736
+ /// clear the resolver and fall back to the default IPFS-based URI.
737
+ /// @param tokenUriResolver The new token URI resolver (or address(0) to clear).
740
738
  function _recordSetTokenUriResolver(IJB721TokenUriResolver tokenUriResolver) internal {
741
739
  emit SetTokenUriResolver({resolver: tokenUriResolver, caller: _msgSender()});
742
740
 
743
741
  STORE.recordSetTokenUriResolver(tokenUriResolver);
744
742
  }
745
743
 
746
- /// @notice Internal function to set the discount percent for a tier.
744
+ /// @notice Delegate discount percent storage to the library, which validates and records it in the store.
747
745
  /// @param tierId The ID of the tier to set the discount percent for.
748
- /// @param discountPercent The discount percent to set for the tier.
746
+ /// @param discountPercent The discount percent to set (0 = no discount, up to DISCOUNT_DENOMINATOR = free).
749
747
  function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
750
- // slither-disable-next-line calls-loop
751
748
  JB721TiersHookLib.setDiscountPercentOf({
752
749
  store: STORE, tierId: tierId, discountPercent: discountPercent, caller: _msgSender()
753
750
  });
754
751
  }
755
752
 
756
753
  /// @notice Before transferring an NFT, register its first owner (if necessary).
757
- /// @param to The address the NFT is being transferred to.
758
- /// @param tokenId The token ID of the NFT being transferred.
754
+ /// @param to The address to transfer the NFT to.
755
+ /// @param tokenId The token ID of the NFT to transfer.
759
756
  function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) {
760
757
  // Get only the tier ID and transfersPausable flag (lightweight — avoids full struct construction).
761
- // slither-disable-next-line calls-loop
762
758
  (uint256 tierId, bool transfersPausable) =
763
759
  STORE.tierTransferInfoOfTokenId({hook: address(this), tokenId: tokenId});
764
760
 
@@ -778,26 +774,26 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
778
774
  && JB721TiersRulesetMetadataResolver.transfersPaused(
779
775
  (JBRulesetMetadataResolver.metadata(ruleset))
780
776
  )
781
- ) revert JB721TiersHook_TierTransfersPaused();
777
+ ) {
778
+ revert JB721TiersHook_TierTransfersPaused({
779
+ projectId: PROJECT_ID, tokenId: tokenId, from: from, to: to
780
+ });
781
+ }
782
782
  }
783
783
 
784
784
  // If the token isn't already associated with a first owner, store the sender as the first owner.
785
- // slither-disable-next-line calls-loop
786
785
  if (_firstOwnerOf[tokenId] == address(0)) _firstOwnerOf[tokenId] = from;
787
786
  }
788
787
 
789
788
  // Record the transfer.
790
- // slither-disable-next-line reentrency-events,calls-loop
791
789
  STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
792
790
 
793
791
  // Deploy the checkpoint module lazily on the first transfer.
794
792
  if (address(CHECKPOINTS) == address(0)) {
795
- // slither-disable-next-line calls-loop,reentrancy-events
796
793
  CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy(address(this));
797
794
  }
798
795
 
799
796
  // Notify the checkpoint module to update checkpointed voting power.
800
- // slither-disable-next-line calls-loop,reentrancy-events
801
797
  CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
802
798
  }
803
799
  }
@@ -13,7 +13,9 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
13
13
  import {JBDeploy721TiersHookConfig} from "./structs/JBDeploy721TiersHookConfig.sol";
14
14
 
15
15
  /// @title JB721TiersHookDeployer
16
- /// @notice Deploys a `JB721TiersHook` for an existing project.
16
+ /// @notice Factory that deploys EIP-1167 clones of `JB721TiersHook` for existing projects. Each clone is initialized
17
+ /// with its own tiers, metadata, and flags, then ownership is transferred to the caller. The deployed hook is
18
+ /// registered in the `IJBAddressRegistry` for cross-chain address verification.
17
19
  contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
18
20
  //*********************************************************************//
19
21
  // --------------- public immutable stored properties ---------------- //
@@ -22,7 +24,7 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
22
24
  /// @notice A registry which stores references to contracts and their deployers.
23
25
  IJBAddressRegistry public immutable ADDRESS_REGISTRY;
24
26
 
25
- /// @notice A 721 tiers hook.
27
+ /// @notice The reference 721 tiers hook implementation that gets cloned for each new deployment.
26
28
  JB721TiersHook public immutable HOOK;
27
29
 
28
30
  /// @notice The contract that stores and manages data for this contract's NFTs.
@@ -60,10 +62,11 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
60
62
  // ---------------------- external transactions ---------------------- //
61
63
  //*********************************************************************//
62
64
 
63
- /// @notice Deploys a 721 tiers hook for the specified project.
65
+ /// @notice Deploy a new 721 tiers hook for a project. Clones the implementation, initializes it with the provided
66
+ /// tiers and flags, transfers ownership to the caller, and registers the hook in the address registry.
64
67
  /// @param projectId The ID of the project to deploy the hook for.
65
- /// @param deployTiersHookConfig The config to deploy the hook with, which determines its behavior.
66
- /// @param salt A salt to use for the deterministic deployment.
68
+ /// @param deployTiersHookConfig The tiers, metadata, and flags to initialize the hook with.
69
+ /// @param salt A salt for deterministic (CREATE2) deployment. Pass `bytes32(0)` for non-deterministic deployment.
67
70
  /// @return newHook The address of the newly deployed hook.
68
71
  function deployHookFor(
69
72
  uint256 projectId,
@@ -100,7 +103,6 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
100
103
  JBOwnable(address(newHook)).transferOwnership(_msgSender());
101
104
 
102
105
  // Increment the nonce.
103
- // slither-disable-next-line reentrancy-benign
104
106
  ++_nonce;
105
107
 
106
108
  // Add the hook to the address registry. This contract's nonce starts at 1.