@bananapus/721-hook-v6 0.0.29 → 0.0.31

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 (47) hide show
  1. package/ADMINISTRATION.md +38 -11
  2. package/ARCHITECTURE.md +53 -99
  3. package/AUDIT_INSTRUCTIONS.md +84 -383
  4. package/CHANGELOG.md +71 -0
  5. package/README.md +80 -225
  6. package/RISKS.md +32 -11
  7. package/SKILLS.md +29 -296
  8. package/STYLE_GUIDE.md +57 -18
  9. package/USER_JOURNEYS.md +57 -501
  10. package/package.json +4 -4
  11. package/references/operations.md +28 -0
  12. package/references/runtime.md +32 -0
  13. package/script/Deploy.s.sol +5 -4
  14. package/src/JB721TiersHook.sol +4 -2
  15. package/src/JB721TiersHookDeployer.sol +1 -1
  16. package/src/JB721TiersHookProjectDeployer.sol +1 -1
  17. package/src/JB721TiersHookStore.sol +24 -25
  18. package/src/libraries/JB721Constants.sol +1 -1
  19. package/src/libraries/JB721TiersHookLib.sol +39 -12
  20. package/src/libraries/JB721TiersRulesetMetadataResolver.sol +1 -1
  21. package/src/libraries/JBBitmap.sol +1 -1
  22. package/src/libraries/JBIpfsDecoder.sol +1 -1
  23. package/src/structs/JB721Tier.sol +5 -11
  24. package/src/structs/JB721TierConfig.sol +5 -20
  25. package/src/structs/JB721TierConfigFlags.sol +26 -0
  26. package/src/structs/JB721TierFlags.sol +17 -0
  27. package/test/721HookAttacks.t.sol +22 -17
  28. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +19 -14
  29. package/test/Fork.t.sol +69 -54
  30. package/test/TestAuditGaps.sol +73 -56
  31. package/test/TestSafeTransferReentrancy.t.sol +4 -4
  32. package/test/TestVotingUnitsLifecycle.t.sol +11 -11
  33. package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +10 -7
  34. package/test/audit/CodexSplitCreditsMismatch.t.sol +10 -7
  35. package/test/fork/ERC20CashOutFork.t.sol +37 -28
  36. package/test/fork/ERC20TierSplitFork.t.sol +28 -21
  37. package/test/fork/IssueTokensForSplitsFork.t.sol +10 -7
  38. package/test/invariants/handlers/TierLifecycleHandler.sol +10 -7
  39. package/test/invariants/handlers/TierStoreHandler.sol +10 -7
  40. package/test/regression/ProjectDeployerRulesets.t.sol +10 -7
  41. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +6 -6
  42. package/test/unit/AuditFixes_Unit.t.sol +39 -30
  43. package/test/unit/adjustTier_Unit.t.sol +268 -202
  44. package/test/unit/getters_constructor_Unit.t.sol +20 -14
  45. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +2 -2
  46. package/test/unit/pay_Unit.t.sol +1 -1
  47. package/CHANGE_LOG.md +0 -359
package/ADMINISTRATION.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  Admin privileges and their scope in nana-721-hook-v6.
4
4
 
