@bananapus/721-hook-v6 0.0.14 → 0.0.16

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 (58) hide show
  1. package/ADMINISTRATION.md +4 -3
  2. package/ARCHITECTURE.md +2 -2
  3. package/README.md +2 -2
  4. package/RISKS.md +4 -3
  5. package/SKILLS.md +12 -12
  6. package/STYLE_GUIDE.md +14 -1
  7. package/package.json +5 -5
  8. package/script/Deploy.s.sol +11 -2
  9. package/script/helpers/Hook721DeploymentLib.sol +8 -1
  10. package/src/JB721TiersHook.sol +149 -123
  11. package/src/JB721TiersHookProjectDeployer.sol +4 -3
  12. package/src/JB721TiersHookStore.sol +8 -1
  13. package/src/abstract/ERC721.sol +38 -19
  14. package/src/abstract/JB721Hook.sol +5 -1
  15. package/src/interfaces/IJB721TiersHook.sol +22 -3
  16. package/src/interfaces/IJB721TiersHookStore.sol +3 -0
  17. package/src/libraries/JB721TiersHookLib.sol +156 -34
  18. package/src/libraries/JB721TiersRulesetMetadataResolver.sol +4 -1
  19. package/src/libraries/JBBitmap.sol +1 -0
  20. package/src/libraries/JBIpfsDecoder.sol +4 -1
  21. package/src/structs/JB721InitTiersConfig.sol +1 -5
  22. package/src/structs/JB721Tier.sol +2 -0
  23. package/src/structs/JB721TierConfig.sol +2 -0
  24. package/src/structs/JB721TiersHookFlags.sol +1 -0
  25. package/src/structs/JB721TiersMintReservesConfig.sol +1 -0
  26. package/src/structs/JB721TiersRulesetMetadata.sol +1 -0
  27. package/src/structs/JB721TiersSetDiscountPercentConfig.sol +1 -0
  28. package/src/structs/JBBitmapWord.sol +1 -0
  29. package/src/structs/JBDeploy721TiersHookConfig.sol +1 -0
  30. package/src/structs/JBLaunchProjectConfig.sol +1 -0
  31. package/src/structs/JBLaunchRulesetsConfig.sol +1 -0
  32. package/src/structs/JBPayDataHookRulesetConfig.sol +1 -0
  33. package/src/structs/JBPayDataHookRulesetMetadata.sol +4 -0
  34. package/src/structs/JBQueueRulesetsConfig.sol +1 -0
  35. package/src/structs/JBStored721Tier.sol +1 -0
  36. package/test/721HookAttacks.t.sol +1 -0
  37. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +30 -12
  38. package/test/Fork.t.sol +60 -11
  39. package/test/fork/ERC20TierSplitFork.t.sol +51 -9
  40. package/test/invariants/TierLifecycleInvariant.t.sol +3 -0
  41. package/test/invariants/TieredHookStoreInvariant.t.sol +5 -0
  42. package/test/invariants/handlers/TierLifecycleHandler.sol +32 -0
  43. package/test/invariants/handlers/TierStoreHandler.sol +3 -0
  44. package/test/regression/CacheTierLookup.t.sol +2 -0
  45. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +1 -0
  46. package/test/regression/SplitNoBeneficiary.t.sol +2 -0
  47. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +3 -0
  48. package/test/unit/JBBitmap.t.sol +1 -0
  49. package/test/unit/JBIpfsDecoder.t.sol +5 -0
  50. package/test/unit/TierSupplyReserveCheck.t.sol +1 -0
  51. package/test/unit/adjustTier_Unit.t.sol +53 -34
  52. package/test/unit/deployer_Unit.t.sol +11 -0
  53. package/test/unit/getters_constructor_Unit.t.sol +54 -27
  54. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +25 -0
  55. package/test/unit/pay_CrossCurrency_Unit.t.sol +54 -23
  56. package/test/unit/pay_Unit.t.sol +56 -13
  57. package/test/unit/redeem_Unit.t.sol +10 -0
  58. package/test/unit/tierSplitRouting_Unit.t.sol +13 -2
package/ADMINISTRATION.md CHANGED
@@ -38,7 +38,7 @@ Admin privileges and their scope in nana-721-hook-v6.
38
38
  | `mintFor()` (line 338) | `MINT_721` | `owner()` | Manually mints NFTs from tiers that have `allowOwnerMint` enabled. Bypasses price checks (passes `type(uint256).max` as amount). |
39
39
  | `setDiscountPercentOf()` (line 389) | `SET_721_DISCOUNT_PERCENT` | `owner()` | Sets the discount percentage for a single tier. |
40
40
  | `setDiscountPercentsOf()` (line 399) | `SET_721_DISCOUNT_PERCENT` | `owner()` | Batch-sets discount percentages for multiple tiers. |
41
- | `setMetadata()` (line 420) | `SET_721_METADATA` | `owner()` | Updates baseURI, contractURI, tokenUriResolver, and/or per-tier encoded IPFS URIs. |
41
+ | `setMetadata()` (line 430) | `SET_721_METADATA` | `owner()` | Updates collection name, symbol, baseURI, contractURI, tokenUriResolver, and/or per-tier encoded IPFS URIs. Empty strings leave values unchanged. |
42
42
  | `initialize()` (line 223) | None (one-time) | `PROJECT_ID == 0` check | Initializes a cloned hook. Can only be called once. Transfers ownership to caller on completion. |
