@bananapus/721-hook-v6 0.0.28 → 0.0.30

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 (46) 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 +79 -225
  6. package/RISKS.md +28 -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 +1 -1
  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 +1 -1
  15. package/src/JB721TiersHookDeployer.sol +1 -1
  16. package/src/JB721TiersHookProjectDeployer.sol +1 -1
  17. package/src/JB721TiersHookStore.sol +23 -17
  18. package/src/libraries/JB721Constants.sol +1 -1
  19. package/src/libraries/JB721TiersRulesetMetadataResolver.sol +1 -1
  20. package/src/libraries/JBBitmap.sol +1 -1
  21. package/src/libraries/JBIpfsDecoder.sol +1 -1
  22. package/src/structs/JB721Tier.sol +5 -11
  23. package/src/structs/JB721TierConfig.sol +5 -20
  24. package/src/structs/JB721TierConfigFlags.sol +26 -0
  25. package/src/structs/JB721TierFlags.sol +17 -0
  26. package/test/721HookAttacks.t.sol +22 -17
  27. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +19 -14
  28. package/test/Fork.t.sol +69 -54
  29. package/test/TestAuditGaps.sol +73 -56
  30. package/test/TestSafeTransferReentrancy.t.sol +4 -4
  31. package/test/TestVotingUnitsLifecycle.t.sol +11 -11
  32. package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +10 -7
  33. package/test/audit/CodexSplitCreditsMismatch.t.sol +10 -7
  34. package/test/fork/ERC20CashOutFork.t.sol +37 -28
  35. package/test/fork/ERC20TierSplitFork.t.sol +28 -21
  36. package/test/fork/IssueTokensForSplitsFork.t.sol +10 -7
  37. package/test/invariants/handlers/TierLifecycleHandler.sol +10 -7
  38. package/test/invariants/handlers/TierStoreHandler.sol +10 -7
  39. package/test/regression/ProjectDeployerRulesets.t.sol +10 -7
  40. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +6 -6
  41. package/test/unit/AuditFixes_Unit.t.sol +37 -28
  42. package/test/unit/adjustTier_Unit.t.sol +268 -202
  43. package/test/unit/getters_constructor_Unit.t.sol +20 -14
  44. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +2 -2
  45. package/test/unit/pay_Unit.t.sol +1 -1
  46. package/CHANGE_LOG.md +0 -359
package/USER_JOURNEYS.md CHANGED
@@ -1,531 +1,87 @@
1
- # User Journeys -- nana-721-hook-v6
1
+ # User Journeys
2
2
 
3
- Every user-facing operation in the tiered 721 hook system, with exact entry points, parameters, state changes, events, and edge cases.
3
+ ## Who This Repo Serves
4
4
 
5
- These journeys describe functional behavior, not a promise that the theoretical `uint16.max` tier ceiling is a
6
- production-ready catalog size. Several important reads and cash-out calculations scale with `maxTierId`, so large-tier
7
- deployments should be evaluated against the repo's documented operating envelope before launch.
5
+ - project owners selling or rewarding supporters with tiered NFTs
6
+ - operators managing tier supply, pricing, reserves, and ruleset-aware hook behavior
7
+ - supporters minting or cashing out tier positions through normal Juicebox flows
8
+ - integrators composing custom token URI resolvers on top of the standard 721 hook
8
9
 
9
- ---
10
+ ## Journey 1: Add A Tiered 721 Hook To An Existing Project
10
11
 
11
- ## 1. Pay and Receive NFTs
12
+ **Starting state:** the project already exists in Juicebox and needs tiered NFT issuance without relaunching.
12
13
 
13
- A user pays a Juicebox project and receives tiered NFTs based on the amount paid and the tier IDs specified in metadata.
14
+ **Success:** a hook clone is deployed, initialized, and attached to the project's ruleset metadata.
14
15
 
15
- **Entry point**: `JBMultiTerminal.pay()` (external). The hook is invoked as both a data hook (`beforePayRecordedWith`) and a pay hook (`afterPayRecordedWith`).
16
+ **Flow**
17
+ 1. Use `JB721TiersHookDeployer` to clone a hook for the target project.
18
+ 2. Define tier config, reserve behavior, token URI resolver, and per-ruleset flags.
19
+ 3. Queue or install ruleset metadata that tells the project when the hook should participate in pay and cash-out flows.
20
+ 4. Future payments into the project can now mint tiers under the configured constraints.
16
21
 
17
- **Who can call**: Anyone. The terminal's `pay()` is permissionless.
22
+ ## Journey 2: Launch A New Project With A 721 Hook Already Wired In
18
23
 
19
- **Parameters** (encoded in payment metadata via `JBMetadataResolver`):
20
- - `bool allowOverspending` -- whether leftover funds after minting should be stored as credits (true) or revert (false). The hook-level `preventOverspending` flag can override this.
21
- - `uint16[] tierIdsToMint` -- which tier IDs to mint, in order. The same tier can appear multiple times.
24
+ **Starting state:** the product wants its project treasury and NFT logic created in one operation.
22
25
 
23
- **Metadata encoding**: Use `JBMetadataResolver.getId({purpose: "pay", target: hook.METADATA_ID_TARGET()})` as the metadata ID. Encode the value as `abi.encode(allowOverspending, tierIdsToMint)`.
26
+ **Success:** the project launches with the hook, terminals, and initial tiers already aligned.
24
27
 