5
+ ## At A Glance
6
+
7
+ | Item | Details |
8
+ |------|---------|
9
+ | Scope | Per-project NFT administration for tier configuration, metadata, discounts, owner mints, and hook deployment. |
10
+ | Operators | The resolved hook owner via `JBOwnable`, project-scoped delegates through `JBPermissions`, terminals, and the deployer/store contracts. |
11
+ | Highest-risk actions | Adjusting tiers after launch, changing metadata or discounts that affect sale behavior, and deploying or initializing the wrong hook clone. |
12
+ | Recovery posture | Clone-level mistakes are usually fixed by deploying a new hook and moving future rulesets to it; immutable constructor references cannot be changed in place. |
13
+
14
+ ## Routine Operations
15
+
16
+ - Grant only the specific 721 permission IDs a project operator needs instead of handing out broad owner-equivalent access.
17
+ - Use tier adjustments, metadata updates, owner mints, and discount changes with awareness of the project's current sale state and ruleset flags.
18
+ - Treat deployer and clone initialization steps as setup-only actions; verify ownership, pricing, and store references before publishing a hook address to users.
19
+ - When cash-out behavior or pay-hook composition changes, coordinate that with the project's ruleset configuration rather than assuming the hook can pause itself.
20
+
21
+ ## One-Way Or High-Risk Actions
22
+
23
+ - Clone initialization is one-time, and immutable constructor references on the implementation cannot be changed afterward.
24
+ - Hook ownership transfers change who can exercise every `onlyOwner` surface; accidental ownership moves are high-impact.
25
+ - Tier and pricing changes can have user-facing economic effects immediately even when they are technically reversible for future sales.
26
+
27
+ ## Recovery Notes
28
+
29
+ - If a hook is initialized with the wrong immutable dependencies or ownership model, deploy a new hook and migrate future project rulesets to it.
30
+ - If a project-specific configuration goes bad, prefer moving the project to a replacement hook over trying to retrofit behavior the hook was not designed to support.
31
+
5
32
  ## Roles
6
33
 
7
34
  ### Hook Owner (JBOwnable)
@@ -35,7 +62,7 @@ Admin privileges and their scope in nana-721-hook-v6.
35
62
  | Function | Permission ID | Checked Against | What It Does |
36
63
  |----------|--------------|-----------------|--------------|
37
64
  | `adjustTiers()` | `ADJUST_721_TIERS` | `owner()` | Adds new tiers and/or soft-removes existing tiers. Sets tier split groups in JBSplits. |
38
- | `mintFor()` | `MINT_721` | `owner()` | Manually mints NFTs from tiers that have `allowOwnerMint` enabled. Bypasses price checks (passes `type(uint256).max` as amount). |
65
+ | `mintFor()` | `MINT_721` | `owner()` | Manually mints NFTs from tiers that have `flags.allowOwnerMint` enabled. Bypasses price checks (passes `type(uint256).max` as amount). |
39
66
  | `setDiscountPercentOf()` | `SET_721_DISCOUNT_PERCENT` | `owner()` | Sets the discount percentage for a single tier. |
40
67
  | `setDiscountPercentsOf()` | `SET_721_DISCOUNT_PERCENT` | `owner()` | Batch-sets discount percentages for multiple tiers. |
41
68
  | `setMetadata()` | `SET_721_METADATA` | `owner()` | Updates collection name, symbol, baseURI, contractURI, tokenUriResolver, and/or per-tier encoded IPFS URIs. Empty strings leave values unchanged. |
@@ -46,8 +73,8 @@ Admin privileges and their scope in nana-721-hook-v6.
46
73
  | Function | Permission ID | Checked Against | What It Does |
47
74
  |----------|--------------|-----------------|--------------|
48
75
  | `launchProjectFor()` | None | Anyone can call | Creates a new project with a 721 hook. Ownership goes to the specified `owner` address. |
49
- | `launchRulesetsFor()` | `QUEUE_RULESETS` + `SET_TERMINALS` | Project NFT owner | Deploys a hook and launches rulesets for an existing project. |
50
- | `queueRulesetsOf()` | `QUEUE_RULESETS` | Project NFT owner | Deploys a hook and queues rulesets for an existing project. |
76
+ | `launchRulesetsFor()` | `QUEUE_RULESETS` + `SET_TERMINALS` | Project NFT owner or delegate | Deploys a hook and launches rulesets for an existing project. |
77
+ | `queueRulesetsOf()` | `QUEUE_RULESETS` | Project NFT owner or delegate | Deploys a hook and queues rulesets for an existing project. |
51
78
 
52
79
  ### JB721TiersHookDeployer
