@bananapus/721-hook-v6 0.0.34 → 0.0.36

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 (36) hide show
  1. package/ADMINISTRATION.md +62 -164
  2. package/ARCHITECTURE.md +59 -44
  3. package/AUDIT_INSTRUCTIONS.md +35 -32
  4. package/README.md +22 -3
  5. package/RISKS.md +7 -1
  6. package/SKILLS.md +8 -2
  7. package/USER_JOURNEYS.md +144 -49
  8. package/foundry.toml +2 -0
  9. package/package.json +1 -1
  10. package/references/operations.md +7 -3
  11. package/references/runtime.md +5 -4
  12. package/src/JB721TiersHook.sol +6 -6
  13. package/src/JB721TiersHookProjectDeployer.sol +0 -1
  14. package/src/JB721TiersHookStore.sol +1 -2
  15. package/src/abstract/JB721Hook.sol +0 -1
  16. package/src/interfaces/IJB721TiersHook.sol +0 -2
  17. package/src/interfaces/IJB721TiersHookStore.sol +1 -1
  18. package/src/libraries/JB721Constants.sol +0 -1
  19. package/src/structs/JB721InitTiersConfig.sol +0 -1
  20. package/src/structs/JB721Tier.sol +0 -2
  21. package/src/structs/JB721TierConfig.sol +0 -2
  22. package/src/structs/JB721TierConfigFlags.sol +0 -1
  23. package/src/structs/JB721TierFlags.sol +0 -1
  24. package/src/structs/JB721TiersHookFlags.sol +0 -1
  25. package/src/structs/JB721TiersMintReservesConfig.sol +0 -1
  26. package/src/structs/JB721TiersRulesetMetadata.sol +0 -1
  27. package/src/structs/JB721TiersSetDiscountPercentConfig.sol +0 -1
  28. package/src/structs/JBBitmapWord.sol +0 -1
  29. package/src/structs/JBDeploy721TiersHookConfig.sol +0 -1
  30. package/src/structs/JBLaunchProjectConfig.sol +0 -1
  31. package/src/structs/JBLaunchRulesetsConfig.sol +0 -1
  32. package/src/structs/JBPayDataHookRulesetConfig.sol +0 -1
  33. package/src/structs/JBPayDataHookRulesetMetadata.sol +0 -1
  34. package/src/structs/JBQueueRulesetsConfig.sol +0 -1
  35. package/src/structs/JBStored721Tier.sol +0 -1
  36. package/test/TestCheckpoints.t.sol +16 -4
package/ADMINISTRATION.md CHANGED
@@ -1,189 +1,87 @@
1
1
  # Administration
2
2
 
3
- Admin privileges and their scope in nana-721-hook-v6.
4
-
5
3
  ## At A Glance
6
4
 
7
5
  | 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.
6
+ | --- | --- |
7
+ | Scope | Per-hook ownership, delegated 721 administration, and hook deployment flows |
8
+ | Control posture | Per-instance owner or project-owner control with delegated `JBPermissions` |
9
+ | Highest-risk actions | Tier adjustments, owner minting, metadata changes, and misassigned hook ownership |
10
+ | Recovery posture | Project-specific config can sometimes be superseded with new rulesets, but bad clone wiring usually means replacement hooks |
20
11
 
21
- ## One-Way Or High-Risk Actions
12
+ ## Purpose
22
13
 
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.
14
+ `nana-721-hook-v6` is administered per hook instance. The effective admin is the hook owner resolved through `JBOwnable`, plus any operators granted specific `JBPermissions`. The dangerous surfaces are tier adjustment, metadata changes, owner minting, discount changes, and hook deployment ownership.
26
15
 
27
- ## Recovery Notes
16
+ ## Control Model
28
17
 
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.
18
+ - Each hook instance has its own owner.
19
+ - Ownership can follow an EOA or a Juicebox project NFT through `JBOwnable`.
20
+ - Fine-grained operator delegation runs through `JBPermissions`.
21
+ - Deployers are permissionless for new hooks, but existing-project launch and queue flows are permission-gated.
22
+ - The store has no owner role; it trusts `msg.sender`-keyed namespaces.
31
23
 
32
24
  ## Roles
33
25
 
