@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
package/foundry.lock CHANGED
@@ -1,11 +1,5 @@
1
1
  {
2
2
  "lib/forge-std": {
3
3
  "rev": "77876f8a5b44b770a935621bb331660c90ac928e"
4
- },
5
- "lib/sphinx": {
6
- "branch": {
7
- "name": "v0.23.0",
8
- "rev": "5fb24a825f46bd6ae0b5359fe0da1d2346126b09"
9
- }
10
4
  }
11
- }
5
+ }
package/foundry.toml CHANGED
@@ -14,7 +14,7 @@ depth = 100
14
14
  fail_on_revert = false
15
15
 
16
16
  [lint]
17
- exclude_lints = ["pascal-case-struct", "mixed-case-variable"]
17
+ exclude_lints = ["mixed-case-variable", "pascal-case-struct"]
18
18
  [fmt]
19
19
  number_underscore = "thousands"
20
20
  multiline_func_header = "all"
package/package.json CHANGED
@@ -1,11 +1,22 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.42",
3
+ "version": "0.0.45",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Bananapus/nana-721-hook-v6"
8
8
  },
9
+ "files": [
10
+ "CHANGELOG.md",
11
+ "foundry.lock",
12
+ "foundry.toml",
13
+ "references/",
14
+ "remappings.txt",
15
+ "script/",
16
+ "sphinx.lock",
17
+ "src/",
18
+ "test/utils/"
19
+ ],
9
20
  "engines": {
10
21
  "node": ">=20.0.0"
11
22
  },
@@ -17,15 +28,15 @@
17
28
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
18
29
  },
19
30
  "dependencies": {
20
- "@bananapus/address-registry-v6": "^0.0.20",
21
- "@bananapus/core-v6": "^0.0.36",
22
- "@bananapus/ownable-v6": "^0.0.20",
23
- "@bananapus/permission-ids-v6": "^0.0.19",
24
- "@openzeppelin/contracts": "^5.6.1",
25
- "@prb/math": "^4.1.0",
26
- "solady": "^0.1.8"
31
+ "@bananapus/address-registry-v6": "0.0.25",
32
+ "@bananapus/core-v6": "^0.0.39",
33
+ "@bananapus/ownable-v6": "^0.0.24",
34
+ "@bananapus/permission-ids-v6": "0.0.22",
35
+ "@openzeppelin/contracts": "5.6.1",
36
+ "@prb/math": "4.1.1",
37
+ "solady": "0.1.26"
27
38
  },
28
39
  "devDependencies": {
29
- "@sphinx-labs/plugins": "^0.33.2"
40
+ "@sphinx-labs/plugins": "0.33.3"
30
41
  }
31
42
  }
@@ -87,12 +87,12 @@ contract DeployScript is Script, Sphinx {
87
87
  (address _deployer, bool _deployerIsDeployed) = _isDeployed({
88
88
  salt: CHECKPOINTS_DEPLOYER_SALT,
89
89
  creationCode: type(JB721CheckpointsDeployer).creationCode,
90
- arguments: ""
90
+ arguments: abi.encode(store)
91
91
  });
92
92
 
93
93
  // Deploy it if it has not been deployed yet.
94
94
  checkpointsDeployer = !_deployerIsDeployed
95
- ? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}()
95
+ ? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}(store)
96
96
  : JB721CheckpointsDeployer(_deployer);
97
97
  }
98
98
 
@@ -1,9 +1,12 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
- import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
5
4
  import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
5
+ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6
+ import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
7
+
6
8
  import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
9
+ import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
7
10
  import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
8
11
 
9
12
  /// @title JB721Checkpoints
@@ -14,6 +17,8 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
14
17
  /// (`_cachedThis`) is uninitialized on clones, so `domainSeparatorV4()` always rebuilds using the clone's
15
18
  /// `address(this)` — correct behavior, tiny gas overhead.