43
43
 
44
44
  ### JB721TiersHookProjectDeployer
@@ -108,12 +108,13 @@ The following are set at deploy/initialization time and **cannot be changed afte
108
108
  | Property | Set In | Scope |
109
109
  |----------|--------|-------|
110
110
  | `DIRECTORY` | Constructor | Which terminal/controller directory is trusted |
111
+ | `PRICES` | Constructor | Which prices contract is used for cross-currency conversions |
111
112
  | `RULESETS` | Constructor | Which rulesets contract is consulted |
112
113
  | `STORE` | Constructor | Which store manages tier data |
113
114
  | `SPLITS` | Constructor | Which splits contract manages tier split groups |
114
115
  | `METADATA_ID_TARGET` | Constructor | The address used for metadata ID derivation (original implementation address for clones) |
115
116
  | `PROJECT_ID` | `initialize()` | Which project this hook belongs to |
116
- | Pricing context (currency, decimals, prices contract) | `initialize()` | Packed into `_packedPricingContext` -- the token denomination for tier prices |
117
+ | Pricing context (currency, decimals) | `initialize()` | Packed into `_packedPricingContext` -- the token denomination for tier prices |
117
118
  | `JB721TiersHookFlags` | `initialize()` | `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending`, `issueTokensForSplits` |
118
119
  | Per-tier `cannotBeRemoved` | `recordAddTiers()` | Whether a tier can be soft-removed |
119
120
  | Per-tier `cannotIncreaseDiscountPercent` | `recordAddTiers()` | Whether a tier's discount can be increased |
@@ -145,7 +146,7 @@ What the hook owner **cannot** do:
145
146
  - **Cannot increase a tier's discount if `cannotIncreaseDiscountPercent` is set.** The store enforces this in `recordSetDiscountPercentOf()` (line 1176).
146
147
  - **Cannot mint from tiers without `allowOwnerMint`.** The `mintFor()` function passes `isOwnerMint: true` to the store, which checks the flag (line 1060).
147
148
  - **Cannot re-initialize a hook.** The `initialize()` function reverts if `PROJECT_ID != 0` (line 237).
148
- - **Cannot change the pricing currency or decimals.** The `_packedPricingContext` is set once during initialization.
149
+ - **Cannot change the pricing currency, decimals, or prices contract.** `PRICES` is immutable (set in constructor), and the currency/decimals in `_packedPricingContext` are set once during initialization.
149
150
  - **Cannot bypass the flag restrictions.** Once `noNewTiersWithReserves`, `noNewTiersWithVotes`, or `noNewTiersWithOwnerMinting` are set, all future tiers added via `adjustTiers()` must comply.
150
151
  - **Cannot mint more reserves than the formula allows.** Reserve mints are bounded by `ceil(nonReserveMints / reserveFrequency)`.
151
152
  - **Cannot modify the split groups outside of `adjustTiers()`.** Tier split groups are set during tier addition via the library; there is no separate admin function to change them directly on the hook (though the project owner could call `JBSplits.setSplitGroupsOf()` directly if they have the appropriate permission).
package/ARCHITECTURE.md CHANGED
@@ -16,7 +16,7 @@ src/
16
16
  │ ├── JB721Hook.sol — Base ERC-721 + pay/cashout hook integration
17
17
  │ └── ERC721.sol — Minimal ERC-721 implementation
18
18
  ├── libraries/
19
- │ └── JB721TiersHookLib.sol — Tier packing/unpacking helpers
19
+ │ └── JB721TiersHookLib.sol — Split calculations, price normalization, weight math, fund distribution
20
20
  ├── interfaces/ — All interfaces (IJB721TiersHook, etc.)
21
21
  └── structs/ — Tier config, mint context, cash-out structs