34
- ### Hook Owner (JBOwnable)
35
-
36
- - **Assigned by**: `initialize()` transfers ownership to the caller. When deployed via `JB721TiersHookProjectDeployer.launchProjectFor()`, ownership is transferred to the project NFT, meaning the project owner controls the hook.
37
- - **Scope**: Per-hook instance. Each cloned hook has its own independent owner.
38
- - **Inheritance**: `JBOwnable` supports both EOA ownership and project-based ownership (owner = holder of the project's ERC-721 NFT). When ownership is transferred to a project via `transferOwnershipToProject()`, whoever owns that project NFT becomes the hook's owner.
39
-
40
- ### Permission Operators
41
-
42
- - **Assigned by**: The hook owner grants permissions via the `JBPermissions` contract.
43
- - **Scope**: Per-project. Operators can be granted specific permission IDs scoped to the hook's `PROJECT_ID`.
44
- - **How it works**: Each privileged function calls `_requirePermissionFrom(account: owner(), projectId: PROJECT_ID, permissionId: ...)`. This passes if the caller IS the owner, OR if the caller has been granted the specified permission ID by the owner for the project.
45
-
46
- ### Terminal (Protocol-Level Caller)
47
-
48
- - **Assigned by**: The project's `JBDirectory` configuration.
49
- - **Scope**: Only a contract registered as a terminal for the hook's project in `JBDirectory` can call `afterPayRecordedWith()` and `afterCashOutRecordedWith()`.
50
- - **Verification**: `DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender))` is checked in `JB721Hook.sol`.
51
-
52
- ### Store Callers (msg.sender Trust Model)
53
-
54
- - **Assigned by**: Implicit. `JB721TiersHookStore` trusts `msg.sender` as the hook contract.
55
- - **Scope**: All `record*` functions in the store use `msg.sender` as the hook address key. Any contract can call the store, but state changes are scoped to `msg.sender`'s own data namespace.
56
- - **Why this is safe**: Each hook clone has its own address, and the store keys all data by `[msg.sender][tierId]`. A malicious contract calling the store can only modify its own namespace.
57
-
58
- ## Privileged Functions
59
-
60
- ### JB721TiersHook
61
-
62
- | Function | Permission ID | Checked Against | What It Does |
63
- |----------|--------------|-----------------|--------------|
64
- | `adjustTiers()` | `ADJUST_721_TIERS` | `owner()` | Adds new tiers and/or soft-removes existing tiers. Sets tier split groups in JBSplits. |
65
- | `mintFor()` | `MINT_721` | `owner()` | Manually mints NFTs from tiers that have `flags.allowOwnerMint` enabled. Bypasses price checks (passes `type(uint256).max` as amount). |
66
- | `setDiscountPercentOf()` | `SET_721_DISCOUNT_PERCENT` | `owner()` | Sets the discount percentage for a single tier. |
67
- | `setDiscountPercentsOf()` | `SET_721_DISCOUNT_PERCENT` | `owner()` | Batch-sets discount percentages for multiple tiers. |
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. |
69
- | `initialize()` | None (one-time) | `_initialized` flag check | Initializes a cloned hook. Can only be called once. Transfers ownership to caller on completion. |
70
-
71
- ### JB721TiersHookProjectDeployer
72
-
73
- | Function | Permission ID | Checked Against | What It Does |
74
- |----------|--------------|-----------------|--------------|
75
- | `launchProjectFor()` | None | Anyone can call | Creates a new project with a 721 hook. Ownership goes to the specified `owner` address. |
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. |
78
-
79
- ### JB721TiersHookDeployer
80
-
81
- | Function | Permission ID | Checked Against | What It Does |
82
- |----------|--------------|-----------------|--------------|
83
- | `deployHookFor()` | None | Anyone can call | Clones and initializes a new hook instance. Ownership starts with the deployer contract, then is transferred to `msg.sender`. |
84
-
85
- ### JB721Hook (Abstract Base)
86
-
87
- | Function | Required Caller | What It Does |
88
- |----------|----------------|--------------|
89
- | `afterPayRecordedWith()` | Project terminal | Processes payment, mints NFTs. Verifies caller via `DIRECTORY.isTerminalOf()`. |
90
- | `afterCashOutRecordedWith()` | Project terminal | Burns NFTs on cash out. Verifies caller via `DIRECTORY.isTerminalOf()` and that `msg.value == 0`. |
91
-
92
- ### JB721TiersHookStore (No Access Control -- msg.sender Keyed)
93
-
94
- | Function | Caller | What It Does |
95
- |----------|--------|--------------|
96
- | `recordAddTiers()` | Hook contract | Adds tiers to the caller's namespace. Category sort order enforced. |
97
- | `recordRemoveTierIds()` | Hook contract | Marks tiers as removed in bitmap. Respects `flags.cantBeRemoved` flag. |
98
- | `recordMint()` | Hook contract | Records mints, decrements supply, enforces price and reserve checks. |
99
- | `recordMintReservesFor()` | Hook contract | Mints reserved NFTs from a tier. |
100
- | `recordBurn()` | Hook contract | Increments burn counter for token IDs. |
101
- | `recordFlags()` | Hook contract | Sets behavioral flags for the caller's hook. |
102
- | `recordSetTokenUriResolver()` | Hook contract | Sets the token URI resolver. |
103
- | `recordSetEncodedIPFSUriOf()` | Hook contract | Sets the encoded IPFS URI for a tier. |
104
- | `recordSetDiscountPercentOf()` | Hook contract | Updates a tier's discount percent. Enforces bounds and `flags.cantIncreaseDiscountPercent`. |
105
- | `recordTransferForTier()` | Hook contract | Updates per-tier balance tracking on transfer. |
106
- | `cleanTiers()` | Anyone | Reorganizes the tier sorting linked list to skip removed tiers. Pure bookkeeping, no value at risk. |
107
-
108
- ## Permission System
109
-
110
- Permissions flow through two mechanisms:
111
-
112
- 1. **JBOwnable** (`JB721TiersHook` inherits from it): The hook has a single `owner()` that can be an EOA or a Juicebox project. When owned by a project, the holder of that project's ERC-721 NFT is the effective owner.
26
+ | Role | How Assigned | Scope | Notes |
27
+ | --- | --- | --- | --- |
28
+ | Hook owner | `JBOwnable.owner()` | Per hook | May resolve dynamically through a project NFT |
29
+ | Hook operator | Granted by `JBPermissions` | Per project | Usually `ADJUST_721_TIERS`, `MINT_721`, `SET_721_METADATA`, `SET_721_DISCOUNT_PERCENT` |
30
+ | Project owner | `JBProjects.ownerOf(projectId)` | Per project | Relevant for project-deployer flows |
31
+ | Terminal | `JBDirectory` routing | Per project | Can call pay and cash-out hook entrypoints |
32
+ | Deployer caller | Anyone | Per deployment | Can deploy new standalone hooks |
113
33
 
114
- 2. **JBPermissions** (protocol-wide permission registry): The owner can grant specific permission IDs to operator addresses. Each permission is scoped to a `(operator, account, projectId, permissionId)` tuple. The `ROOT` permission (ID 1) grants all permissions.
34
+ ## Privileged Surfaces
115
35
 
116
- The `_requirePermissionFrom()` check (inherited from `JBOwnable` via `JBPermissioned`) passes if:
117
- - `msg.sender == account` (the owner themselves), OR
118
- - `JBPermissions.hasPermission(msg.sender, account, projectId, permissionId)` returns true.
36
+ | Contract | Function | Who Can Call | Effect |
37
+ | --- | --- | --- | --- |
38
+ | `JB721TiersHook` | `adjustTiers(...)` | Owner or `ADJUST_721_TIERS` operator | Adds or removes tiers and updates split groups |
39
+ | `JB721TiersHook` | `mintFor(...)` | Owner or `MINT_721` operator | Owner mint path, subject to tier flags |
40
+ | `JB721TiersHook` | `setMetadata(...)` | Owner or `SET_721_METADATA` operator | Updates collection-level metadata and resolver references |
41
+ | `JB721TiersHook` | `setDiscountPercentOf(...)`, `setDiscountPercentsOf(...)` | Owner or `SET_721_DISCOUNT_PERCENT` operator | Changes discount settings where allowed |
42
+ | `JB721TiersHook` | `initialize(...)` | Anyone once per clone | One-time hook initialization and ownership setup |
43
+ | `JB721TiersHookProjectDeployer` | `launchRulesetsFor(...)` | Project owner or relevant delegates | Launches hook-backed rulesets for an existing project |
44
+ | `JB721TiersHookProjectDeployer` | `queueRulesetsOf(...)` | Project owner or `QUEUE_RULESETS` delegate | Queues hook-backed rulesets for an existing project |
119
45
 
120
- ### Permission IDs Used
46
+ ## Immutable And One-Way
121
47
 
122
- | Permission ID | Constant Name | Used By |
123
- |--------------|---------------|---------|
124
- | `JBPermissionIds.ADJUST_721_TIERS` | `ADJUST_721_TIERS` | `adjustTiers()` |
125
- | `JBPermissionIds.MINT_721` | `MINT_721` | `mintFor()` |
126
- | `JBPermissionIds.SET_721_DISCOUNT_PERCENT` | `SET_721_DISCOUNT_PERCENT` | `setDiscountPercentOf()`, `setDiscountPercentsOf()` |
127
- | `JBPermissionIds.SET_721_METADATA` | `SET_721_METADATA` | `setMetadata()` |
128
- | `JBPermissionIds.QUEUE_RULESETS` | `QUEUE_RULESETS` | `launchRulesetsFor()`, `queueRulesetsOf()` |
129
- | `JBPermissionIds.SET_TERMINALS` | `SET_TERMINALS` | `launchRulesetsFor()` |
48
+ - Implementation constructor dependencies are immutable.
49
+ - Clone initialization is one-time.
50
+ - Per-tier price, reserve frequency, and several flags are effectively set-once semantics.
51
+ - Ownership transfers change every permission check because privileged functions check against `owner()`.
130
52
 
131
- ## Immutable Configuration
53
+ ## Operational Notes
132
54
 
133
- The following are set at deploy/initialization time and **cannot be changed afterward**:
55
+ - Grant narrow per-project permissions instead of owner-equivalent access where possible.
56
+ - Treat `adjustTiers(...)` as an economic change, not just content management.
57
+ - Verify hook ownership and pricing context before publishing a clone address.
58
+ - When this hook is wrapped by deployers, review hook-order assumptions as part of administration.
134
59
 
135
- | Property | Set In | Scope |
136
- |----------|--------|-------|
137
- | `DIRECTORY` | Constructor | Which terminal/controller directory is trusted |
138
- | `PRICES` | Constructor | Which prices contract is used for cross-currency conversions |
139
- | `RULESETS` | Constructor | Which rulesets contract is consulted |
140
- | `STORE` | Constructor | Which store manages tier data |
141
- | `SPLITS` | Constructor | Which splits contract manages tier split groups |
142
- | `METADATA_ID_TARGET` | Constructor | The address used for metadata ID derivation (original implementation address for clones) |
143
- | `PROJECT_ID` | `initialize()` | Which project this hook belongs to |
144
- | Pricing context (currency, decimals) | `initialize()` | Packed into `_packedPricingContext` -- the token denomination for tier prices |
145
- | `JB721TiersHookFlags` | `initialize()` | `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending`, `issueTokensForSplits` |
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 |
148
- | Per-tier `reserveFrequency` | `recordAddTiers()` | How often reserve NFTs accrue |
149
- | Per-tier `initialSupply` | `recordAddTiers()` | Maximum number of NFTs mintable from the tier |
150
- | Per-tier `price` | `recordAddTiers()` | The base price (and cash-out weight) of NFTs in the tier |
151
- | Per-tier `category` | `recordAddTiers()` | The category grouping for sort order |
60
+ ## Machine Notes
152
61
 
153
- ## Clone Pattern
154
-
155
- `JB721TiersHook` is deployed as an implementation contract and then cloned via `LibClone.clone()` in `JB721TiersHookDeployer`. Each clone is a minimal proxy that delegates all calls to the implementation.
62
+ - Do not assume `owner()` is a static address; it may resolve through a project NFT.
63
+ - Treat `src/JB721TiersHook.sol` and `src/JB721TiersHookStore.sol` together as the control-plane source of truth.
64
+ - If a downstream deployer changes hook ownership or ruleset ordering, re-evaluate the admin model instead of reusing old assumptions.
156
65
 
157
- **Admin implications:**
158
- - The implementation contract cannot be self-destructed or modified after deployment. Even if it could be, clones would break since they `delegatecall` to the implementation address.
159
- - Each clone has its own storage (including `PROJECT_ID`, ownership, and tier data). The implementation's storage is unused.
160
- - `METADATA_ID_TARGET` is set to the original implementation address, ensuring consistent metadata ID derivation across all clones.
161
- - The `initialize()` function uses an `_initialized` bool flag to prevent re-initialization. The implementation contract's constructor sets `_initialized = true`, blocking direct initialization. Clones start with `_initialized = false` and set it to `true` during `initialize()`.
162
-
163
- ## Ruleset-Level Pauses
164
-
165
- Two behaviors are controlled by the project's current ruleset metadata (packed into the 14-bit `metadata` field of `JBRulesetMetadata`), parsed by `JB721TiersRulesetMetadataResolver`:
166
-
167
- | Bit | Flag | Effect |
168
- |-----|------|--------|
169
- | 0 | `transfersPaused` | When set, NFT transfers are blocked for tiers that have `flags.transfersPausable` enabled |
170
- | 1 | `mintPendingReservesPaused` | When set, `mintPendingReservesFor()` reverts |
66
+ ## Recovery
171
67
 
172
- These can change each ruleset cycle, giving the project owner temporary control over these behaviors without modifying the hook itself.
68
+ - If the wrong immutable dependencies or ownership model were deployed, use a new hook clone.
69
+ - If a project-specific configuration is bad, prefer migrating future rulesets to a replacement hook over trying to retrofit unsupported behavior.
173
70
 
174
71
  ## Admin Boundaries
175
72
 
176
- What the hook owner **cannot** do:
177
-
178
- - **Cannot steal or redirect existing NFTs.** The ERC-721 transfer logic is standard; the owner has no backdoor to move tokens between arbitrary addresses.
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.
180
- - **Cannot change reserve frequency after creation.** The `reserveFrequency` is immutable per tier.
181
- - **Cannot reduce a tier's initial supply.** Supply can only decrease through minting and burning.
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.
185
- - **Cannot re-initialize a hook.** The `initialize()` function reverts if `_initialized` is already true.
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.
187
- - **Cannot bypass the flag restrictions.** Once `noNewTiersWithReserves`, `noNewTiersWithVotes`, or `noNewTiersWithOwnerMinting` are set, all future tiers added via `adjustTiers()` must comply.
188
- - **Cannot mint more reserves than the formula allows.** Reserve mints are bounded by `ceil(nonReserveMints / reserveFrequency)`.
189
- - **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).
73
+ - The owner cannot change the store's constructor immutables or the clone's one-time initialization.
74
+ - The owner cannot overwrite original tier price semantics used for cash-out weight.
75
+ - The owner cannot bypass store-level flags such as non-removable tiers or discount increase restrictions.
76
+ - There is no global admin who can rewrite every hook instance at once.
77
+
78
+ ## Source Map
79
+
80
+ - `src/JB721TiersHook.sol`
81
+ - `src/JB721TiersHookProjectDeployer.sol`
82
+ - `src/JB721TiersHookDeployer.sol`
83
+ - `src/JB721TiersHookStore.sol`
84
+ - `script/Deploy.s.sol`
85
+ - `script/helpers/Hook721DeploymentLib.sol`
86
+ - `test/E2E/`
87
+ - `test/TestAuditGaps.sol`
package/ARCHITECTURE.md CHANGED
@@ -2,75 +2,90 @@
2
2
 