16
19
  contract JB721Checkpoints is Votes, IJB721Checkpoints {
20
+ using Checkpoints for Checkpoints.Trace160;
21
+
17
22
  //*********************************************************************//
18
23
  // --------------------------- custom errors ------------------------- //
19
24
  //*********************************************************************//
@@ -22,55 +27,67 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
22
27
  error JB721Checkpoints_Unauthorized();
23
28
 
24
29
  //*********************************************************************//
25
- // --------------------- private stored properties ------------------ //
30
+ // --------------- public immutable stored properties ---------------- //
26
31
  //*********************************************************************//
27
32
 
28
- /// @notice Whether this contract has been initialized.
29
- bool private _initialized;
33
+ /// @notice The store that holds tier and voting data for the hook's NFTs.
34
+ IJB721TiersHookStore public immutable override STORE;
30
35
 
31
36
  //*********************************************************************//
32
- // ---------------------- public stored properties ------------------- //
37
+ // --------------------- public stored properties -------------------- //
33
38
  //*********************************************************************//
34
39
 
35
40
  /// @notice The hook that this module tracks voting power for.
36
41
  address public override HOOK;
37
42
 
38
- /// @notice The store that holds tier and voting data for the hook's NFTs.
39
- IJB721TiersHookStore public override STORE;
43
+ //*********************************************************************//
44
+ // -------------------- internal stored properties ------------------- //
45
+ //*********************************************************************//
46
+
47
+ /// @notice Checkpointed token owners for historical reward eligibility after first transfer.
48
+ /// @custom:param tokenId The token ID to get historical owner checkpoints for.
49
+ mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
40
50
 
41
51
  //*********************************************************************//
42
52
  // -------------------------- constructor ---------------------------- //
43
53
  //*********************************************************************//
44
54
 
45
- /// @dev Parameterless. The implementation contract is initialized in the constructor to prevent direct use.
46
- /// Clones are initialized via `initialize()`.
47
- constructor() EIP712("JB721Checkpoints", "1") {
48
- _initialized = true;
55
+ /// @dev The implementation contract is initialized in the constructor to prevent direct use. Clones are initialized
56
+ /// via `initialize()`.
57
+ /// @param store The store that holds tier data for each hook's NFTs.
58
+ constructor(IJB721TiersHookStore store) EIP712("JB721Checkpoints", "1") {
59
+ STORE = store;
60
+ HOOK = address(1);
49
61
  }
50
62
 
51
63
  //*********************************************************************//
52
64
  // ---------------------- external transactions ---------------------- //
53
65
  //*********************************************************************//
54
66
 
55
- /// @notice Initializes a cloned module with its hook and store references.
67
+ /// @notice Initializes a cloned module with its hook reference.
56
68
  /// @dev Can only be called once. Called by the deployer after cloning.
57
69
  /// @param hook The hook this module serves.
58
- /// @param store The store that holds tier data for the hook's NFTs.
59
- function initialize(address hook, IJB721TiersHookStore store) external override {
60
- if (_initialized) revert JB721Checkpoints_AlreadyInitialized();
61
- _initialized = true;
70
+ function initialize(address hook) external override {
71
+ if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized();
72
+ // `hook` cannot be zero when called through the deployer because `msg.sender` must equal `hook`.
73
+ // slither-disable-next-line missing-zero-check
62
74
  HOOK = hook;
63
- STORE = store;
64
75
  }
65
76
 
66
77
  /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
67
78
  /// @dev Only callable by the HOOK. Looks up the token's tier voting units from the store.
68
79
  /// @param from The previous owner (address(0) on mint).
69
80
  /// @param to The new owner (address(0) on burn).
70
- /// @param tokenId The token ID being transferred.
81
+ /// @param tokenId The token ID to transfer.
71
82
  function onTransfer(address from, address to, uint256 tokenId) external override {
72
83
  if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized();
73
84
 
85
+ if (from != address(0)) {
86
+ // forge-lint: disable-next-line(unsafe-typecast)
87
+ // slither-disable-next-line unused-return
88
+ _ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(to)});
89
+ }
90
+
74
91
  // Look up this token's tier to get its voting units.
75
92
  uint256 votingUnits = STORE.tierOfTokenId({hook: HOOK, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
76
93
 
@@ -79,7 +96,32 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
79
96
  }
80
97
 
81
98
  //*********************************************************************//
82
- // ------------------------ internal functions ----------------------- //
99
+ // ----------------------- external views ---------------------------- //
100
+ //*********************************************************************//
101
+
102
+ /// @notice The owner of an NFT at a past block.
103
+ /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
104
+ /// inferred from the hook's `firstOwnerOf`.
105
+ /// @param tokenId The token ID of the NFT to get the historical owner of.
106
+ /// @param blockNumber The block number to look up.
107
+ /// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
108
+ function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address) {
109
+ // forge-lint: disable-next-line(unsafe-typecast)
110
+ uint96 blockNumber96 = uint96(blockNumber);
111
+
112
+ Checkpoints.Trace160 storage checkpoints = _ownerCheckpointsOf[tokenId];
113
+ uint256 checkpointCount = checkpoints.length();
114
+
115
+ // Before the first transfer/burn checkpoint, the mint owner is implicit in the hook's first-owner tracking.
116
+ if (checkpointCount == 0 || checkpoints.at(0)._key > blockNumber96) {
117
+ return IJB721TiersHook(HOOK).firstOwnerOf(tokenId);
118
+ }
119
+
120
+ return address(uint160(checkpoints.upperLookupRecent(blockNumber96)));
121
+ }
122
+
123
+ //*********************************************************************//
124
+ // ----------------------- internal views ---------------------------- //
83
125
  //*********************************************************************//
84
126
 
85
127
  /// @notice Returns the total voting units held by an account (across all tiers).
@@ -2,6 +2,7 @@
2
2
  pragma solidity 0.8.28;
3
3
 
4
4
  import {LibClone} from "solady/src/utils/LibClone.sol";
5
+
5
6
  import {JB721Checkpoints} from "./JB721Checkpoints.sol";
6
7
  import {IJB721Checkpoints} from "./interfaces/IJB721Checkpoints.sol";
7
8
  import {IJB721CheckpointsDeployer} from "./interfaces/IJB721CheckpointsDeployer.sol";
@@ -19,12 +20,17 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
19
20
  /// @notice The checkpoint module implementation that clones delegate to.
20
21
  address public immutable override IMPLEMENTATION;
21
22
 
23
+ /// @notice The store that holds tier and voting data for each hook's NFTs.
24
+ IJB721TiersHookStore public immutable override STORE;
25
+
22
26
  //*********************************************************************//
23
27
  // -------------------------- constructor ---------------------------- //
24
28
  //*********************************************************************//
25
29
 
26
- constructor() {
27
- IMPLEMENTATION = address(new JB721Checkpoints());
30
+ /// @param store The store that holds tier data for each hook's NFTs.
31
+ constructor(IJB721TiersHookStore store) {
32
+ STORE = store;
33
+ IMPLEMENTATION = address(new JB721Checkpoints(store));
28
34
  }
29
35
 
30
36
  //*********************************************************************//
@@ -34,14 +40,13 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
34
40
  /// @notice Deploys a new deterministic checkpoint clone for the given hook.
35
41
  /// @dev Uses CREATE2 with the hook address as salt so the clone address is the same across chains.
36
42
  /// @param hook The hook address the module will serve.
37
- /// @param store The store that holds tier data for the hook's NFTs.
38
43
  /// @return module The newly deployed and initialized checkpoint module.
39
- function deploy(address hook, IJB721TiersHookStore store) external override returns (IJB721Checkpoints module) {
44
+ function deploy(address hook) external override returns (IJB721Checkpoints module) {
40
45
  if (msg.sender != hook) revert JB721CheckpointsDeployer_Unauthorized();
41
46
 
42
47
  module = IJB721Checkpoints(
43
48
  LibClone.cloneDeterministic({implementation: IMPLEMENTATION, salt: bytes32(uint256(uint160(hook)))})
44
49
  );
45
- module.initialize({hook: hook, store: store});
50
+ module.initialize(hook);
46
51
  }
47
52
  }
@@ -151,18 +151,19 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
151
151
  // ------------------------- external views -------------------------- //
152
152
  //*********************************************************************//
153
153
 
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.
154
+ /// @notice The address that originally received an NFT (typically the payer). Tracked separately from the current
155
+ /// owner so it persists through transfers, useful for provenance and historical voting checkpoints.
156
+ /// @param tokenId The token ID of the NFT.
157
157
  /// @return The address of the NFT's first owner.
158
158
  function firstOwnerOf(uint256 tokenId) external view override returns (address) {
159
159
  address first = _firstOwnerOf[tokenId];
160
160
  return first != address(0) ? first : _ownerOf(tokenId);
161
161
  }
162
162
 
163
- /// @notice Context for the pricing of this hook's tiers.
163
+ /// @notice The currency and decimal precision used for this hook's tier prices. For example, if tiers are priced
164
+ /// in ETH with 18 decimals, `currency` would be the ETH currency ID and `decimals` would be 18.
164
165
  /// @return currency The currency used for tier prices.
165
- /// @return decimals The amount of decimals being used in tier prices.
166
+ /// @return decimals The number of decimals used in tier prices.
166
167
  function pricingContext() external view override returns (uint256 currency, uint256 decimals) {
167
168
  // Get a reference to the packed pricing context.
168
169
  uint256 packed = _packedPricingContext;
@@ -185,12 +186,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
185
186
  return STORE.balanceOf({hook: address(this), owner: owner});
186
187
  }
187
188
 
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.
189
+ /// @notice Called by the terminal before recording a payment. Calculates how much of the payment should be routed
190
+ /// to tier-based splits vs. kept by the project, and adjusts the minting weight accordingly.
191
+ /// @dev Overrides the base to compute tier split amounts from each tier's `splitPercent`.
192
+ /// @param context The payment context from the terminal.
193
+ /// @return weight The adjusted weight for project token minting (reduced when splits route funds away).
194
+ /// @return hookSpecifications Specifies this hook as the pay hook, with the split amount to forward.
194
195
  function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
195
196
  public
196
197
  view
@@ -222,17 +223,20 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
222
223
  });
223
224
  }