22
22
  ```
@@ -29,7 +29,7 @@ User → JBMultiTerminal.pay(metadata)
29
29
  → beforePayRecordedWith()
30
30
  → calculateSplitAmounts(): per-tier split amounts (in tier pricing denomination)
31
31
  → convertSplitAmounts(): convert to payment token denomination (if currencies differ)
32
- Adjust weight down by split fraction
32
+ calculateWeight(): adjust weight down by split fraction
33
33
  → JBTerminalStore records payment
34
34
  → afterPayRecordedWith()
35
35
  → Decode tier IDs from metadata
package/README.md CHANGED
@@ -162,7 +162,7 @@ nana-721-hook/
162
162
  │ ├── JB721TiersHookStore.sol - Stores and manages data for tiered NFT hooks.
163
163
  │ ├── abstract/
164
164
  │ │ ├── JB721Hook.sol - Abstract base hook: handles pay/cash out lifecycle, metadata, and terminal validation.
165
- │ │ └── ERC721.sol - Clone-compatible abstract ERC-721 implementation.
165
+ │ │ └── ERC721.sol - Clone-compatible abstract ERC-721 implementation with mutable name/symbol.
166
166
  │ ├── interfaces/ - Contract interfaces.
167
167
  │ ├── libraries/ - Libraries (includes JB721TiersHookLib for tier adjustments, split distribution, price normalization, and token URI resolution).
168
168
  │ └── structs/ - Structs.
@@ -241,7 +241,7 @@ Each tier has the following properties:
241
241
  Additional notes:
242
242
 
243
243
  - A payer can specify any number of tiers to mint as long as the total price does not exceed the amount being paid. If tiers aren't specified, the leftover amount is stored as pay credits (if allowed).
244
- - If the payment and a tier's price are specified in different currencies, the `JBPrices` contract is used to normalize the values. If no `JBPrices` contract is set and the currencies differ, the payment is silently ignored (no mint, no revert).
244
+ - If the payment and a tier's price are specified in different currencies, the hook's immutable `PRICES` contract is used to normalize the values. If `PRICES` is the zero address and the currencies differ, the payment is silently ignored (no mint, no revert).
245
245
  - If some of a payment does not go towards purchasing an NFT, those extra funds will be stored as "NFT credits" which can be used for future purchases. Credits are only combined with the payment when `payer == beneficiary`. Optionally, the hook can disallow credits and reject payments with leftover funds (via `preventOverspending`).
246
246
  - If enabled by the project owner, holders can burn their NFTs to reclaim funds from the project. These cash outs are proportional to the NFTs price, relative to the combined price of all the NFTs (including pending reserves in the denominator).
247
247
  - NFT cash outs can be enabled by setting `useDataHookForCashOut` to `true` in the project's `JBRulesetMetadata`. If NFT cash outs are enabled, project token cash outs are disabled -- attempting to cash out fungible tokens when the data hook is active will revert.
package/RISKS.md CHANGED
@@ -6,13 +6,13 @@ Deep implementation-level risk analysis covering all contracts in the 721 tiered
6
6
 
7
7
  ## Trust Assumptions
8
8
 
9
- 1. **Project Owner / Hook Owner** -- Can adjust tiers (add/remove), set metadata, set discount percent, manually mint from `allowOwnerMint` tiers, and configure hook flags. Full control over NFT economics within the boundaries enforced by immutable per-tier flags.
9
+ 1. **Project Owner / Hook Owner** -- Can adjust tiers (add/remove), set metadata (including collection name and symbol), set discount percent, manually mint from `allowOwnerMint` tiers, and configure hook flags. Full control over NFT economics within the boundaries enforced by immutable per-tier flags.
10
10
  2. **Core Protocol (JBMultiTerminal)** -- The hook trusts that `afterPayRecordedWith()` and `afterCashOutRecordedWith()` are only called by a registered terminal. Verification at `JB721Hook.sol` lines 194-197 and 236-237 via `DIRECTORY.isTerminalOf()`.
11
11
  3. **JBDirectory** -- Trusted to correctly report terminal registrations. If compromised, arbitrary addresses could call pay/cashout hooks.
12
12
  4. **JBSplits** -- Trusted to store and return correct split configurations for tier split groups. The hook delegates split group management to this contract.
13
13
  5. **Token URI Resolver** -- If set, controls all NFT metadata rendering. Cannot affect funds but can misrepresent NFT properties. Set via `SET_721_METADATA` permission.
14
14
  6. **Store Contract** -- `JB721TiersHookStore` manages all tier state using a `msg.sender`-keyed trust model. The hook delegates pricing, supply, and reserve logic to the store.
15
- 7. **JBPrices** -- If a prices contract is configured (for cross-currency payments), the hook trusts it for price conversion. A reverting price feed will block all payments in non-native currencies.
15
+ 7. **JBPrices** -- The hook's immutable `PRICES` contract (set in the constructor, shared across all clones) is trusted for cross-currency price conversion. A reverting price feed will block all payments in non-native currencies.
16
16
 
17
17
  ---
18
18
 
@@ -41,7 +41,7 @@ Deep implementation-level risk analysis covering all contracts in the 721 tiered
41
41
  - **Severity**: MEDIUM
42
42
  - **Location**: `JB721TiersHookLib.sol` lines 265-285 (`_distributeSingleSplit`) and lines 312-315 (`_sendPayoutToSplit`)
43
43
  - **Description**: During `afterPayRecordedWith()`, if tiers have `splitPercent > 0`, the hook distributes forwarded funds to split beneficiaries. For native token splits, this involves a low-level `.call{value: amount}("")` to the beneficiary address (line 314). This is an external call to an untrusted address during payment processing.
44
- - **Reentrancy path**: `afterPayRecordedWith` -> `_processPayment` -> `distributeAll` -> `_distributeSingleSplit` -> `_sendPayoutToSplit` -> `beneficiary.call{value}` -- the beneficiary could reenter the hook.
44
+ - **Reentrancy path**: `afterPayRecordedWith` -> `_processPayment` -> `distributeAll` (DELEGATECALL) -> `safeTransferFrom` (ERC-20 pull) -> `_distributeSingleSplit` -> `_sendPayoutToSplit` -> `beneficiary.call{value}` -- the beneficiary could reenter the hook.
45
45
  - **Why it is mitigated**: The NFT mint (`_mintAll`) happens BEFORE split distribution (line 646 vs. line 678 in `JB721TiersHook.sol`). The store's `recordMint` has already decremented supply. Pay credits are already updated. A reentrant call to `afterPayRecordedWith` would require terminal authorization and would process as a separate independent payment.
46
46
  - **Tested**: PARTIALLY -- Split distribution is tested in `test/unit/tierSplitRouting_Unit.t.sol` and `test/regression/L36_SplitNoBeneficiary.t.sol`, but no explicit reentrancy test exists for the `.call{value}` path.
47
47
  - **Mitigation**: State is settled before external calls. The terminal authorization check prevents casual reentrancy. No explicit `ReentrancyGuard` is used.
@@ -203,6 +203,7 @@ An attacker could observe a large cash-out and front-run it with their own cash-
203
203
  - `STORE.recordMint()` (cross-contract, trusted)
204
204
  - `_mint()` (internal, no receiver callback)
205
205
  - `JB721TiersHookLib.distributeAll()` via DELEGATECALL:
206
+ - `SafeERC20.safeTransferFrom()` (ERC-20 token pull from terminal)
206
207
  - `SPLITS.splitsOf()` (cross-contract, trusted)
207
208
  - `split.beneficiary.call{value}()` (untrusted external call)
208
209
  - `terminal.pay()` (cross-contract, semi-trusted)
package/SKILLS.md CHANGED
@@ -15,13 +15,13 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
15
15
  | `JB721TiersHookProjectDeployer` | Convenience deployer: creates a Juicebox project + hook in one transaction. Also supports `launchRulesetsFor` and `queueRulesetsOf`. Wires the hook as the data hook with `useDataHookForPay: true`. |
16
16
  | `JB721TiersHookLib` (library) | External library called via DELEGATECALL from the hook. Handles tier adjustments (`adjustTiersFor`), split amount calculation (`calculateSplitAmounts`), split fund distribution (`distributeAll`), price normalization (`normalizePaymentValue`), and token URI resolution (`resolveTokenURI`). Extracted to stay within EIP-170 contract size limit. |
17
17
  | `IJB721Hook` (interface) | Interface for `JB721Hook`: extends `IJBRulesetDataHook`, `IJBPayHook`, `IJBCashOutHook`. Declares `DIRECTORY()`, `METADATA_ID_TARGET()`, `PROJECT_ID()`. |
18
- | `ERC721` (abstract) | Clone-compatible ERC-721 with `_initialize(name, symbol)` instead of constructor args. |
18
+ | `ERC721` (abstract) | Clone-compatible ERC-721 with `_initialize(name, symbol)` instead of constructor args. Exposes `_setName()` and `_setSymbol()` for post-initialization updates. |
19
19
 
20
20
  ## Key Functions
21
21
 
22
22
  | Function | Contract | What it does |
23
23
  |----------|----------|--------------|
24
- | `initialize(projectId, name, symbol, baseUri, tokenUriResolver, contractUri, tiersConfig, flags)` | `JB721TiersHook` | One-time setup for a cloned hook instance. Stores pricing context (currency, decimals, prices contract packed into uint256), records tiers and flags in the store. Registers any configured tier splits in `JBSplits` via `SPLITS.setSplitGroupsOf`. Validates `decimals <= 18`. |
24
+ | `initialize(projectId, name, symbol, baseUri, tokenUriResolver, contractUri, tiersConfig, flags)` | `JB721TiersHook` | One-time setup for a cloned hook instance. Stores pricing context (currency, decimals, and the immutable `PRICES` contract packed into uint256), records tiers and flags in the store. Registers any configured tier splits in `JBSplits` via `SPLITS.setSplitGroupsOf`. Validates `decimals <= 18`. |
25
25
  | `afterPayRecordedWith(context)` | `JB721Hook` | Called by terminal after payment. Validates caller is a project terminal, delegates to virtual `_processPayment`. |
26
26
  | `_processPayment(context)` | `JB721TiersHook` | Normalizes payment value via pricing context, decodes payer metadata for tier IDs to mint, calls `_mintAll`, manages pay credits for overspending. Distributes tier split funds via `JB721TiersHookLib.distributeAll` if split amounts were forwarded. |
27
27
  | `afterCashOutRecordedWith(context)` | `JB721Hook` | Called by terminal during cash out. Decodes token IDs from metadata, validates ownership, burns NFTs, delegates to virtual `_didBurn`. Reverts if `msg.value != 0`. |
@@ -31,12 +31,12 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
31
31
  | `mintFor(tierIds, beneficiary)` | `JB721TiersHook` | Owner-only manual mint. Requires `MINT_721` permission. Passes `amount: type(uint256).max` and `isOwnerMint: true` to force the mint. |
32
32
  | `mintPendingReservesFor(tierId, count)` | `JB721TiersHook` | Public. Mints pending reserve NFTs for a tier to the tier's `reserveBeneficiary`. Checks ruleset metadata for `mintPendingReservesPaused`. |
33
33
  | `mintPendingReservesFor(configs[])` | `JB721TiersHook` | Batch variant. Calls `mintPendingReservesFor(tierId, count)` for each config. |
34
- | `setMetadata(baseUri, contractUri, tokenUriResolver, encodedIPFSUriTierId, encodedIPFSUri)` | `JB721TiersHook` | Owner-only. Updates base URI, contract URI, token URI resolver, or per-tier encoded IPFS URI. Requires `SET_721_METADATA` permission. |
34
+ | `setMetadata(name, symbol, baseUri, contractUri, tokenUriResolver, encodedIPFSUriTierId, encodedIPFSUri)` | `JB721TiersHook` | Owner-only. Updates collection name, symbol, base URI, contract URI, token URI resolver, or per-tier encoded IPFS URI. Empty strings leave values unchanged. Requires `SET_721_METADATA` permission. |
35
35
  | `setDiscountPercentOf(tierId, discountPercent)` | `JB721TiersHook` | Owner-only. Sets discount percent for a tier. Requires `SET_721_DISCOUNT_PERCENT` permission. |
36
36
  | `setDiscountPercentsOf(configs[])` | `JB721TiersHook` | Batch variant. Sets discount percent for multiple tiers. Requires `SET_721_DISCOUNT_PERCENT` permission. |
37
37
  | `tokenURI(tokenId)` | `JB721TiersHook` | Resolves token metadata URI. Delegates to `JB721TiersHookLib.resolveTokenURI`, which checks for a custom `tokenUriResolver` first, then falls back to IPFS decoding via `JBIpfsDecoder`. |
38
38
  | `firstOwnerOf(tokenId)` | `JB721TiersHook` | Returns the first owner of an NFT (the address that originally received it). Stored on first transfer out; returns current owner if never transferred. |
39
- | `pricingContext()` | `JB721TiersHook` | Unpacks and returns the currency, decimals, and prices contract from the packed `_packedPricingContext`. |
39
+ | `pricingContext()` | `JB721TiersHook` | Unpacks and returns the currency and decimals from the packed `_packedPricingContext`. The prices contract is available via the `PRICES()` immutable getter. |
40
40
  | `balanceOf(owner)` | `JB721TiersHook` | Overrides ERC-721 `balanceOf` to delegate to `STORE.balanceOf`, which sums across all tiers. |
41
41
  | `hasMintPermissionFor(...)` | `JB721Hook` | Always returns `false`. Required by `IJBRulesetDataHook`; prevents the hook from granting mint permissions to anyone. |
42
42
  | `supportsInterface(interfaceId)` | `JB721TiersHook` | Returns `true` for `IJB721TiersHook`, `IJBRulesetDataHook`, `IJBPayHook`, `IJBCashOutHook`, `IERC2981`, `IERC721`, `IERC721Metadata`, `IERC165`. |
@@ -85,12 +85,12 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
85
85
  | `JB721TierConfig` | `uint104 price`, `uint32 initialSupply`, `uint32 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool useReserveBeneficiaryAsDefault`, `bool transfersPausable`, `bool useVotingUnits`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent`, `uint32 splitPercent`, `JBSplit[] splits` | `adjustTiers`, `initialize`, `recordAddTiers` |