3
3
  ## Purpose
4
4
 
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.
5
+ `nana-721-hook-v6` is the canonical tiered NFT issuance layer for Juicebox V6. It lets a project mint NFTs on payment, manage tier pricing and supply, accumulate NFT credits, lazily mint reserves, and optionally use NFT-aware cash-out behavior.
6
6
 
7
- ## Boundaries
7
+ ## System Overview
8
8
 
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.
9
+ `JB721TiersHook` is the project-facing hook surface. Through the shared `JB721Hook` base, it installs itself as both the ruleset data hook and the post-settlement pay and cash-out hook for the project. `JB721TiersHookStore` is the compact storage and validation backend that defines most tier semantics. The deployers package that behavior for existing projects or one-shot launches. The repo composes `nana-core-v6` rather than replacing terminal, controller, or surplus accounting.
13
10
 
14
- ## Main Components
11
+ ## Core Invariants
15
12
 
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 |
13
+ - Tier ordering, category ordering, and tier IDs are part of storage semantics.
14
+ - Original configured tier price drives cash-out weight; discounts affect mint price, not reclaim weight.
15
+ - Reserve frequency and owner-mint settings must not combine into duplicate mint authority.
16
+ - Pending reserves count in supply-sensitive logic before reserve tokens are lazily minted.
17
+ - Preview behavior must stay aligned with live mint and cash-out behavior.
18
+ - Tier splits can reduce the fungible-token mint weight before terminal settlement unless `issueTokensForSplits` is enabled.
19
+ - If `useDataHookForCashOut` is enabled, NFT cash-out semantics intentionally displace fungible-token cash-out behavior.
23
20
 