25
- **State changes**:
26
- 1. `JBTerminalStore` records the payment with adjusted weight (reduced by split fraction unless `issueTokensForSplits` is set).
27
- 2. For each tier ID in `tierIdsToMint`:
28
- - `JB721TiersHookStore._storedTierOf[hook][tierId].remainingSupply` decremented by 1.
29
- - A new ERC-721 token is minted to the beneficiary. Token ID = `tierId * 1e9 + tokenNumber`.
30
- - `JB721TiersHookStore.tierBalanceOf[hook][beneficiary][tierId]` incremented by 1.
31
- 3. `payCreditsOf[beneficiary]` updated to reflect leftover amount plus unused credits.
32
- 4. If tiers have `splitPercent > 0`, forwarded funds are distributed to tier split groups via `JB721TiersHookLib.distributeAll`.
28
+ **Flow**
29
+ 1. Use `JB721TiersHookProjectDeployer` with launch config, rulesets, and hook config.
30
+ 2. The deployer launches the project through the core protocol and deploys the hook in the same packaged flow.
31
+ 3. The initial ruleset metadata points at the newly created hook so there is no post-launch rewiring gap.
32
+ 4. The project starts life as a tiered 721 project instead of becoming one later.
33
33
 
34
- **Events**:
35
- - `Mint(tokenId, tierId, beneficiary, totalAmountPaid, caller)` -- one per NFT minted.
36
- - `AddPayCredits(amount, newTotalCredits, account, caller)` -- if credits increased.
37
- - `UsePayCredits(amount, newTotalCredits, account, caller)` -- if credits decreased.
38
- - `SplitPayoutReverted(projectId, split, amount, reason, caller)` -- if a split recipient's payout reverts during distribution (funds route to project balance instead).
39
- - `AddToBalanceReverted(projectId, token, amount, reason)` -- if the `addToBalanceOf` call for leftover funds reverts after split distribution.
34
+ ## Journey 3: Mint Specific Tiers Through A Payment
40
35
 
41
- **Edge cases**:
42
- - **No metadata or metadata not found**: No NFTs minted. If `preventOverspending` is false, the entire payment becomes credits for the beneficiary. If true, reverts.
43
- - **Payer != beneficiary**: The payer's existing credits are NOT applied. Only the beneficiary's credits are combined with the payment. Leftover accrues to the beneficiary.
44
- - **Tier removed**: Reverts with `JB721TiersHookStore_TierRemoved`.
45
- - **Tier sold out**: Reverts with `JB721TiersHookStore_InsufficientSupplyRemaining`.
46
- - **Insufficient payment**: Reverts with `JB721TiersHookStore_PriceExceedsAmount`.
47
- - **Last slot is reserved**: If minting would leave `remainingSupply < pendingReserves`, reverts with `JB721TiersHookStore_InsufficientSupplyRemaining`.
48
- - **Cross-currency payment with no price feed**: `normalizePaymentValue` returns `(0, false)`. The hook returns without minting or reverting. The payment is still processed by the terminal.
49
- - **Discounted tier**: Effective price is `price - mulDiv(price, discountPercent, 200)`. A `discountPercent` of 200 makes it free.
50
- - **Split distribution**: If a split recipient's payout reverts, the failed amount is accumulated separately and added to the project's balance after the loop. Later split recipients receive only their proportional share, not the failed recipient's share.
36
+ **Starting state:** the project has live tiers and the payer knows which tiers they want.
51
37
 
52
- ---
38
+ **Success:** the treasury receives funds and the beneficiary receives the intended NFT tiers plus any accompanying project-token behavior.
53
39
 
54
- ## 2. Cash Out NFTs
40
+ **Flow**
41
+ 1. The payer submits a payment with metadata encoding the desired tier selections.
42
+ 2. `JB721TiersHook` validates tier availability, quantity rules, discounts, category constraints, and any ruleset flags affecting minting.
43
+ 3. `JB721TiersHookStore` updates supply and reserve accounting.
44
+ 4. The hook mints the correct NFTs and the underlying terminal completes treasury accounting.
55
45
 
56
- An NFT holder burns their NFTs to reclaim funds from the project's surplus, proportional to the NFTs' cash-out weight relative to the total cash-out weight.
46
+ **Failure cases that matter:** sold-out tiers, bad metadata, cross-currency pricing mistakes, paused pay-hook behavior, and split-routing edge cases when part of the payment should bypass normal minting assumptions.
57
47
 
58
- **Entry point**: `JBMultiTerminal.cashOutTokensOf()` (external). The hook is invoked as both a data hook (`beforeCashOutRecordedWith`) and a cash out hook (`afterCashOutRecordedWith`).
48
+ ## Journey 4: Mint Reserves And Operate Tier Inventory Over Time
59
49
 
60
- **Who can call**: The NFT holder, or an operator with `CASH_OUT_TOKENS` permission from the holder.
50
+ **Starting state:** the collection is live and the owner needs to manage what exists for future minters versus reserve recipients.
61
51
 
62
- **Parameters** (encoded in cash-out metadata via `JBMetadataResolver`):
63
- - `uint256[] tokenIds` -- the token IDs of the NFTs to burn.
52
+ **Success:** reserves, new tiers, removed tiers, and editable fields evolve without corrupting ownership or supply history.
64
53
 
65
- **Metadata encoding**: Use `JBMetadataResolver.getId({purpose: "cashOut", target: hook.METADATA_ID_TARGET()})` as the metadata ID. Encode the value as `abi.encode(tokenIds)`.
54
+ **Flow**
55
+ 1. Authorized operators use the hook's tier-management surfaces to add, remove, or adjust tiers.
56
+ 2. Reserve minting uses the configured reserve frequency and reserve beneficiary settings instead of ad hoc treasury actions.
57
+ 3. The store keeps historical tier state coherent so existing token IDs continue to resolve correctly.
58
+ 4. Downstream products such as Croptop, Banny, and Revnets can continue assuming stable tier semantics.
66
59
 
67
- **Preconditions**:
68
- - The project's ruleset must have `useDataHookForCashOut` set to true.
69
- - `cashOutCount` in the terminal call must be 0 (no fungible tokens cashed out alongside NFTs). The hook reverts with `JB721Hook_UnexpectedTokenCashedOut` otherwise.
70
- - The caller must be the holder of all specified token IDs.
60
+ ## Journey 5: Let Holders Cash Out Tier Positions
71
61
 