53
80
 
@@ -67,14 +94,14 @@ Admin privileges and their scope in nana-721-hook-v6.
67
94
  | Function | Caller | What It Does |
68
95
  |----------|--------|--------------|
69
96
  | `recordAddTiers()` | Hook contract | Adds tiers to the caller's namespace. Category sort order enforced. |
70
- | `recordRemoveTierIds()` | Hook contract | Marks tiers as removed in bitmap. Respects `cannotBeRemoved` flag. |
97
+ | `recordRemoveTierIds()` | Hook contract | Marks tiers as removed in bitmap. Respects `flags.cantBeRemoved` flag. |
71
98
  | `recordMint()` | Hook contract | Records mints, decrements supply, enforces price and reserve checks. |
72
99
  | `recordMintReservesFor()` | Hook contract | Mints reserved NFTs from a tier. |
73
100
  | `recordBurn()` | Hook contract | Increments burn counter for token IDs. |
74
101
  | `recordFlags()` | Hook contract | Sets behavioral flags for the caller's hook. |
75
102
  | `recordSetTokenUriResolver()` | Hook contract | Sets the token URI resolver. |
76
103
  | `recordSetEncodedIPFSUriOf()` | Hook contract | Sets the encoded IPFS URI for a tier. |
77
- | `recordSetDiscountPercentOf()` | Hook contract | Updates a tier's discount percent. Enforces bounds and `cannotIncreaseDiscountPercent`. |
104
+ | `recordSetDiscountPercentOf()` | Hook contract | Updates a tier's discount percent. Enforces bounds and `flags.cantIncreaseDiscountPercent`. |
78
105
  | `recordTransferForTier()` | Hook contract | Updates per-tier balance tracking on transfer. |
79
106
  | `cleanTiers()` | Anyone | Reorganizes the tier sorting linked list to skip removed tiers. Pure bookkeeping, no value at risk. |
80
107
 
@@ -116,8 +143,8 @@ The following are set at deploy/initialization time and **cannot be changed afte
116
143
  | `PROJECT_ID` | `initialize()` | Which project this hook belongs to |
117
144
  | Pricing context (currency, decimals) | `initialize()` | Packed into `_packedPricingContext` -- the token denomination for tier prices |
118
145
  | `JB721TiersHookFlags` | `initialize()` | `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending`, `issueTokensForSplits` |
119
- | Per-tier `cannotBeRemoved` | `recordAddTiers()` | Whether a tier can be soft-removed |
120
- | Per-tier `cannotIncreaseDiscountPercent` | `recordAddTiers()` | Whether a tier's discount can be increased |
146
+ | Per-tier `flags.cantBeRemoved` | `recordAddTiers()` | Whether a tier can be soft-removed |
147
+ | Per-tier `flags.cantIncreaseDiscountPercent` | `recordAddTiers()` | Whether a tier's discount can be increased |
121
148
  | Per-tier `reserveFrequency` | `recordAddTiers()` | How often reserve NFTs accrue |
122
149
  | Per-tier `initialSupply` | `recordAddTiers()` | Maximum number of NFTs mintable from the tier |
123
150
  | Per-tier `price` | `recordAddTiers()` | The base price (and cash-out weight) of NFTs in the tier |
@@ -139,7 +166,7 @@ Two behaviors are controlled by the project's current ruleset metadata (packed i
139
166
 
140
167
  | Bit | Flag | Effect |
141
168
  |-----|------|--------|
142
- | 0 | `transfersPaused` | When set, NFT transfers are blocked for tiers that have `transfersPausable` enabled |
169
+ | 0 | `transfersPaused` | When set, NFT transfers are blocked for tiers that have `flags.transfersPausable` enabled |
143
170
  | 1 | `mintPendingReservesPaused` | When set, `mintPendingReservesFor()` reverts |
144
171
 
145
172
  These can change each ruleset cycle, giving the project owner temporary control over these behaviors without modifying the hook itself.