24
- ## Runtime Model
21
+ ## Modules
25
22
 
26
- ### Payment Path
23
+ | Module | Responsibility | Notes |
24
+ | --- | --- | --- |
25
+ | `JB721TiersHook` | Data-hook, pay-hook, and optional cash-out-hook behavior | Project-facing entrypoint |
26
+ | `JB721TiersHookStore` | Tier storage, mint accounting, reserves, validation | Storage-critical |
27
+ | `JB721Hook`, `ERC721` | Shared NFT machinery and metadata plumbing | Base abstractions |
28
+ | `JB721TiersHookDeployer`, `JB721TiersHookProjectDeployer` | Clone and launch helpers | Deployment surface |
29
+
30
+ ## Trust Boundaries
31
+
32
+ - Treasury accounting, controller semantics, and permissions live in `nana-core-v6`.
33
+ - Resolver contracts such as Banny are outside this repo and are part of the trusted metadata surface when configured.
34
+ - Hook composition order matters when this hook is wrapped by deployers such as `nana-omnichain-deployers-v6`.
35
+
36
+ ## Critical Flows
37
+
38
+ ### Payment
27
39
 
28
40
  ```text
29
41
  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
42
+ -> data hook decodes metadata and requested tiers
43
+ -> hook computes mintable tiers, split forwarding, beneficiary resolution, and leftover value
32
44
  -> terminal settles the payment into the project
33
- -> pay hook mints NFTs and stores any remaining value as credits when allowed
45
+ -> post-settlement pay hook mints NFTs and may store remaining value as credits
34
46
  ```