72
- **State changes**:
73
- 1. Each NFT is burned (ERC-721 `_burn`).
74
- 2. `JB721TiersHookStore.tierBalanceOf[hook][holder][tierId]` decremented by 1 for each NFT.
75
- 3. `JB721TiersHookStore.numberOfBurnedFor[hook][tierId]` incremented by 1 for each NFT.
76
- 4. `_firstOwnerOf[tokenId]` set to `holder` if not already set (recorded during the `_update` override on burn).
77
- 5. Terminal transfers reclaim amount to beneficiary based on bonding curve math.
62
+ **Starting state:** a holder owns one or more NFTs from a hook-enabled project and the active ruleset allows surplus-backed exits.
78
63
 
79
- **Cash-out weight calculation**:
80
- - Per-NFT weight: `storedTier.price` (original price, NOT discounted).
81
- - Total weight: sum of `price * (outstandingCount + pendingReserves)` across ALL tiers (including removed tiers), where `outstandingCount = initialSupply - remainingSupply - numberOfBurned`. Burned tokens are excluded from the total weight, so burning NFTs reduces the denominator and increases the per-NFT reclaim for remaining holders.
82
- - Reclaim amount is computed by the terminal's bonding curve using `cashOutCount / totalSupply` as the ratio.
64
+ **Success:** the holder burns the intended tier exposure and receives the correct reclaim value through the core terminal.
83
65
 
84
- **Events**:
85
- - ERC-721 `Transfer(holder, address(0), tokenId)` -- one per NFT burned.
66
+ **Flow**
67
+ 1. The holder calls the project's cash-out path on the terminal.
68
+ 2. The hook participates in the cash-out calculation so tier-specific weight and store state are reflected correctly.
69
+ 3. The terminal settles reclaim value and the NFT position is burned or otherwise marked as consumed by the exit path.
86
70
 
87
- **Edge cases**:
88
- - **Token not owned by holder**: Reverts with `JB721Hook_UnauthorizedToken`.
89
- - **No metadata**: No token IDs decoded. No NFTs burned. Cash out weight is 0, so reclaim is 0.
90
- - **Removed tier NFTs**: Still valid for cash out. Their weight is still counted in `totalCashOutWeight`.
91
- - **Pending reserves inflate totalSupply**: The denominator includes unminted reserves, reducing per-NFT reclaim. This is by design.
71
+ **Edge conditions that change user experience:** ERC-20 cash-out surfaces, tier splits, reserve accounting drift, broken downstream terminals, and projects that use the hook for metadata only versus economically binding flows.
92
72
 
93
- ---
73
+ ## Journey 6: Compose A Custom Product On Top Of The Standard Hook
94
74
 
95
- ## 3. Add Tiers
75
+ **Starting state:** the team wants product-specific rendering or business rules but does not want to fork tier issuance.
96
76
 
97
- The project owner adds new NFT tiers to the hook.
77
+ **Success:** the product resolver or wrapper composes with the hook instead of reimplementing pricing, tier accounting, and treasury logic.
98
78
 
99
- **Entry point**: `JB721TiersHook.adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove)` (external).
79
+ **Flow**
80
+ 1. Plug a custom resolver into the hook for metadata and product-specific presentation.
81
+ 2. Keep collection-specific behavior in the downstream repo while leaving pay, reserve, and cash-out semantics in this repo.
82
+ 3. Audit hook-store interactions here first, then audit the downstream resolver or wrapper.
100
83
 
101
- **Who can call**: Hook owner, or an operator with `ADJUST_721_TIERS` permission from the hook owner.
84
+ ## Hand-Offs
102
85
 