224
225
 
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.
226
+ /// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeight()` to get the fraction of
227
+ /// surplus these NFTs can reclaim. Weight is based on the original tier price, not any discount paid.
228
+ /// @param tokenIds The token IDs of the NFTs to get the combined cash-out weight of.
229
+ /// @return weight The combined cash-out weight.
230
230
  function cashOutWeightOf(uint256[] memory tokenIds) public view virtual override returns (uint256) {
231
231
  return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
232
232
  }
233
233
 
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.
234
+ /// @notice Initialize a cloned copy of the hook. Sets the project association, ERC-721 name/symbol, pricing
235
+ /// context (currency + decimals), metadata URIs, initial tiers, and behavioral flags. Can only be called once
236
+ /// per clone — the implementation contract is pre-initialized in its constructor to prevent misuse.
237
+ /// @dev Called by `JB721TiersHookDeployer` immediately after cloning. Reverts with
238
+ /// `JB721TiersHook_AlreadyInitialized` if called more than once, or `JB721TiersHook_NoProjectId` if projectId is 0.
239
+ /// @param projectId The ID of the project this hook is associated with.
236
240
  /// @param name The name of the NFT collection.
237
241
  /// @param symbol The symbol representing the NFT collection.
238
242
  /// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
@@ -328,9 +332,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
328
332
  return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