35
47
 
36
- ### Reserve Path
48
+ ### Reserve Minting
37
49
 
38
50
  ```text
39
- tier purchases accumulate reserve entitlement
40
- -> reserves are minted lazily via explicit reserve-mint calls
51
+ tier purchases
52
+ -> accumulate reserve entitlement
53
+ -> reserve-mint call later realizes those pending reserve tokens
41
54
  ```
42
55
 
43
- ### Cash-Out Path
56
+ ### Cash Out
44
57
 
45
58
  ```text
46
59
  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
60
+ -> data hook overrides fungible-token cash-out inputs when enabled
61
+ -> terminal settles the cash out using NFT-derived weight
62
+ -> post-settlement cash-out hook burns the specified NFTs
63
+ -> reclaim value is derived from the tier's original configured price
49
64
  ```
50
65
 
51
- ## Critical Invariants
66
+ ## Accounting Model
52
67
 
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.
68
+ The repo owns tier accounting, reserve accounting, credit accounting, and NFT-specific cash-out inputs. It also owns the mapping from hook metadata to NFT mint and burn side effects after terminal settlement. It does not own the canonical treasury ledger, which remains in `nana-core-v6`.
58
69
 
59
- ## Where Complexity Lives
70
+ ## Security Model
60
71
 
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.
72
+ - Store layout changes have repo-wide blast radius because many downstream packages assume stable tier semantics.
73
+ - Metadata decoding is part of economic correctness because it chooses tiers, credits, and cash-out behavior.
74
+ - Terminal authorization in the base hook is part of the trust model; arbitrary callers must not be able to trigger pay or cash-out hooks.
75
+ - Initialization is one-time and ends by transferring ownership to the initializer. Clone deployers and launch flows depend on that handoff being preserved.
76
+ - Reserve math and supply math must be reviewed together.
64
77
 
65
- ## Dependencies
78
+ ## Safe Change Guide
66
79
 