86
86
  | `JB721Tier` | `uint32 id`, `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint104 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool transfersPausable`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent`, `uint32 splitPercent`, `string resolvedUri` | Return type from `tierOf`, `tiersOf`, `tierOfTokenId` |
87
87
  | `JBStored721Tier` | `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint32 splitPercent`, `uint24 category`, `uint8 discountPercent`, `uint16 reserveFrequency`, `uint8 packedBools` (allowOwnerMint, transfersPausable, useVotingUnits, cannotBeRemoved, cannotIncreaseDiscountPercent) | Internal storage in `JB721TiersHookStore`. Voting units stored separately in `_tierVotingUnitsOf` when `useVotingUnits` is true. |
88
- | `JB721InitTiersConfig` | `JB721TierConfig[] tiers`, `uint32 currency`, `uint8 decimals`, `IJBPrices prices` | `initialize` -- defines tiers and pricing context |
88
+ | `JB721InitTiersConfig` | `JB721TierConfig[] tiers`, `uint32 currency`, `uint8 decimals` | `initialize` -- defines tiers and pricing context. The prices contract is an immutable on the hook, not passed per-config. |
89
89
  | `JBDeploy721TiersHookConfig` | `string name`, `string symbol`, `string baseUri`, `IJB721TokenUriResolver tokenUriResolver`, `string contractUri`, `JB721InitTiersConfig tiersConfig`, `address reserveBeneficiary`, `JB721TiersHookFlags flags` | `deployHookFor`, `launchProjectFor` |
90
90
  | `JB721TiersHookFlags` | `bool noNewTiersWithReserves`, `bool noNewTiersWithVotes`, `bool noNewTiersWithOwnerMinting`, `bool preventOverspending`, `bool issueTokensForSplits` | `initialize`, `recordFlags` |
91
91
  | `JB721TiersRulesetMetadata` | `bool pauseTransfers`, `bool pauseMintPendingReserves` | Packed into `JBRulesetMetadata.metadata` per-ruleset (bit 0 = pauseTransfers, bit 1 = pauseMintPendingReserves) |
92
92
  | `JBPayDataHookRulesetConfig` | `uint48 mustStartAtOrAfter`, `uint32 duration`, `uint112 weight`, `uint32 weightCutPercent`, `IJBRulesetApprovalHook approvalHook`, `JBPayDataHookRulesetMetadata metadata`, `JBSplitGroup[] splitGroups`, `JBFundAccessLimitGroup[] fundAccessLimitGroups` | `JB721TiersHookProjectDeployer` -- wraps core ruleset config with `useDataHookForPay: true` hardcoded |
93
- | `JBPayDataHookRulesetMetadata` | Same as `JBRulesetMetadata` minus `allowSetCustomToken` (hardcoded false) and `useDataHookForPay` (hardcoded true) and `dataHook` (set to deployed hook). Includes `ownerMustSendPayouts`. | `launchProjectFor`, `launchRulesetsFor`, `queueRulesetsOf` |
93
+ | `JBPayDataHookRulesetMetadata` | Same as `JBRulesetMetadata` minus `useDataHookForPay` (hardcoded true) and `dataHook` (set to deployed hook). | `launchProjectFor`, `launchRulesetsFor`, `queueRulesetsOf` |
94
94
  | `JBLaunchProjectConfig` | `string projectUri`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `JBTerminalConfig[] terminalConfigurations`, `string memo` | `launchProjectFor` |
95
95
  | `JBLaunchRulesetsConfig` | `uint56 projectId`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `JBTerminalConfig[] terminalConfigurations`, `string memo` | `launchRulesetsFor` |
96
96
  | `JBQueueRulesetsConfig` | `uint56 projectId`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `string memo` | `queueRulesetsOf` |
@@ -145,9 +145,9 @@ Each tier has configurable voting power:
145
145
 
146
146
  ## Gotchas
147
147
 
148
- - `JB721TiersHook` is deployed as a **minimal clone** (not a full deployment). The constructor sets immutables (`RULESETS`, `STORE`, `SPLITS`, `DIRECTORY`, `METADATA_ID_TARGET`), and `initialize()` sets per-instance state. Calling `initialize()` twice reverts with `JB721TiersHook_AlreadyInitialized`.
148
+ - `JB721TiersHook` is deployed as a **minimal clone** (not a full deployment). The constructor sets immutables (`PRICES`, `RULESETS`, `STORE`, `SPLITS`, `DIRECTORY`, `METADATA_ID_TARGET`), and `initialize()` sets per-instance state. Calling `initialize()` twice reverts with `JB721TiersHook_AlreadyInitialized`.
149
149
  - **`JB721Hook` abstract base**: `JB721TiersHook` extends `JB721Hook`, which handles generic 721 hook lifecycle (terminal validation, burn loop, metadata decoding). `JB721TiersHook` overrides `cashOutWeightOf`, `totalCashOutWeight`, `_didBurn`, `_processPayment`, and `beforePayRecordedWith`. Errors like `JB721Hook_InvalidPay` and `JB721Hook_InvalidCashOut` are defined on the abstract class, not `JB721TiersHook`.
150
- - **Pricing context is bit-packed** into a single `uint256`: currency (bits 0-31), decimals (bits 32-39), prices contract address (bits 40-199). Read it via `pricingContext()`.
150
+ - **Pricing context is bit-packed** into a single `uint256`: currency (bits 0-31) and decimals (bits 32-39). The prices contract is the `PRICES` immutable (set in constructor). Read pricing context via `pricingContext()`.
151
151
  - **Pricing decimals must be <= 18**: `initialize` reverts with `JB721TiersHook_InvalidPricingDecimals` otherwise.
152
152
  - **Token IDs encode tier ID**: `tokenId = tierId * 1_000_000_000 + mintNumber`. Use `STORE.tierIdOfToken(tokenId)` to extract the tier ID.
153
153
  - **Pay credits**: If a payer overpays (amount > total tier prices), the excess is stored as `payCreditsOf[beneficiary]` and can be applied to future mints. This only works when `preventOverspending` flag is `false`. Credits are only combined with payment when `payer == beneficiary`.
@@ -156,15 +156,16 @@ Each tier has configurable voting power:
156
156
  - **Pending reserves inflate totalCashOutWeight**: `totalCashOutWeight` includes pending reserves in the denominator (`price * (minted + pendingReserves)`). This dilutes cash-out value before reserves are minted, preventing early cashers from extracting more than their fair share.
157
157
  - **Reserve minting is permissionless** but governed by ruleset metadata. Anyone can call `mintPendingReservesFor` as long as `mintPendingReservesPaused` is not set in the current ruleset's metadata.
158
158
  - **Reserve + owner-mint mutual exclusion**: Tiers with `allowOwnerMint: true` cannot have a `reserveFrequency`. The store rejects this combination during `recordAddTiers`.
159
+ - `setMetadata` accepts `name` and `symbol` as the first two parameters. Empty strings leave the current values unchanged.
159
160
  - `setMetadata` uses `address(this)` as the sentinel for "no change" on `tokenUriResolver` (not `address(0)`). Passing `address(0)` will clear the resolver.
160
- - `JBPayDataHookRulesetConfig` hardcodes `allowSetCustomToken: false` and `useDataHookForPay: true` when wiring rulesets through the project deployer.
161
+ - `JBPayDataHookRulesetConfig` hardcodes `useDataHookForPay: true` when wiring rulesets through the project deployer. All other metadata fields are passed through.
161
162
  - The `_update` override in `JB721TiersHook` checks `tier.transfersPausable` and consults the current ruleset's metadata for `transfersPaused`. Transfers to `address(0)` (burns) are never blocked.
162
163
  - **IERC2981 declared but not implemented**: `supportsInterface` returns `true` for `IERC2981`, but no `royaltyInfo` function is implemented. Callers querying `royaltyInfo` will get a revert. This appears intentional -- the interface is declared for future extension or to signal capability to marketplaces that may override behavior.
163
164
  - **Tier splits**: Each tier can route a percentage of its mint price to configured split recipients. `splitPercent` is out of `JBConstants.SPLITS_TOTAL_PERCENT` (1,000,000,000). Split group IDs are `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`.
164
165
  - **`useReserveBeneficiaryAsDefault` overwrites globally**: Adding a tier with `useReserveBeneficiaryAsDefault: true` silently overwrites `defaultReserveBeneficiaryOf` for ALL existing tiers that lack a tier-specific beneficiary. A `SetDefaultReserveBeneficiary` event is emitted when the default changes.
165
166
  - **Removing tiers does not update the sorted list**: `recordRemoveTierIds` only marks tiers in the bitmap. Call `cleanTiers()` afterward to remove them from the iteration sequence.
166
167
  - `JB721TiersHookStore` is a **shared singleton** -- all hook instances on the same chain use the same store, keyed by `address(hook)`.
167
- - The `ERC721` abstract uses `_initialize(name, symbol)` instead of a constructor, making it clone-compatible. The standard `_owners` mapping is `internal` (not `private`).
168
+ - The `ERC721` abstract uses `_initialize(name, symbol)` instead of a constructor, making it clone-compatible. It also exposes `_setName()` and `_setSymbol()` for post-initialization updates. The standard `_owners` mapping is `internal` (not `private`).
168
169
  - **`hasMintPermissionFor` always returns `false`**: The hook never grants mint permission to any address. This is part of the `IJBRulesetDataHook` interface.
169
170
  - **Max tier count is 65,535** (`type(uint16).max`). Adding tiers beyond this limit reverts.
170
171
  - **Max initial supply per tier is 999,999,999** (`_ONE_BILLION - 1`). Exceeding this would cause token ID overflow into the next tier's ID space.
@@ -218,8 +219,7 @@ tiers[0] = JB721TierConfig({
218
219
  tiersConfig: JB721InitTiersConfig({
219
220
  tiers: tiers,
220
221
  currency: 1, // ETH
221
- decimals: 18,
222
- prices: IJBPrices(address(0)) // no cross-currency pricing
222
+ decimals: 18
223
223
  }),
224
224
  reserveBeneficiary: address(0),
225
225
  flags: JB721TiersHookFlags({
package/STYLE_GUIDE.md CHANGED
@@ -253,9 +253,12 @@ uint256 public constant MAX_RESERVED_PERCENT = 10_000;
253
253
 
254
254
  ## Function Calls
255
255
 
256
- Use named parameters for readability when calling functions with 3+ arguments:
256
+ Use named arguments for all function calls with 2 or more arguments — in both `src/` and `script/`:
257
257
 
258
258
  ```solidity