329
333
  }
330
334
 
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.
335
+ /// @notice The total cash-out weight across all outstanding NFTs and pending reserves. This is the denominator
336
+ /// for cash-out calculations — an NFT's share of the surplus is its weight divided by this total.
337
+ /// @return weight The total cash-out weight.
334
338
  function totalCashOutWeight() public view virtual override returns (uint256) {
335
339
  return STORE.totalCashOutWeight(address(this));
336
340
  }
@@ -339,12 +343,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
339
343
  // ---------------------- external transactions ---------------------- //
340
344
  //*********************************************************************//
341
345
 
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.
346
+ /// @notice Add new NFT tiers or remove existing ones. Added tiers get sequential IDs and must be sorted by
347
+ /// category. Removed tiers stop accepting new mints but existing NFTs remain valid.
348
+ /// @dev Only the collection owner or an operator with `ADJUST_721_TIERS` permission can call this.
349
+ /// @dev Added tiers must respect this hook's flags (e.g. `noNewTiersWithVotes`, `noNewTiersWithReserves`).
350
+ /// @param tiersToAdd The tiers to add, as an array of `JB721TierConfig` structs.
351
+ /// @param tierIdsToRemove The IDs of the tiers to remove.
348
352
  function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override {
349
353
  // Enforce permissions.
350
354
  _requirePermissionFrom({
@@ -363,9 +367,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
363
367
  });
364
368
  }