67
- - `nana-core-v6` hooks, permissions, controller, and terminal surfaces
68
- - Optional token URI resolvers such as `banny-retail-v6` and `defifa`
80
+ - Treat store changes as ecosystem-wide changes.
81
+ - Keep previews aligned with state-changing behavior.
82
+ - If you change split behavior, re-check both NFT mint side effects and the fungible-token weight returned to the terminal.
83
+ - When adding metadata fields or flags, update wrapper deployers and downstream integrations in the same change set.
84
+ - If reserve logic changes, re-check supply math, reserve minting, and cash-out denominators together.
69
85
 
70
- ## Safe Change Guide
86
+ ## Source Map
71
87
 
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.
88
+ - `src/JB721TiersHook.sol`
89
+ - `src/JB721TiersHookStore.sol`
90
+ - `src/JB721TiersHookDeployer.sol`
91
+ - `src/JB721TiersHookProjectDeployer.sol`
@@ -2,7 +2,7 @@
2
2
 
3
3
  This repo is the tiered ERC-721 hook system for Juicebox payments and NFT cash-outs. Audit it as a shared primitive used by many other repos.
4
4
 
5
- ## Objective
5
+ ## Audit Objective
6
6
 
7
7
  Find issues that:
8
8
  - let users mint tiers more cheaply than intended
@@ -26,7 +26,13 @@ In scope:
26
26
 
27
27
  This repo is depended on by Defifa, Croptop, Banny, Revnets, and omnichain deployers. Bugs here often have ecosystem-wide blast radius.
28
28
 
29
- ## System Model
29
+ ## Start Here
30
+
31
+ 1. `src/JB721TiersHookStore.sol`
32
+ 2. `src/JB721TiersHook.sol`
33
+ 3. `src/JB721TiersHookDeployer.sol` and `src/JB721TiersHookProjectDeployer.sol`
34
+
35
+ ## Security Model
30
36
 
31
37
  The hook can act as:
32
38
  - a data hook for payment and cash-out accounting inputs
@@ -45,6 +51,22 @@ The most important design subtlety is that this repo affects both:
45
51
 
46
52
  That combination is why small-looking mistakes here often become ecosystem-wide economic bugs.
47
53
 
54
+ ## Roles And Privileges
55
+
56
+ | Role | Powers | How constrained |
57
+ |------|--------|-----------------|
58
+ | Project authority | Adjust tiers, discounts, and resolver setup | Must not break supply, ordering, or accounting assumptions |
59
+ | Hook instance | Mint, burn, and compute accounting inputs | Must stay isolated from other hook instances |
60
+ | Store contract | Hold shared tier state | Must not leak or corrupt cross-project data |
61
+ | Token URI resolver | Supply metadata only | Must not become a hidden control surface |
62
+
63
+ ## Integration Assumptions
64
+
65
+ | Dependency | Assumption | What breaks if wrong |
66
+ |------------|------------|----------------------|
67
+ | `nana-core-v6` | Payment and cash-out semantics remain coherent | Downstream economic routing becomes unsafe |
68
+ | Split recipients and hooks | Failures are handled in bounded ways | Mint accounting and treasury routing desync |
69
+
48
70
  ## Critical Invariants
49
71
 
50
72
  1. Supply caps hold
@@ -68,21 +90,7 @@ Unused payment value that becomes credits must not let a user later mint tiers,
68
90
  7. Resolver trust stays read-only unless explicitly intended
69
91
  Token URI resolvers must not become an implicit control plane for mint, burn, or accounting behavior.
70
92
 
71
- ## Threat Model
72
-
73
- Prioritize:
74
- - overspending and leftover-credit edge cases
75
- - cross-currency pricing with missing or stale feeds
76
- - tier additions or adjustments with invalid sort order or percent bounds
77
- - split hooks or terminal recipients that revert or partially fail
78
- - data-hook and pay-hook interactions inside the same payment
79
-
80
- Especially high-value attacker profiles:
81
- - a payer crafting metadata and tier selections to desync credits, split routing, and token issuance
82
- - a project owner adjusting tiers between preview and execution windows
83
- - a downstream app assuming tier cash-out weight tracks discounted price when the primitive uses different economics
84
-
85
- ## Hotspots
93
+ ## Attack Surfaces
86
94
 
87
95
  - `beforePayRecordedWith`, `afterPayRecordedWith`, and cash-out hooks
88
96
  - credit handling when `payer != beneficiary`
@@ -91,24 +99,19 @@ Especially high-value attacker profiles:
91
99
  - `splitPercent` handling and hook distribution fallback behavior
92
100
  - deployers that transfer ownership or queue rulesets around the hook
93
101
 
94
- ## Sequences Worth Replaying
102
+ Replay these sequences:
103
+ 1. cross-currency payment with missing, stale, or asymmetric prices
104
+ 2. leftover credits across different payer and beneficiary arrangements
105
+ 3. split-routed tier purchases when downstream hooks or terminals fail
106
+ 4. reserve-heavy tiers followed by NFT cash-out before reserves are minted
107
+ 5. tier adjustment or discount changes around active minting and cash-out windows
95
108
 