259
+ // Good — named arguments
260
+ token.mint({account: beneficiary, amount: count});
261
+ _transferOwnership({newOwner: address(0), projectId: 0});
259
262
  PERMISSIONS.hasPermission({
260
263
  operator: sender,
261
264
  account: account,
@@ -264,8 +267,18 @@ PERMISSIONS.hasPermission({
264
267
  includeRoot: true,
265
268
  includeWildcardProjectId: true
266
269
  });
270
+
271
+ // Bad — positional arguments with 2+ args
272
+ token.mint(beneficiary, count);
273
+ _transferOwnership(address(0), 0);
267
274
  ```
268
275
 
276
+ Single-argument calls use positional style: `_burn(amount)`.
277
+
278
+ This also applies to constructor calls, struct literals, and inherited/library calls (e.g., OZ `_mint`, `_safeMint`, `safeTransfer`, `allowance`, `Clones.cloneDeterministic`).
279
+
280
+ Named argument keys must use **camelCase** — never underscores. If a function's parameter names use underscores, rename them to camelCase first.
281
+
269
282
  ## Multiline Signatures
270
283
 
271
284
  ```solidity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,10 +17,10 @@
17
17
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/address-registry-v6": "^0.0.8",
21
- "@bananapus/core-v6": "^0.0.15",
22
- "@bananapus/ownable-v6": "^0.0.7",
23
- "@bananapus/permission-ids-v6": "^0.0.7",
20
+ "@bananapus/address-registry-v6": "^0.0.9",
21
+ "@bananapus/core-v6": "^0.0.16",
22
+ "@bananapus/ownable-v6": "^0.0.9",
23
+ "@bananapus/permission-ids-v6": "^0.0.9",
24
24
  "@openzeppelin/contracts": "^5.6.1",
25
25
  "@prb/math": "^4.1.0",
26
26
  "solady": "^0.1.8"
@@ -1,7 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
7
  import "@bananapus/address-registry-v6/script/helpers/AddressRegistryDeploymentLib.sol";
6
8
 
7
9
  import {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
@@ -19,12 +21,17 @@ contract DeployScript is Script, Sphinx {
19
21
  AddressRegistryDeployment registry;
20
22
 
21
23
  /// @notice The address that is allowed to forward calls to the terminal and controller on a users behalf.
24
+ // forge-lint: disable-next-line(mixed-case-variable)
22
25
  address private TRUSTED_FORWARDER;
23
26
 
24
27
  /// @notice the salts that are used to deploy the contracts.
28
+ // forge-lint: disable-next-line(mixed-case-variable)
25
29
  bytes32 HOOK_SALT = "JB721TiersHookV6_";
30
+ // forge-lint: disable-next-line(mixed-case-variable)
26
31
  bytes32 HOOK_DEPLOYER_SALT = "JB721TiersHookDeployerV6_";
32
+ // forge-lint: disable-next-line(mixed-case-variable)
27
33
  bytes32 HOOK_STORE_SALT = "JB721TiersHookStoreV6_";
34
+ // forge-lint: disable-next-line(mixed-case-variable)
28
35
  bytes32 PROJECT_DEPLOYER_SALT = "JB721TiersHookProjectDeployerV6";
29
36
 
30
37
  function configureSphinx() public override {
@@ -74,13 +81,15 @@ contract DeployScript is Script, Sphinx {
74
81
  (address _hook, bool _hookIsDeployed) = _isDeployed(
75
82
  HOOK_SALT,
76
83
  type(JB721TiersHook).creationCode,
77
- abi.encode(core.directory, core.permissions, core.rulesets, store, core.splits, TRUSTED_FORWARDER)
84
+ abi.encode(
85
+ core.directory, core.permissions, core.prices, core.rulesets, store, core.splits, TRUSTED_FORWARDER
86
+ )
78
87
  );
79
88
 
80
89
  // Deploy it if it has not been deployed yet.
81
90
  hook = !_hookIsDeployed
82
91
  ? new JB721TiersHook{salt: HOOK_SALT}(
83
- core.directory, core.permissions, core.rulesets, store, core.splits, TRUSTED_FORWARDER
92
+ core.directory, core.permissions, core.prices, core.rulesets, store, core.splits, TRUSTED_FORWARDER
84
93
  )
85
94
  : JB721TiersHook(_hook);
86
95
  }
@@ -11,7 +11,9 @@ import {IJB721TiersHookProjectDeployer} from "../../src/interfaces/IJB721TiersHo
11
11
  import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
12
12
 
13
13
  struct Hook721Deployment {
14
+ // forge-lint: disable-next-line(mixed-case-variable)
14
15
  IJB721TiersHookDeployer hook_deployer;
16
+ // forge-lint: disable-next-line(mixed-case-variable)
15
17
  IJB721TiersHookProjectDeployer project_deployer;
16
18
  IJB721TiersHookStore store;
17
19
  }
@@ -19,6 +21,7 @@ struct Hook721Deployment {
19
21
  library Hook721DeploymentLib {
20
22
  // Cheat code address, 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D.
21
23
  address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
24
+ // forge-lint: disable-next-line(screaming-snake-case-const)
22
25
  Vm internal constant vm = Vm(VM_ADDRESS);
23
26
 
24
27
  function getDeployment(string memory path) internal returns (Hook721Deployment memory deployment) {
@@ -41,6 +44,7 @@ library Hook721DeploymentLib {
41
44
 
42
45
  function getDeployment(
43
46
  string memory path,
47
+ // forge-lint: disable-next-line(mixed-case-variable)
44
48
  string memory network_name
45
49
  )
46
50
  internal
@@ -66,7 +70,9 @@ library Hook721DeploymentLib {
66
70
  /// @return The address of the contract.
67
71
  function _getDeploymentAddress(
68
72
  string memory path,
73
+ // forge-lint: disable-next-line(mixed-case-variable)
69
74
  string memory project_name,
75
+ // forge-lint: disable-next-line(mixed-case-variable)
70
76
  string memory network_name,
71
77
  string memory contractName
72
78
  )
@@ -75,7 +81,8 @@ library Hook721DeploymentLib {
75
81
  returns (address)
76
82
  {
77
83
  string memory deploymentJson =
78
- vm.readFile(string.concat(path, project_name, "/", network_name, "/", contractName, ".json"));
84
+ // forge-lint: disable-next-line(unsafe-cheatcode)
85
+ vm.readFile(string.concat(path, project_name, "/", network_name, "/", contractName, ".json"));
79
86
  return stdJson.readAddress(deploymentJson, ".address");
80
87
  }
81
88
  }