103
- **Parameters** (per `JB721TierConfig`):
104
- - `uint104 price` -- tier price in the hook's pricing currency.
105
- - `uint32 initialSupply` -- max NFTs mintable (must be > 0, <= 999,999,999).
106
- - `uint32 votingUnits` -- custom voting power per NFT (used if `useVotingUnits` is true).
107
- - `uint16 reserveFrequency` -- one reserve mint per N paid mints. 0 = no reserves.
108
- - `address reserveBeneficiary` -- who receives reserve mints.
109
- - `bytes32 encodedIPFSUri` -- IPFS CID for NFT metadata.
110
- - `uint24 category` -- grouping category. Tiers MUST be sorted by category ascending.
111
- - `uint8 discountPercent` -- discount out of 200 (not 100). 200 = free. Must be <= 200.
112
- - `bool allowOwnerMint` -- allow manual owner minting from this tier.
113
- - `bool useReserveBeneficiaryAsDefault` -- set this tier's reserve beneficiary as the global default. WARNING: overwrites the default for ALL tiers.
114
- - `bool transfersPausable` -- whether transfers can be paused per ruleset.
115
- - `bool useVotingUnits` -- use custom `votingUnits` instead of price for voting power.
116
- - `bool cannotBeRemoved` -- makes this tier permanent.
117
- - `bool cannotIncreaseDiscountPercent` -- locks the discount from being increased.
118
- - `uint32 splitPercent` -- percentage of effective price routed to splits (out of 1,000,000,000).
119
- - `JBSplit[] splits` -- split recipients for this tier's split group.
120
-
121
- **State changes**:
122
- 1. New tier IDs assigned sequentially: `maxTierIdOf + 1`, `maxTierIdOf + 2`, etc.
123
- 2. `_storedTierOf[hook][tierId]` populated with a `JBStored721Tier`.
124
- 3. Sorted linked list (`_tierIdAfter`) updated to insert tiers at correct category position.
125
- 4. `_startingTierIdOfCategory[hook][category]` set for first tier in a new category.
126
- 5. `encodedIPFSUriOf[hook][tierId]` set if provided.
127
- 6. Reserve beneficiary set (per-tier or global default).
128
- 7. `maxTierIdOf[hook]` updated.
129
- 8. If any tier has `splits.length > 0`, `JBSplits.setSplitGroupsOf` is called with the tier's split group (groupId = `hookAddress | tierId << 160`, rulesetId = 0).
130
-
131
- **Events**:
132
- - `AddTier(tierId, tierConfig, caller)` -- one per tier added.
133
- - `SetDefaultReserveBeneficiary(hook, newBeneficiary, caller)` -- if any tier has `useReserveBeneficiaryAsDefault = true` and changes the current default.
134
-
135
- **Edge cases**:
136
- - **Categories not sorted ascending**: Reverts with `JB721TiersHookStore_InvalidCategorySortOrder`.
137
- - **Exceeds 65,535 total tiers**: Reverts with `JB721TiersHookStore_MaxTiersExceeded`.
138
- - **`initialSupply == 0`**: Reverts with `JB721TiersHookStore_ZeroInitialSupply`.
139
- - **`initialSupply > 999,999,999`**: Reverts with `JB721TiersHookStore_InvalidQuantity`. Each tier's supply must fit within the token ID encoding scheme (`tierId * 1e9 + tokenNumber`).
140
- - **`discountPercent > 200`**: Reverts with `JB721TiersHookStore_DiscountPercentExceedsBounds`.
141
- - **`splitPercent > SPLITS_TOTAL_PERCENT`**: Reverts with `JB721TiersHookStore_SplitPercentExceedsBounds`.
142
- - **`noNewTiersWithVotes` flag set**: Reverts with `JB721TiersHookStore_VotingUnitsNotAllowed` if the new tier would have any voting power. This means tiers with `useVotingUnits = true` and `votingUnits > 0` are rejected, AND tiers with `useVotingUnits = false` and `price > 0` are also rejected (since price is used as voting power by default when `useVotingUnits` is false).
143
- - **`noNewTiersWithReserves` flag set**: Reverts with `JB721TiersHookStore_ReserveFrequencyNotAllowed` if tier has `reserveFrequency > 0`.
144
- - **`noNewTiersWithOwnerMinting` flag set**: Reverts with `JB721TiersHookStore_ManualMintingNotAllowed` if tier has `allowOwnerMint = true`.
145
- - **`allowOwnerMint` + `reserveFrequency > 0`**: Reverts with `JB721TiersHookStore_ReserveFrequencyNotAllowed`. Owner-mintable tiers cannot have reserves.
146
- - **`useReserveBeneficiaryAsDefault = true`**: Only takes effect when both `reserveBeneficiary != address(0)` AND `reserveFrequency != 0`. When both conditions are met, silently overwrites `defaultReserveBeneficiaryOf[hook]`, affecting all existing tiers that use the default. If either condition is missing, the flag is silently ignored.
147
-
148
- ---
149
-
150
- ## 4. Remove Tiers
151
-
152
- The project owner removes tiers, preventing new mints but preserving existing NFTs' cash-out weight.
153
-
154
- **Entry point**: `JB721TiersHook.adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove)` (external). Pass an empty `tiersToAdd` array to only remove.
155
-
156
- **Who can call**: Hook owner, or an operator with `ADJUST_721_TIERS` permission from the hook owner.
157
-
158
- **Parameters**:
159
- - `uint256[] tierIdsToRemove` -- IDs of tiers to remove.
160
-
161
- **State changes**:
162
- 1. Each tier ID is marked in `_removedTiersBitmapWordOf[hook]` via `JBBitmap.removeTier`.
163
- 2. The stored tier data (`_storedTierOf`) is NOT deleted.
164
- 3. The sorted linked list (`_tierIdAfter`) is NOT updated. Call `cleanTiers()` separately to update it.
165
-
166
- **Events**:
167
- - `RemoveTier(tierId, caller)` -- one per tier removed.
168
-
169
- **Edge cases**:
170
- - **`cannotBeRemoved = true`**: Reverts with `JB721TiersHookStore_CantRemoveTier`.
171
- - **Already removed tier**: No revert. Bitmap set is idempotent.
172
- - **Existing NFTs**: Retain full cash-out weight. `totalCashOutWeight` still counts them.
173
- - **Pending reserves**: Can still be minted from removed tiers via `mintPendingReservesFor`.
174
-
175
- ---
176
-
177
- ## 5. Mint Reserves
178
-
179
- Anyone can mint pending reserved NFTs for a tier. Reserves accumulate based on the ratio of paid mints to the tier's `reserveFrequency`.
180
-
181
- **Entry point**: `JB721TiersHook.mintPendingReservesFor(uint256 tierId, uint256 count)` (public, permissionless).
182
-
183
- **Batch entry point**: `JB721TiersHook.mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs)` (external, permissionless).
184
-
185
- **Who can call**: Anyone. Both entry points are permissionless.
186
-
187
- **Parameters**:
188
- - `uint256 tierId` -- the tier to mint reserves from.
189
- - `uint256 count` -- how many reserve NFTs to mint.
190
-
191
- **Preconditions**:
192
- - `mintPendingReservesPaused` must be false in the current ruleset's metadata (bit 1 of `JBRulesetMetadata.metadata`).
193
- - `count <= numberOfPendingReserves` for the tier.
194
- - The tier must have a reserve beneficiary (tier-specific or global default).
195
-
196
- **Pending reserve formula**:
197
- ```
198
- nonReserveMints = initialSupply - remainingSupply - reservesMinted
199
- pendingReserves = ceil(nonReserveMints / reserveFrequency) - reservesMinted
200
- ```
201
-
202
- **State changes**:
203
- 1. `numberOfReservesMintedFor[hook][tierId]` incremented by `count`.
204
- 2. `_storedTierOf[hook][tierId].remainingSupply` decremented by `count`.
205
- 3. NFTs minted to `reserveBeneficiaryOf(hook, tierId)`.
206
- 4. `tierBalanceOf[hook][beneficiary][tierId]` incremented by `count`.
207
-
208
- **Events**:
209
- - `MintReservedNft(tokenId, tierId, beneficiary, caller)` -- one per reserve NFT minted.
210
-
211
- **Edge cases**:
212
- - **Paused**: Reverts with `JB721TiersHook_MintReserveNftsPaused`.
213
- - **Count > pending**: Reverts with `JB721TiersHookStore_InsufficientPendingReserves`.
214
- - **No reserve beneficiary**: `_numberOfPendingReservesFor` returns 0 when `reserveBeneficiaryOf` is `address(0)`. Effectively, no reserves can be minted.
215
- - **Removed tier**: Reserves can still be minted from removed tiers.
216
- - **Changing default beneficiary**: If the default beneficiary is changed (via `useReserveBeneficiaryAsDefault` on a new tier), pending reserves for tiers using the default are redirected to the new beneficiary.
217
-
218
- ---
219
-
220
- ## 6. Set Discount Percent
221
-
222
- The project owner adjusts the discount on a tier's mint price. Does not affect cash-out weight.
223
-
224
- **Entry point**: `JB721TiersHook.setDiscountPercentOf(uint256 tierId, uint256 discountPercent)` (external).
225
-
226
- **Batch entry point**: `JB721TiersHook.setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs)` (external).
227
-
228
- **Who can call**: Hook owner, or an operator with `SET_721_DISCOUNT_PERCENT` permission from the hook owner.
229
-
230
- **Parameters**:
231
- - `uint256 tierId` -- the tier to update.
232
- - `uint256 discountPercent` -- the new discount. Out of 200 (not 100). 0 = no discount, 100 = 50% off, 200 = free.
233
-
234
- **State changes**:
235
- 1. `_storedTierOf[hook][tierId].discountPercent` updated.
236
-
237
- **Events**:
238
- - `SetDiscountPercent(tierId, discountPercent, caller)`.
239
-
240
- **Edge cases**:
241
- - **`discountPercent > 200`**: Reverts with `JB721TiersHookStore_DiscountPercentExceedsBounds`.
242
- - **Increasing discount when `cannotIncreaseDiscountPercent = true`**: Reverts with `JB721TiersHookStore_DiscountPercentIncreaseNotAllowed`.
243
- - **Decreasing discount**: Always allowed, even when `cannotIncreaseDiscountPercent` is set.
244
- - **Free mints (200)**: Effective price becomes 0. Cash-out weight still uses original price.
245
- - **Split amounts**: `calculateSplitAmounts` uses the discounted price, so splits are proportional to what the payer actually pays.
246
-
247
- ---
248
-
249
- ## 7. Manual Owner Mint
250
-
251
- The project owner directly mints NFTs from tiers that have `allowOwnerMint = true`, bypassing payment.
252
-
253
- **Entry point**: `JB721TiersHook.mintFor(uint16[] calldata tierIds, address beneficiary)` (external).
254
-
255
- **Who can call**: Hook owner, or an operator with `MINT_721` permission from the hook owner.
256
-
257
- **Parameters**:
258
- - `uint16[] tierIds` -- the tiers to mint from. One NFT per entry. Can repeat tiers.
259
- - `address beneficiary` -- the address that receives the NFTs.
260
-
261
- **State changes**:
262
- 1. `_storedTierOf[hook][tierId].remainingSupply` decremented by 1 per mint.
263
- 2. NFTs minted to beneficiary.
264
- 3. `tierBalanceOf[hook][beneficiary][tierId]` incremented.
265
-
266
- **Events**:
267
- - `Mint(tokenId, tierId, beneficiary, 0, caller)` -- `totalAmountPaid` is 0 for manual mints.
268
-
269
- **Edge cases**:
270
- - **`allowOwnerMint = false`**: Reverts with `JB721TiersHookStore_CantMintManually`.
271
- - **Tier removed**: Reverts with `JB721TiersHookStore_TierRemoved`.
272
- - **Tier sold out**: Reverts with `JB721TiersHookStore_InsufficientSupplyRemaining`.
273
- - **Reserve supply protection**: Enforced. If minting would steal a reserved slot, reverts.
274
- - **No price check**: `amount` is passed as `type(uint256).max`, so price validation always passes.
275
- - **No reserve frequency allowed**: Tiers with `allowOwnerMint = true` cannot have `reserveFrequency > 0` (enforced at tier creation).
276
-
277
- ---
278
-
279
- ## 8. Adjust Tiers (Combined Add + Remove)
280
-
281
- A single call that both adds new tiers and removes existing tiers.
282
-
283
- **Entry point**: `JB721TiersHook.adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove)` (external).
284
-
285
- **Who can call**: Hook owner, or an operator with `ADJUST_721_TIERS` permission from the hook owner.
286
-
287
- **Execution order**:
288
- 1. Removals are processed first (bitmap marked).
289
- 2. Additions are processed second (new tiers stored and sorted).
290
- 3. Split groups are set for any new tiers with splits.
291
-
292
- This ordering means a tier can be removed and a replacement tier added in the same transaction. The removed tier's ID persists; the new tier gets a fresh sequential ID.
293
-
294
- See [Journey 3: Add Tiers](#3-add-tiers) and [Journey 4: Remove Tiers](#4-remove-tiers) for details on each operation.
295
-
296
- ---
297
-
298
- ## 9. Deploy Hook (Standalone)
299
-
300
- Deploy a 721 tiers hook for an existing project.
301
-
302
- **Entry point**: `JB721TiersHookDeployer.deployHookFor(uint256 projectId, JBDeploy721TiersHookConfig calldata deployTiersHookConfig, bytes32 salt)` (external).
303
-
304
- **Who can call**: Anyone. The deployer is permissionless. The caller becomes the initial hook owner.
305
-
306
- **Parameters**:
307
- - `uint256 projectId` -- the project to associate the hook with.
308
- - `JBDeploy721TiersHookConfig deployTiersHookConfig`:
309
- - `string name` -- collection name.
310
- - `string symbol` -- collection symbol.
311
- - `string baseUri` -- base URI for IPFS token URIs.
312
- - `IJB721TokenUriResolver tokenUriResolver` -- custom URI resolver (or address(0)).
313
- - `string contractUri` -- collection-level metadata URI.
314
- - `JB721InitTiersConfig tiersConfig`:
315
- - `JB721TierConfig[] tiers` -- initial tiers (sorted by category).
316
- - `uint32 currency` -- pricing currency (`uint32(uint160(tokenAddress))` for concrete, or abstract like 1=ETH, 2=USD).
317
- - `uint8 decimals` -- pricing decimals (must be <= 18).
318
- - `JB721TiersHookFlags flags` -- collection-level behavior flags.
319
- - `bytes32 salt` -- for deterministic deployment (bytes32(0) for non-deterministic).
320
-
321
- **State changes**:
322
- 1. A minimal proxy clone of `HOOK` is deployed (via `LibClone.clone` or `LibClone.cloneDeterministic`).
323
- 2. `initialize()` is called on the clone: sets `PROJECT_ID`, name, symbol, pricing context, tiers, flags.
324
- 3. Ownership transferred to `_msgSender()` (inside `initialize`), then to the external caller (in `deployHookFor`).
325
- 4. Clone registered with `JBAddressRegistry`.
326
-
327
- **Events**:
328
- - `HookDeployed(projectId, newHook, caller)`.
329
- - `AddTier(tierId, tierConfig, caller)` -- one per initial tier.
330
- - `SetDefaultReserveBeneficiary(hook, newBeneficiary, caller)` -- if any initial tier has `useReserveBeneficiaryAsDefault = true`.
331
-
332
- **Edge cases**:
333
- - **Re-initialization**: Reverts with `JB721TiersHook_AlreadyInitialized` if `_initialized` is already true.
334
- - **`projectId == 0`**: Reverts with `JB721TiersHook_NoProjectId`.
335
- - **`decimals > 18`**: Reverts with `JB721TiersHook_InvalidPricingDecimals`.
336
- - **Deterministic salt collision**: Reverts at the EVM level (CREATE2 collision).
337
- - **Implementation contract**: The implementation contract's constructor sets `_initialized = true`, so no one can call `initialize` on it.
338
-
339
- ---
340
-
341
- ## 10. Deploy Project + Hook
342
-
343
- Launch a new Juicebox project with a 721 tiers hook attached, all in one transaction.
344
-
345
- **Entry point**: `JB721TiersHookProjectDeployer.launchProjectFor(address owner, JBDeploy721TiersHookConfig calldata deployTiersHookConfig, JBLaunchProjectConfig calldata launchProjectConfig, IJBController controller, bytes32 salt)` (external).
346
-
347
- **Who can call**: Anyone. The deployer is permissionless. The `owner` parameter receives the project ERC-721.
348
-
349
- **Parameters**:
350
- - `address owner` -- receives the project ERC-721.
351
- - `JBDeploy721TiersHookConfig deployTiersHookConfig` -- hook config (see Journey 9).
352
- - `JBLaunchProjectConfig launchProjectConfig`:
353
- - `string projectUri` -- project metadata URI.
354
- - `JBPayDataHookRulesetConfig[] rulesetConfigurations` -- rulesets with `useDataHookForPay: true` hardcoded.
355
- - `JBTerminalConfig[] terminalConfigurations` -- which terminals to use.
356
- - `string memo` -- emitted in event.
357
- - `IJBController controller` -- the controller to launch with.
358
- - `bytes32 salt` -- for deterministic hook deployment.
359
-
360
- **Key behavior**: `useDataHookForPay` is always set to `true` in each ruleset configuration. The deployer wraps `JBPayDataHookRulesetConfig` (which omits `useDataHookForPay` and `dataHook`) into `JBRulesetConfig` (which includes them), hardcoding the hook address as the data hook.
361
-
362
- **State changes**:
363
- 1. Hook deployed and initialized (see Journey 9).
364
- 2. Project launched via `controller.launchProjectFor`.
365
- 3. Hook ownership transferred to the project (not the caller): `JBOwnable(hook).transferOwnershipToProject(projectId)`.
366
-
367
- **Events**:
368
- - All events from hook deployment (Journey 9).
369
- - Project launch events from the controller.
370
-
371
- **Edge cases**:
372
- - **Project ID prediction**: Uses `DIRECTORY.PROJECTS().count() + 1` optimistically. If another project is launched in the same block before this transaction, the prediction is wrong and the call reverts.
373
- - **Hook ownership**: Transferred to the project, meaning the project owner (ERC-721 holder) controls the hook.
374
-
375
- ---
376
-
377
- ## 11. Set Token URI Resolver
378
-
379
- Set a custom contract that resolves token URIs for all NFTs in the collection.
380
-
381
- **Entry point**: `JB721TiersHook.setMetadata(...)` (external). The `tokenUriResolver` parameter is an optional contract that can override the default IPFS-based token URI generation. Pass a contract address to set a custom resolver, `address(0)` to clear it and revert to the default, or the sentinel value `address(this)` to leave it unchanged.
382
-
383
- **Who can call**: Hook owner, or an operator with `SET_721_METADATA` permission from the hook owner.
384
-
385
- **Parameters** (relevant subset):
386
- - `IJB721TokenUriResolver tokenUriResolver` -- the new resolver. Pass `IJB721TokenUriResolver(address(this))` to skip (no change). Pass `IJB721TokenUriResolver(address(0))` to clear.
387
-
388
- **State changes**:
389
- 1. `JB721TiersHookStore.tokenUriResolverOf[hook]` updated.
390
-
391
- **Events**:
392
- - `SetTokenUriResolver(resolver, caller)`.
393
-
394
- **Behavior**:
395
- - When set, `tokenURI(tokenId)` calls `resolver.tokenUriOf(nft, tokenId)` instead of using IPFS URIs.
396
- - When cleared (set to `address(0)`), falls back to IPFS-based URIs via `JBIpfsDecoder`.
397
- - The sentinel value for "skip" is `address(this)` (the hook's own address), checked in `tokenURI()`.
398
-
399
- **Edge cases**:
400
- - **Malicious resolver**: Could revert (blocking metadata reads for marketplaces) or return misleading URIs. Cannot affect funds.
401
- - **View function only**: `tokenURI` is a view function, so resolver calls cannot modify state.
402
-
403
- ---
404
-
405
- ## 12. Clean Tiers
406
-
407
- Reorganize the sorted tier linked list to skip removed tiers. Improves iteration efficiency.
408
-
409
- **Entry point**: `JB721TiersHookStore.cleanTiers(address hook)` (external, permissionless).
410
-
411
- **Who can call**: Anyone. This function is permissionless.
412
-
413
- **Parameters**:
414
- - `address hook` -- the hook contract whose tier list to clean.
415
-
416
- **State changes**:
417
- 1. `_tierIdAfter[hook][tierId]` mappings updated to skip removed tier IDs in the sorted sequence.
418
-
419
- **Events**:
420
- - `CleanTiers(hook, caller)`.
421
-
422
- **Edge cases**:
423
- - **Permissionless**: Anyone can call this. It is idempotent and only affects iteration ordering, not tier data or economics.
424
- - **No removed tiers**: No-op (mappings already correct).
425
- - **Griefing**: Repeatedly calling `cleanTiers` wastes gas but has no economic impact.
426
-
427
- ---
428
-
429
- ## 13. Set Metadata (Name, Symbol, URIs)
430
-
431
- Update the collection's name, symbol, base URI, contract URI, or per-tier IPFS URI.
432
-
433
- **Entry point**: `JB721TiersHook.setMetadata(string calldata name, string calldata symbol, string calldata baseUri, string calldata contractUri, IJB721TokenUriResolver tokenUriResolver, uint256 encodedIPFSUriTierId, bytes32 encodedIPFSUri)` (external).
434
-
435
- **Who can call**: Hook owner, or an operator with `SET_721_METADATA` permission from the hook owner.
436
-
437
- **Parameters**:
438
- - `string name` -- new collection name. Empty string = no change.
439
- - `string symbol` -- new collection symbol. Empty string = no change.
440
- - `string baseUri` -- new base URI. Empty string = no change.
441
- - `string contractUri` -- new contract URI. Empty string = no change.
442
- - `IJB721TokenUriResolver tokenUriResolver` -- new URI resolver. `address(this)` = no change. `address(0)` = clear.
443
- - `uint256 encodedIPFSUriTierId` -- tier ID to update IPFS URI for. 0 = no change.
444
- - `bytes32 encodedIPFSUri` -- new encoded IPFS URI. `bytes32(0)` = no change (combined with tierId == 0).
445
-
446
- **State changes**:
447
- 1. `ERC721._name` and/or `ERC721._symbol` updated (if non-empty).
448
- 2. `baseURI` updated (if non-empty).
449
- 3. `contractURI` updated (if non-empty).
450
- 4. `tokenUriResolverOf[hook]` updated (if not sentinel).
451
- 5. `encodedIPFSUriOf[hook][tierId]` updated (if both tierId != 0 and encodedIPFSUri != bytes32(0)).
452
-
453
- **Events**:
454
- - `SetName(name, caller)` -- if name changed.
455
- - `SetSymbol(symbol, caller)` -- if symbol changed.
456
- - `SetBaseUri(baseUri, caller)` -- if base URI changed.
457
- - `SetContractUri(uri, caller)` -- if contract URI changed.
458
- - `SetTokenUriResolver(resolver, caller)` -- if resolver changed.
459
- - `SetEncodedIPFSUri(tierId, encodedUri, caller)` -- if IPFS URI changed.
460
-
461
- ---
462
-
463
- ## 14. Transfer NFT
464
-
465
- Transfer an NFT between addresses. Subject to per-tier and per-ruleset pause controls.
466
-
467
- **Entry point**: Standard ERC-721 `transferFrom(address from, address to, uint256 tokenId)` or `safeTransferFrom(...)`.
468
-
469
- **Who can call**: Standard ERC-721 (token owner, approved address, or approved-for-all operator).
470
-
471
- **State changes**:
472
- 1. ERC-721 ownership updated.
473
- 2. `JB721TiersHookStore.tierBalanceOf[hook][from][tierId]` decremented.
474
- 3. `JB721TiersHookStore.tierBalanceOf[hook][to][tierId]` incremented.
475
- 4. `_firstOwnerOf[tokenId]` set to `from` (if not already set and `from != address(0)`).
476
-
477
- **Transfer pause check** (in `_update` override):
478
- 1. Look up the tier via `STORE.tierOfTokenId(hook, tokenId, false)`.
479
- 2. If `tier.transfersPausable == true`:
480
- - Fetch current ruleset via `RULESETS.currentOf(PROJECT_ID)`.
481
- - Check `JB721TiersRulesetMetadataResolver.transfersPaused(metadata)` (bit 0).
482
- - If paused and `to != address(0)` (not a burn): revert with `JB721TiersHook_TierTransfersPaused`.
483
-
484
- **Events**:
485
- - ERC-721 `Transfer(from, to, tokenId)`.
486
-
487
- **Edge cases**:
488
- - **Tier has `transfersPausable = false`**: Transfers can never be paused for this tier, regardless of ruleset settings.
489
- - **Burns (to == address(0))**: Never blocked by transfer pause. Burns always go through.
490
- - **Mints (from == address(0))**: The pause check is skipped for mints (`from != address(0)` guard).
491
- - **Cross-contract call**: Every transfer triggers `STORE.recordTransferForTier` to update `tierBalanceOf`.
492
-
493
- ---
494
-
495
- ## 15. Set Splits for Tiers
496
-
497
- Configure how a percentage of a tier's effective price is distributed to split recipients when NFTs are minted from that tier.
498
-
499
- **Entry point**: Splits are set during tier creation via `adjustTiers` (see Journey 3). The `splits` field in `JB721TierConfig` defines the split recipients.
500
-
501
- **Who can call**: Splits are configured as part of `adjustTiers`, so the same permission applies: hook owner or an operator with `ADJUST_721_TIERS` permission. Split distribution during payment is automatic and permissionless.
502
-
503
- **Split group ID**: `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`. This is stored in `JBSplits` with `rulesetId = 0` (always active).
504
-
505
- **How it works during payment**:
506
- 1. `beforePayRecordedWith` calls `JB721TiersHookLib.calculateSplitAmounts` to compute per-tier split amounts based on `effectivePrice * splitPercent / SPLITS_TOTAL_PERCENT`.
507
- 2. The `totalSplitAmount` is forwarded to the hook via `hookSpecifications[0].amount`.
508
- 3. The terminal reduces the project's recorded balance by the split amount.
509
- 4. `afterPayRecordedWith` calls `JB721TiersHookLib.distributeAll` to distribute the forwarded funds.
510
- 5. Each tier's split group is read from `JBSplits` and distributed via `_distributeSingleSplit`.
511
- 6. Leftover (from rounding or splits with no valid recipient) is added back to the project's balance.
512
-
513
- **Split recipient priority**: `split.hook` > `split.projectId` > `split.beneficiary`. If none are set, the split's share stays as leftover and goes to the project's balance.
514
-
515
- **Parameters** (per `JBSplit`):
516
- - `bool preferAddToBalance` -- for project splits, use `addToBalanceOf` instead of `pay`.
517
- - `uint32 percent` -- percentage of the remaining amount (sequential, not parallel).
518
- - `uint64 projectId` -- target project (0 = no project split).
519
- - `address beneficiary` -- direct recipient (if no hook and no projectId).
520
- - `IJBSplitHook hook` -- split hook contract (highest priority).
521
- - `uint48 lockedUntil` -- timestamp until which this split is locked and cannot be modified.
522
-
523
- **Events** (during split distribution on payment):
524
- - `SplitPayoutReverted(projectId, split, amount, reason, caller)` -- if an individual split recipient's payout reverts (funds become leftover, routed to project balance).
525
- - `AddToBalanceReverted(projectId, token, amount, reason)` -- if the `addToBalanceOf` call for leftover funds reverts after distribution.
526
-
527
- **Edge cases**:
528
- - **`splitPercent` > SPLITS_TOTAL_PERCENT**: Validated at tier creation in `recordAddTiers`. Reverts with `JB721TiersHookStore_SplitPercentExceedsBounds` if `splitPercent` exceeds `JBConstants.SPLITS_TOTAL_PERCENT` (1e9).
529
- - **Weight adjustment**: When splits route funds away, `calculateWeight` reduces the mint weight so payers receive fewer project tokens proportional to the split fraction. This can be disabled with `issueTokensForSplits = true`.
530
- - **Cross-currency**: Split amounts are calculated in the pricing currency, then converted to the payment token denomination. Rounding occurs at each step.
531
- - **ERC-20 tokens**: The library pulls tokens from the terminal via `safeTransferFrom`, distributes them, and approves terminals for project splits via `forceApprove`.
86
+ - Use [nana-core-v6](../nana-core-v6/USER_JOURNEYS.md) for the treasury, ruleset, and permission surfaces the hook plugs into.
87
+ - Use [banny-retail-v6](../banny-retail-v6/USER_JOURNEYS.md), [croptop-core-v6](../croptop-core-v6/USER_JOURNEYS.md), and [revnet-core-v6](../revnet-core-v6/USER_JOURNEYS.md) for product layers built on this hook.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.28",
3
+ "version": "0.0.30",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,28 @@
1
+ # 721 Hook Operations
2
+
3
+ ## Deployment Surface
4
+
5
+ - [`src/JB721TiersHookDeployer.sol`](../src/JB721TiersHookDeployer.sol) clones and initializes hooks for existing projects.
6
+ - [`src/JB721TiersHookProjectDeployer.sol`](../src/JB721TiersHookProjectDeployer.sol) combines hook deployment with project launch or ruleset setup.
7
+ - [`script/Deploy.s.sol`](../script/Deploy.s.sol) is the deployment entry point when you need current deployment wiring rather than abstract runtime behavior.
8
+
9
+ ## Change Checklist
10
+
11
+ - If you edit hook initialization, verify deployer config structs and project-launch helpers still encode the same assumptions.
12
+ - If you edit tier config or metadata behavior, inspect [`src/structs/`](../src/structs/) and the corresponding interfaces in [`src/interfaces/`](../src/interfaces/).
13
+ - If you touch permissions, verify the caller path and permission constants still line up with the downstream ecosystem package that defines them.
14
+ - If you touch URI behavior, confirm whether the issue belongs in this repo or in a downstream resolver contract that the hook calls.
15
+
16
+ ## Common Failure Modes
17
+
18
+ - Payment metadata decodes to tier IDs that no longer match the intended mint path.
19
+ - Reserve or owner-mint changes accidentally violate the store's supply guarantees.
20
+ - Hook-side assumptions drift from deployer-side assumptions, especially around initialization flags and pricing context.
21
+ - A change looks metadata-only but actually changes treasury behavior because split, credit, or cash-out logic moved with it.
22
+
23
+ ## Useful Proof Points
24
+
25
+ - [`test/Fork.t.sol`](../test/Fork.t.sol) for live-integration assumptions.
26
+ - [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for known edge cases the repo authors considered worth pinning down.
27
+ - [`test/unit/`](../test/unit/) when you need a narrow function-level proof before editing a broad runtime path.
28
+ - [`script/helpers/`](../script/helpers/) when a deployment or launch question is really about config assembly rather than contract behavior.