365
369
 
366
- /// @notice Manually mint NFTs from the provided tiers .
370
+ /// @notice Manually mint NFTs from specific tiers to a beneficiary, without requiring payment. Only tiers with
371
+ /// `allowOwnerMint` enabled can be minted this way.
372
+ /// @dev Only the collection owner or an operator with `MINT_721` permission can call this.
367
373
  /// @param tierIds The IDs of the tiers to mint from.
368
- /// @param beneficiary The address to mint to.
374
+ /// @param beneficiary The address to mint the NFTs to.
369
375
  /// @return tokenIds The IDs of the newly minted tokens.
370
376
  function mintFor(
371
377
  uint16[] calldata tierIds,
@@ -389,9 +395,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
389
395
  _mintTokens({tokenIds: tokenIds, tierIds: tierIds, beneficiary: beneficiary, totalAmountPaid: 0});
390
396
  }
391
397
 
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.
398
+ /// @notice Mint pending reserved NFTs across multiple tiers in a single call. Reserves accumulate automatically
399
+ /// as NFTs are sold (based on each tier's `reserveFrequency`) and anyone can trigger their minting.
400
+ /// @param reserveMintConfigs The tier IDs and counts specifying how many reserves to mint from each tier.
395
401
  function mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs) external override {
396
402
  for (uint256 i; i < reserveMintConfigs.length;) {
397
403
  // Get a reference to the params being iterated upon.
@@ -406,12 +412,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
406
412
  }
407
413
  }
408
414
 
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.
415
+ /// @notice Set a discount on a tier's price. Discounts reduce the price payers must pay, but don't affect the
416
+ /// NFT's cash-out weight (which always uses the original price). The tier must have `cannotIncreaseDiscountPercent`
417
+ /// set appropriately.
418
+ /// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
413
419
  /// @param tierId The ID of the tier to set the discount of.
414
- /// @param discountPercent The discount percent to set.
420
+ /// @param discountPercent The discount percent to set (0–100).
415
421
  function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
416
422
  // Enforce permissions.