96
- 1. Cross-currency payment where prices are missing, stale, or intentionally asymmetric.
97
- 2. Payment with leftover value that becomes credits, then a second payment from a different payer/beneficiary arrangement.
98
- 3. Tier purchases with split routing enabled, especially when split hooks or downstream terminals fail.
99
- 4. Reserve-heavy tiers followed by NFT cash-out before pending reserves are minted.
100
- 5. Tier adjustment or discount updates around active minting and cash-out windows.
109
+ ## Accepted Risks Or Behaviors
101
110
 
102
- ## Build And Verification
111
+ - Conservative behavior is preferable to optimistic behavior because downstream repos often treat these surfaces as economic truth.
112
+
113
+ ## Verification
103
114
 
104
- Standard workflow:
105
115
  - `npm install`
106
116
  - `forge build`
107
117
  - `forge test`
108
-
109
- Current tests emphasize:
110
- - audit and regression fixes around split accounting and cross-currency behavior
111
- - invariants on tier lifecycle and store state
112
- - fork coverage for ERC-20 cash-out and tier split routes
113
-
114
- High-value findings in this repo tend to become repeatable vulnerabilities in downstream repos, so favor proofs that show the primitive itself returning or recording the wrong value.
package/README.md CHANGED
@@ -3,18 +3,23 @@
3
3
  `@bananapus/721-hook-v6` is the tiered NFT issuance layer for Juicebox V6. It lets a project mint ERC-721s on payment, attach tier-specific pricing and supply rules, mint reserves, and integrate custom token URI resolvers.
4
4
 
5
5
  Docs: <https://docs.juicebox.money>
6
- Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
7
+ User journeys: [USER_JOURNEYS.md](./USER_JOURNEYS.md)
8
+ Skills: [SKILLS.md](./SKILLS.md)
9
+ Risks: [RISKS.md](./RISKS.md)
10
+ Administration: [ADMINISTRATION.md](./ADMINISTRATION.md)
11
+ Audit instructions: [AUDIT_INSTRUCTIONS.md](./AUDIT_INSTRUCTIONS.md)
7
12
 
8
13
  ## Overview
9
14
 
10
- This package is the standard NFT hook for the V6 ecosystem. Projects use it to:
15
+ This package is the main shared tiered NFT hook used across the V6 ecosystem. Projects use it to:
11
16
 
12
17
  - sell fixed-price NFT tiers through Juicebox payments
13
18
  - apply tier supply, reserve frequency, voting unit, and discount rules
14
19
  - cash out tiers through the Juicebox terminal surface
15
20
  - compose custom metadata resolvers such as Banny or Defifa
16
21
 
17
- The deployer and project-deployer helpers make it practical to clone hooks for existing projects or launch a new project with a 721 hook already configured.
22
+ The deployer helps clone hooks for existing projects, and the project-deployer helps launch new projects with a hook already attached.
18
23
 
19
24
  Use this repo when a project's NFT logic should be part of its payment and cash-out flow. Do not use it for collection-specific rendering or game logic; those belong in higher-level packages like Banny or Defifa.
20
25
 
@@ -70,6 +75,14 @@ The shortest useful reading order is:
70
75
 
71
76
  That split is why UI bugs, economic bugs, and deployment bugs often land in different repos even though users describe them all as "721 hook issues."
72
77
 
78
+ ## High-Signal Tests
79
+
80
+ 1. `test/E2E/Pay_Mint_Redeem_E2E.t.sol`
81
+ 2. `test/invariants/TierLifecycleInvariant.t.sol`
82
+ 3. `test/invariants/TieredHookStoreInvariant.t.sol`
83
+ 4. `test/audit/CodexSplitCreditsMismatch.t.sol`
84
+ 5. `test/regression/ProjectDeployerRulesets.t.sol`
85
+
73
86
  ## Install
74
87
 