@@ -152,9 +179,9 @@ What the hook owner **cannot** do:
152
179
  - **Cannot change tier prices after creation.** The `price` field in `JBStored721Tier` is set once in `recordAddTiers()` and never modified. Cash-out weight is always based on the original price.
153
180
  - **Cannot change reserve frequency after creation.** The `reserveFrequency` is immutable per tier.
154
181
  - **Cannot reduce a tier's initial supply.** Supply can only decrease through minting and burning.
155
- - **Cannot remove a tier marked `cannotBeRemoved`.** The store enforces this in `recordRemoveTierIds()`.
156
- - **Cannot increase a tier's discount if `cannotIncreaseDiscountPercent` is set.** The store enforces this in `recordSetDiscountPercentOf()`.
157
- - **Cannot mint from tiers without `allowOwnerMint`.** The `mintFor()` function passes `isOwnerMint: true` to the store, which checks the flag.
182
+ - **Cannot remove a tier marked `flags.cantBeRemoved`.** The store enforces this in `recordRemoveTierIds()`.
183
+ - **Cannot increase a tier's discount if `flags.cantIncreaseDiscountPercent` is set.** The store enforces this in `recordSetDiscountPercentOf()`.
184
+ - **Cannot mint from tiers without `flags.allowOwnerMint`.** The `mintFor()` function passes `isOwnerMint: true` to the store, which checks the flag.
158
185
  - **Cannot re-initialize a hook.** The `initialize()` function reverts if `_initialized` is already true.
159
186
  - **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.
160
187
  - **Cannot bypass the flag restrictions.** Once `noNewTiersWithReserves`, `noNewTiersWithVotes`, or `noNewTiersWithOwnerMinting` are set, all future tiers added via `adjustTiers()` must comply.
package/ARCHITECTURE.md CHANGED
@@ -1,122 +1,76 @@
1
- # nana-721-hook-v6 — Architecture
1
+ # Architecture
2
2
 
3
3
  ## Purpose
4
4
 
5
- NFT tier system for Juicebox V6. Allows projects to attach tiered NFT minting to payments and use NFTs as cash-out hooks. Supports on-chain and off-chain metadata, category-based sorting, and configurable pricing with discounts.
5
+ `nana-721-hook-v6` adds tiered NFT behavior to Juicebox projects. It lets a project accept payments, mint NFTs from configured tiers, optionally accumulate NFT credits, lazily mint reserves, and, when enabled, let holders burn NFTs to cash out project surplus according to tier-defined economics.
6
6
 
7
- The design permits a high theoretical tier ceiling, but several important reads and cash-out calculations still scale
8
- with `maxTierId`. In practice, this should be treated as a curated-catalog hook with an explicit operating envelope,
9
- not as a guarantee that very large catalogs are comfortable to run on-chain.
7
+ ## Boundaries
10
8
 
11
- ## Contract Map
9
+ - `JB721TiersHook` owns tier-aware behavior.
10
+ - `JB721TiersHookStore` owns compact tier storage and many validation rules.
11
+ - `JB721TiersHookDeployer` and `JB721TiersHookProjectDeployer` own deployment and project-launch convenience.
12
+ - The repo does not replace the core terminal, controller, or surplus logic; it plugs into them.
12
13
 