417
423
  _requirePermissionFrom({
@@ -420,8 +426,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
420
426
  _setDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
421
427
  }
422
428
 
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.
429
+ /// @notice Set discount percentages for multiple tiers in a single call.
430
+ /// @dev Only the collection owner or an operator with `SET_721_DISCOUNT_PERCENT` permission can call this.
431
+ /// @param configs An array of tier ID + discount percent pairs to apply.
425
432
  function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override {
426
433
  // Enforce permissions.
427
434
  _requirePermissionFrom({
@@ -440,7 +447,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
440
447
  }
441
448
  }
442
449
 
443
- /// @notice Update this hook's metadata properties.
450
+ /// @notice Update any combination of this hook's metadata properties in a single call. Pass empty strings or
451
+ /// sentinel values for fields you don't want to change.
444
452
  /// @dev Only this contract's owner or an operator with the `SET_721_METADATA` permission can set the metadata.
445
453
  /// @param name The new collection name. Send empty to leave unchanged.
446
454
  /// @param symbol The new collection symbol. Send empty to leave unchanged.
@@ -510,8 +518,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
510
518
  // ----------------------- public transactions ----------------------- //
511
519
  //*********************************************************************//
512
520
 
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.
521
+ /// @notice Mint pending reserved NFTs from a specific tier. Anyone can call this — reserves are minted to the
522
+ /// tier's reserve beneficiary (or the hook's default). Reverts if the ruleset has reserve minting paused.
515
523
  /// @param tierId The ID of the tier to mint reserved NFTs from.
516
524
  /// @param count The number of reserved NFTs to mint.
517
525
  function mintPendingReservesFor(uint256 tierId, uint256 count) public override {
@@ -591,11 +599,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
591
599
  STORE.recordBurn(tokenIds);
592
600
  }
593
601
 
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.
602
+ /// @notice Mint a batch of NFTs to the beneficiary and emit a `Mint` event for each. Called after the store has
603
+ /// recorded the mint and generated token IDs.
604
+ /// @param tokenIds The token IDs to mint (generated by the store based on tier and sequence number).
605
+ /// @param tierIds The tier IDs corresponding to each token (same length and order as `tokenIds`).
597
606
  /// @param beneficiary The address receiving the NFTs.
598
- /// @param totalAmountPaid The amount to report in the Mint event.
607
+ /// @param totalAmountPaid The total payment amount (including credits) to report in the Mint event for indexing.
599
608
  function _mintTokens(
600
609
  uint256[] memory tokenIds,
601
610
  uint16[] memory tierIds,
@@ -656,6 +665,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
656
665
  if (tokenIds.length != 0) {
657
666
  // totalAmountPaid is the full amount available before recordMint deducted tier prices.
658
667
  uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
668
+ // slither-disable-next-line reentrancy-events
659
669
  _mintTokens({
660
670
  tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
661
671
  });
@@ -734,17 +744,18 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
734
744
  }
735
745
  }
736
746
 
737
- /// @notice Record the setting of a new token URI resolver.
738
- /// @param tokenUriResolver The new token URI resolver.
747
+ /// @notice Emit the `SetTokenUriResolver` event and persist the new resolver in the store. Pass `address(0)` to
748
+ /// clear the resolver and fall back to the default IPFS-based URI.
749
+ /// @param tokenUriResolver The new token URI resolver (or address(0) to clear).
739
750
  function _recordSetTokenUriResolver(IJB721TokenUriResolver tokenUriResolver) internal {
740
751
  emit SetTokenUriResolver({resolver: tokenUriResolver, caller: _msgSender()});
741
752
 
742
753
  STORE.recordSetTokenUriResolver(tokenUriResolver);
743
754
  }
744
755
 
745
- /// @notice Internal function to set the discount percent for a tier.
756
+ /// @notice Delegate discount percent storage to the library, which validates and records it in the store.
746
757
  /// @param tierId The ID of the tier to set the discount percent for.
747
- /// @param discountPercent The discount percent to set for the tier.
758
+ /// @param discountPercent The discount percent to set (0 = no discount, up to DISCOUNT_DENOMINATOR = free).
748
759
  function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
749
760
  // slither-disable-next-line calls-loop
750
761
  JB721TiersHookLib.setDiscountPercentOf({
@@ -753,8 +764,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
753
764
  }
754
765
 
755
766
  /// @notice Before transferring an NFT, register its first owner (if necessary).
756
- /// @param to The address the NFT is being transferred to.
757
- /// @param tokenId The token ID of the NFT being transferred.
767
+ /// @param to The address to transfer the NFT to.
768
+ /// @param tokenId The token ID of the NFT to transfer.
758
769
  function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) {
759
770
  // Get only the tier ID and transfersPausable flag (lightweight — avoids full struct construction).
760
771
  // slither-disable-next-line calls-loop
@@ -791,10 +802,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
791
802
 
792
803
  // Deploy the checkpoint module lazily on the first transfer.
793
804
  if (address(CHECKPOINTS) == address(0)) {
794
- CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy({hook: address(this), store: STORE});
805
+ // slither-disable-next-line calls-loop,reentrancy-events
806
+ CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy(address(this));
795
807
  }
796
808
 
797
809
  // Notify the checkpoint module to update checkpointed voting power.
810
+ // slither-disable-next-line calls-loop,reentrancy-events
798
811
  CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
799
812
  }
800
813
  }
@@ -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,