75
88
  ```bash
@@ -121,3 +134,9 @@ script/
121
134
  - tier mutations after launch are powerful and should be permissioned carefully
122
135
 
123
136
  When people say "the 721 hook," they often mean three different things: the hook contract, the store, and the metadata resolver plugged into it. Audits and integrations should separate those concerns.
137
+
138
+ ## For AI Agents
139
+
140
+ - Separate hook behavior, store behavior, and resolver behavior in your explanation.
141
+ - Read the store invariants and end-to-end pay/mint/redeem tests before summarizing lifecycle guarantees.
142
+ - If metadata or rendering behavior is project-specific, move to the downstream resolver repo.
package/RISKS.md CHANGED
@@ -44,8 +44,9 @@ This file focuses on the tiered-NFT accounting, reserve-mint, and cash-out risks
44
44
  ## 3. Reentrancy Surface
45
45
 
46
46
  - **Split hook callbacks (`processSplitWith`).** During `afterPayRecordedWith` -> `_processPayment` -> `distributeAll`, the library calls `split.hook.processSplitWith{value}()` for each split with a hook. This executes arbitrary code. At callback time: NFTs already minted, `payCreditsOf` updated, `remainingSupply` decremented in the store. Reentering `afterPayRecordedWith` requires terminal authentication and processes as an independent payment. All split hook and terminal calls are wrapped in try-catch to prevent a single reverting recipient from bricking all payments to the project. For native token hooks, a revert returns false (ETH stays in the contract and routes to project balance). For ERC20 hooks, tokens are transferred before the callback; a revert still returns true because the tokens have already left the contract. Tested: `TestAuditGaps_Reentrancy` confirms reentrancy is blocked by terminal check.
47
- - **Split beneficiary ETH sends.** `_sendPayoutToSplit` uses `beneficiary.call{value: amount}("")`. If the beneficiary reverts, the function returns `false` and the failed amount is accumulated separately, then routed to the project's balance after the distribution loop via `addToBalanceOf`. Later split recipients receive only their proportional share, not the failed recipient's share. Does not revert the entire payment.
47
+ - **Split beneficiary ETH sends.** `_sendPayoutToSplit` uses `beneficiary.call{value: amount}("")`. If the beneficiary reverts, the function returns `false` and the failed amount is accumulated separately, then routed toward the project's balance after the distribution loop via `addToBalanceOf`. Later split recipients receive only their proportional share, not the failed recipient's share. This does not revert the entire payment unless the fallback `addToBalanceOf` path also reverts.
48
48
  - **Terminal `.pay()` / `.addToBalanceOf()` during split distribution.** For project-targeted splits, the library calls the target project's primary terminal via try-catch. A reverting terminal returns false, routing the funds to the project's balance instead. For ERC20 terminal calls, approval is reset to zero on failure to prevent dangling approvals. The target terminal could call back into the hook, but the hook's state is fully settled (supply, credits, mint state). Reentrancy through this path cannot double-mint or corrupt state.
49
+ - **Split fallback can still strand value if the project terminal rejects leftovers.** `_distributeSingleSplit` tries to route failed split payouts into the source project's primary terminal with `addToBalanceOf`. If that fallback also reverts, the whole hook call reverts with `JB721TiersHookLib_SplitFallbackFailed` after the hook has already received the forwarded funds. For native ETH, that leaves the ETH stranded in the hook. For ERC-20s, approval is reset but the tokens remain in the hook. There is no built-in recovery path.
49
50
  - **`afterCashOutRecordedWith` execution order.** Burns tokens via `_burn()` -> `_update()` -> `STORE.tierTransferInfoOfTokenId()` in a loop, then calls `STORE.recordBurn()`. ERC721 `_update` triggers the store's tier balance decrement. Burns go to `address(0)`, so no `onERC721Received` callback.
50
51
  - **No `ReentrancyGuard`.** Protection relies on state ordering (all `STORE.record*` calls before external calls), terminal authentication checks, and try-catch wrapping of all external calls in `_sendPayoutToSplit`. `_mint()` uses the non-safe variant, avoiding `onERC721Received` callbacks during minting.
51
52
 
@@ -83,6 +84,7 @@ This file focuses on the tiered-NFT accounting, reserve-mint, and cash-out risks
83
84
  - **Split group ID encoding.** Composite: `uint256(uint160(hookAddress)) | (tierId << 160)`. Tier IDs are capped at uint16, so no overflow. Splits are permanently coupled to a specific hook address -- migrating to a new hook requires re-creating all split groups.
84
85
  - **ERC-20 split distribution pulls from terminal.** `distributeAll` calls `SafeERC20.safeTransferFrom(token, msg.sender, address(this), amount)` to pull ERC-20s from the terminal. Requires the terminal to have granted allowance via its `_beforeTransferTo` pattern. If the terminal's allowance mechanism changes, distribution fails.
85
86
  - **Forwarded funds depend on non-empty split metadata.** `_processPayment` only calls `distributeAll` when both `context.forwardedAmount.value != 0` and `context.hookMetadata.length != 0`. If an integration ever forwards funds with empty hook metadata, distribution is skipped and the funds remain in the hook contract with no dedicated rescue path.
87
+ - **Split-fallback success depends on the source project's active terminal.** Failed split payouts are not simply burned or refunded. They are re-routed into the source project's current primary terminal for the forwarded token. If that terminal is unset or rejects `addToBalanceOf`, the call reverts and the hook can retain the funds.
86
88
  - **Token URI resolver external calls.** `tokenURI()` and `tiersOf(..., includeResolvedUri=true)` call the resolver if set. A reverting resolver blocks all metadata reads (marketplace/frontend impact, no fund risk).
87
89
 
88
90
  ## 7. Invariants to Verify
@@ -117,3 +119,7 @@ If the payment currency differs from the tier pricing currency and `PRICES == ad
117
119
  ### 8.4 Tiny split allocations can round down to zero recipient amounts
118
120
 
119
121
  Split metadata is expressed in whole token units after conversion and capping. For very small allocations, each rounded per-tier split amount can become zero even though the overall forwarded amount is still reduced by the capped split total. This is an accepted precision tradeoff for dust-sized payments: integrations should not rely on sub-precision split routing and should expect tiny split allocations to be economically lossy.
122
+
123
+ ### 8.5 Failed split payouts only degrade cleanly if the fallback terminal path works
124
+
125
+ The hook treats a reverting split hook, beneficiary, or target terminal as a soft failure and attempts to re-route that amount into the source project's balance. That graceful degradation depends on the source project's current primary terminal accepting `addToBalanceOf` for the forwarded token. If that terminal is missing or rejects the call, the transaction reverts after funds have already reached the hook, and the hook can retain those assets without a built-in rescue path.