13
- ```
14
- src/
15
- ├── JB721TiersHook.sol — Pay + cash-out hook that mints/burns tiered NFTs
16
- ├── JB721TiersHookStore.sol — Tier configuration storage and pricing logic
17
- ├── JB721TiersHookDeployer.sol — Deploys hook+store pairs (clone-based)
18
- ├── JB721TiersHookProjectDeployer.sol — Launches project + hook in one transaction
19
- ├── abstract/
20
- │ ├── JB721Hook.sol — Base ERC-721 + pay/cashout hook integration
21
- │ └── ERC721.sol — Minimal ERC-721 implementation
22
- ├── libraries/
23
- │ ├── JB721Constants.sol — Global constants (DISCOUNT_DENOMINATOR)
24
- │ ├── JB721TiersHookLib.sol — Split calculations, price normalization, weight math, fund distribution
25
- │ ├── JB721TiersRulesetMetadataResolver.sol — Bit-packed 721 ruleset metadata (transfer/reserve pause flags)
26
- │ ├── JBBitmap.sol — Bitmap utilities for tier removal tracking
27
- │ └── JBIpfsDecoder.sol — IPFS CID encoding/decoding for token URIs
28
- ├── interfaces/ — All interfaces (IJB721TiersHook, etc.)
29
- └── structs/ — Tier config, mint context, cash-out structs
30
- ```
31
-
32
- ## Key Data Flows
33
-
34
- ### NFT Minting (via Payment)
35
- ```
36
- User → JBMultiTerminal.pay(metadata)
37
- → beforePayRecordedWith()
38
- → calculateSplitAmounts(): per-tier split amounts (in tier pricing denomination)
39
- → convertAndCapSplitAmounts(): convert to payment token denomination (if currencies differ)
40
- → calculateWeight(): adjust weight down by split fraction
41
- → JBTerminalStore records payment
42
- → afterPayRecordedWith() → _processPayment()
43
- → Normalize payment value to tier pricing currency
44
- → Decode tier IDs from metadata
45
- → For each tier:
46
- → Validate: not removed, not paused, supply available
47
- → Check price (with optional discount, normalized to tier pricing currency)
48
- → Mint NFT to beneficiary
49
- → Leftover amount stored as pay credits (revert if overspending not allowed)
50
- → Distribute split funds (priority: split.hook > split.projectId > split.beneficiary)
51
- ```
52
-
53
- #### Split Amount and Price Normalization
54
-
55
- Tiers can be priced in a different currency than the payment token (e.g. tiers priced in USD while payments arrive in ETH). The split/weight pipeline in `beforePayRecordedWith` resolves this in two steps:
56
-
57
- 1. **`calculateSplitAmounts`** — For each tier the payer wants to mint, looks up its `splitPercent` (the fraction of the tier price that should be routed to the tier's split group rather than into the project treasury). Computes `effectivePrice * splitPercent / SPLITS_TOTAL_PERCENT` for each tier, where `effectivePrice` accounts for any active discount. Returns the total and a per-tier breakdown, all denominated in the **tier pricing currency**.
58
-
59
- 2. **`convertAndCapSplitAmounts`** — If the payment currency differs from the tier pricing currency, converts every per-tier split amount (and the total) into the **payment token denomination** using `JBPrices`, and caps the total at the actual payment value. This is necessary because the terminal will subtract the split amount from the payment value, so both must be in the same unit.
14
+ ## Main Components
60
15
 
61
- After conversion, `calculateWeight` scales the terminal's minting weight down by the fraction of the payment that was routed to splits (unless the `issueTokensForSplits` flag is set, in which case the full weight is preserved).
16
+ | Component | Responsibility |
17
+ | --- | --- |
18
+ | `JB721TiersHook` | Data hook, pay hook, and cash-out hook for tiered NFTs |
19
+ | `JB721TiersHookStore` | Packed tier storage, mint accounting, reserves, and validation |
20
+ | `JB721Hook`, `ERC721` | Shared NFT machinery and token metadata plumbing |
21
+ | deployers | Clone and launch helpers for hook instances and projects |
22
+ | libraries and structs | Tier math, metadata decoding, flags, and config surfaces |
62
23
 
63
- During `afterPayRecordedWith`, the payment value is separately normalized into the tier pricing currency via `normalizePaymentValue` so that tier prices can be compared against what was paid. The forwarded split funds are then distributed to each tier's split group by `distributeAll`.
24
+ ## Runtime Model
64
25
 
65
- ### Discount Mechanism
26
+ ### Payment Path
66
27
 
67
- Each tier has a `discountPercent` (0-200 scale where 200 = 100% discount / free mint). The project owner can adjust it via `setDiscountPercentOf`, subject to a per-tier `cannotIncreaseDiscountPercent` flag that prevents raising the discount once set. Discounts can only be decreased unless the tier explicitly allows increases.
68
-
69
- Key behaviors:
70
- - **Minting**: The store applies the discount when checking the price during `recordMint`, so payers pay less.
71
- - **Split calculations**: `calculateSplitAmounts` applies the discount to the tier price before computing the split portion, so splits are proportional to the actual amount paid.
72
- - **Cash-out weight**: Uses the **original undiscounted price**, not the effective price. This means a discounted NFT carries the same cash-out weight as a full-price one. Project owners should be aware that heavily discounted mints dilute the cash-out pool at full weight while contributing less to the treasury.
73
-
74
- ### NFT Cash Out
75
- ```
76
- Holder → JBMultiTerminal.cashOutTokensOf()
77
- → JB721TiersHook.afterCashOutRecordedWith()
78
- → Burn specified NFT token IDs
79
- → Each NFT's cash-out weight = tier.price (full price, ignoring discounts)
80
- → Total cash-out weight (denominator) = sum of (tier.price * (outstanding + pending)) across all tiers
81
- → where outstanding = minted - burned, pending = pending reserve mints
28
+ ```text
29
+ terminal payment
30
+ -> data hook inspects metadata and requested tier IDs
31
+ -> hook computes which tiers can be minted and how much value is left over
32
+ -> terminal settles the payment into the project
33
+ -> pay hook mints NFTs and stores any remaining value as credits when allowed
82
34
  ```
83
35
 
84
- ### Tier Management
85
- ```
86
- Owner → JB721TiersHook.adjustTiers()
87
- → Add new tiers (must be sorted by category)
88
- → Remove existing tiers (flags, doesn't delete)
89
- ```
36
+ ### Reserve Path
90
37
 
91
- ## Extension Points
38
+ ```text
39
+ tier purchases accumulate reserve entitlement
40
+ -> reserves are minted lazily via explicit reserve-mint calls
41
+ ```
92
42
 
93
- | Point | Interface | Purpose |
94
- |-------|-----------|---------|
95
- | Token URI resolver | `IJB721TokenUriResolver` | Custom metadata rendering |
96
- | Pay hook | `IJBPayHook` | Called after payment recorded |
97
- | Cash out hook | `IJBCashOutHook` | Called during cash out |
43
+ ### Cash-Out Path
98
44
 
99
- ## Design Decisions
45
+ ```text
46
+ holder burns NFT
47
+ -> data hook can override cash-out count, supply, and tax behavior
48
+ -> reclaim value is based on the tier's original configured price, not any discount used at mint time
49
+ ```
100
50
 
101
- 1. **Clone-based deployment** — `JB721TiersHookDeployer` uses Solady's `LibClone` to deploy minimal proxies of a reference `JB721TiersHook` instance. This keeps deployment cost low and consistent regardless of the hook contract's bytecode size. Each clone is initialized via `initialize()` (not a constructor), which sets the project ID, tiers, and flags. Deterministic deploys are supported via `cloneDeterministic` with a caller-scoped salt.
51
+ ## Critical Invariants
102
52
 
103
- 2. **Separate store contract** `JB721TiersHookStore` holds all tier data, mint tracking, and pricing logic in a standalone contract shared across hook instances. This serves two purposes: it keeps the hook contract under the EIP-170 size limit (24 KB), and it allows the store to act as the `msg.sender` for state mutations (tier additions use `msg.sender` as the hook key), providing a natural access-control boundary.
53
+ - Tiers are an ordered, compact data structure. Category ordering and tier IDs are part of storage semantics, not just metadata.
54
+ - Original tier price drives cash-out weight. Discounts change purchase price, not reclaim weight.
55
+ - Reserve frequency and owner-mint settings interact; configurations that would double-count mint authority must stay invalid.
56
+ - Pending reserves belong in supply-sensitive calculations even before they are lazily minted.
57
+ - If `useDataHookForCashOut` is enabled, fungible-token cash outs are intentionally displaced by NFT cash-out semantics.
104
58
 
105
- 3. **Category sorting instead of price sorting** — Tiers are stored in a linked list sorted by `category` (a uint24 grouping field), not by price. This lets projects organize NFTs into logical groups (e.g. membership tiers, collectibles, special editions) and query them by group. The `InvalidCategorySortOrder` error enforces that new tiers are added in non-decreasing category order.
59
+ ## Where Complexity Lives
106
60
 
107
- 4. **Cash-out weight uses `initialSupply * price`** — The `totalCashOutWeight` denominator sums `price * (outstanding + pendingReserves)` across all tiers, where outstanding = minted minus burned. Each individual NFT's weight is simply its tier's `price`. Using the original undiscounted price (rather than what was actually paid) ensures that cash-out values are stable and predictable: discounts are treated as transient purchase incentives, not permanent reductions in an NFT's share of the treasury.
61
+ - The store is compact and efficient, which means seemingly small layout changes have wide effects.
62
+ - Preview behavior, pay behavior, reserve minting, and cash-out behavior all depend on the same tier semantics.
63
+ - Metadata decoding and credit handling create edge cases around partial spends and exact-tier selection.
108
64
 
109
- 5. **Library extraction for EIP-170 compliance** — `JB721TiersHookLib` contains split calculations, price normalization, fund distribution, tier adjustment logic, and IPFS URI decoding. These are called via `external` library functions (which deploy as a separate contract) or via `DELEGATECALL`. This pattern keeps the hook contract's deployed bytecode under the 24 KB limit while preserving the ability to emit events from the hook's address.
65
+ ## Dependencies
110
66
 
111
- 6. **Discount denominator of 200** — The `discountPercent` field is a uint8 with a denominator of 200 (`JB721Constants.DISCOUNT_DENOMINATOR`). A value of 200 represents 100% discount (free mint), giving 0.5% granularity. The `cannotIncreaseDiscountPercent` flag on each tier lets project owners create promotional discounts that can be reduced but never increased beyond their initial level.
67
+ - `nana-core-v6` hooks, permissions, controller, and terminal surfaces
68
+ - Optional token URI resolvers such as `banny-retail-v6` and `defifa`
112
69
 
113
- 7. **Split fund distribution with try-catch** — All external calls during split distribution (to split hooks, terminals, and beneficiaries) are wrapped in try-catch. A reverting recipient does not brick payments for the entire project. Failed amounts are accumulated separately during the loop and routed to the project balance afterward, ensuring proportional redistribution -- later recipients receive only their configured share, not an inflated share from earlier failures.
70
+ ## Safe Change Guide
114
71
 
115
- ## Dependencies
116
- - `@bananapus/core-v6` Core protocol interfaces
117
- - `@bananapus/ownable-v6` JB-aware ownership
118
- - `@bananapus/address-registry-v6` Deterministic deploy addresses
119
- - `@bananapus/permission-ids-v6` Permission constants
120
- - `@openzeppelin/contracts` — ERC-721 utils, Ownable
121
- - `@prb/math` — mulDiv
122
- - `solady` — LibClone
72
+ - Treat store changes as consensus-level changes for every repo that composes this hook.
73
+ - Preview behavior and live behavior must stay aligned. If a preview lies, integrators misprice payments.
74
+ - Be careful when adding metadata fields; many sibling repos depend on stable encoding conventions.
75
+ - Keep hook-order assumptions explicit when this hook is composed with other data hooks through a wrapper deployer.
76
+ - If you touch reserve logic, also inspect supply math and cash-out denominators.