@bananapus/721-hook-v6 0.0.35 → 0.0.37

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 (38) hide show
  1. package/ADMINISTRATION.md +49 -163
  2. package/ARCHITECTURE.md +71 -49
  3. package/AUDIT_INSTRUCTIONS.md +47 -84
  4. package/README.md +40 -28
  5. package/RISKS.md +85 -86
  6. package/SKILLS.md +17 -16
  7. package/USER_JOURNEYS.md +85 -62
  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/JB721CheckpointsDeployer.sol +2 -0
  13. package/src/JB721TiersHook.sol +0 -2
  14. package/src/JB721TiersHookProjectDeployer.sol +0 -1
  15. package/src/JB721TiersHookStore.sol +1 -2
  16. package/src/abstract/JB721Hook.sol +0 -1
  17. package/src/interfaces/IJB721CheckpointsDeployer.sol +3 -0
  18. package/src/interfaces/IJB721TiersHook.sol +0 -2
  19. package/src/interfaces/IJB721TiersHookStore.sol +1 -1
  20. package/src/libraries/JB721Constants.sol +0 -1
  21. package/src/structs/JB721InitTiersConfig.sol +0 -1
  22. package/src/structs/JB721Tier.sol +0 -2
  23. package/src/structs/JB721TierConfig.sol +0 -2
  24. package/src/structs/JB721TierConfigFlags.sol +0 -1
  25. package/src/structs/JB721TierFlags.sol +0 -1
  26. package/src/structs/JB721TiersHookFlags.sol +0 -1
  27. package/src/structs/JB721TiersMintReservesConfig.sol +0 -1
  28. package/src/structs/JB721TiersRulesetMetadata.sol +0 -1
  29. package/src/structs/JB721TiersSetDiscountPercentConfig.sol +0 -1
  30. package/src/structs/JBBitmapWord.sol +0 -1
  31. package/src/structs/JBDeploy721TiersHookConfig.sol +0 -1
  32. package/src/structs/JBLaunchProjectConfig.sol +0 -1
  33. package/src/structs/JBLaunchRulesetsConfig.sol +0 -1
  34. package/src/structs/JBPayDataHookRulesetConfig.sol +0 -1
  35. package/src/structs/JBPayDataHookRulesetMetadata.sol +0 -1
  36. package/src/structs/JBQueueRulesetsConfig.sol +0 -1
  37. package/src/structs/JBStored721Tier.sol +0 -1
  38. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +116 -0
package/ADMINISTRATION.md CHANGED
@@ -1,189 +1,75 @@
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 | Tiered NFT hook configuration, tier adjustment, and deployer wiring |
8
+ | Control posture | Mixed project-owner and delegated control |
9
+ | Highest-risk actions | Adjusting tiers, setting discount or metadata behavior, and wiring the wrong hook or resolver |
10
+ | Recovery posture | Some configuration is mutable, but many tier properties are intentionally one-way |
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` splits control between project-level hook ownership and the tier rules enforced by the store. Many important settings can be changed only in limited ways after launch.
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
+ - project owners or delegates control hook-level configuration
19
+ - tier creation and mutation are permissioned and partially one-way
20
+ - the store trusts the calling hook for its own state namespace
21
+ - deployers package setup, but do not remove the need for runtime review
31
22
 
32
23
  ## Roles
33
24
 
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
25
+ | Role | How Assigned | Scope | Notes |
26
+ | --- | --- | --- | --- |
27
+ | Project owner | `owner()` or project control surface | Per hook | Main authority |
28
+ | Tier delegate | `JBPermissions` grant | Per project | Usually tier, mint, discount, or metadata permissions |
29
+ | Reserve beneficiary | Tier config | Per tier | Receives reserve NFTs |
61
30
 
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. |
31
+ ## Privileged Surfaces
70
32
 
71
- ### JB721TiersHookProjectDeployer
33
+ - `adjustTiers(...)`
34
+ - `mintFor(...)`
35
+ - `setDiscountPercentOf(...)`
36
+ - `setMetadata(...)`
37
+ - deployer setup and hook ownership transfer paths
72
38
 
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. |
39
+ ## Immutable And One-Way
78
40
 
79
- ### JB721TiersHookDeployer
41
+ - many tier properties are immutable once created
42
+ - removed tiers do not reduce `maxTierIdOf`
43
+ - pool-like mutable rescue does not exist here; bad tier design is often expensive to unwind
80
44
 
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`. |
45
+ ## Operational Notes
84
46
 
85
- ### JB721Hook (Abstract Base)
47
+ - review tier parameters before launch as if they were economic policy
48
+ - treat discount changes and metadata changes as meaningful authority, not cosmetic controls
49
+ - be explicit about whether the hook participates in pay, cash out, or both
50
+ - separate resolver trust from hook and store trust
86
51
 
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`. |
52
+ ## Machine Notes
91
53
 
92
- ### JB721TiersHookStore (No Access Control -- msg.sender Keyed)
54
+ - do not reason from the hook alone when the bug may live in the store
55
+ - if a resolver is involved, inspect it separately
56
+ - if tier counts are large, re-check gas-sensitive reads and writes before operational changes
93
57
 
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. |
58
+ ## Recovery
107
59
 
108
- ## Permission System
60
+ - some mistakes can be corrected through allowed metadata or discount changes
61
+ - many tier-design mistakes are effectively permanent once live
62
+ - deployer mistakes may require a new hook path rather than in-place repair
109
63
 
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.
113
-
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.
115
-
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.
119
-
120
- ### Permission IDs Used
121
-
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()` |
130
-
131
- ## Immutable Configuration
132
-
133
- The following are set at deploy/initialization time and **cannot be changed afterward**:
134
-
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 |
152
-
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.
156
-
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 |
64
+ ## Admin Boundaries
171
65
 
172
- These can change each ruleset cycle, giving the project owner temporary control over these behaviors without modifying the hook itself.
66
+ - no one can rewrite immutable tier properties after creation
67
+ - deployers do not bypass runtime permissions after setup
68
+ - resolver behavior cannot fix broken hook accounting
173
69
 
174
- ## Admin Boundaries
70
+ ## Source Map
175
71
 
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).
72
+ - `src/JB721TiersHook.sol`
73
+ - `src/JB721TiersHookStore.sol`
74
+ - `src/JB721TiersHookDeployer.sol`
75
+ - `src/JB721TiersHookProjectDeployer.sol`
package/ARCHITECTURE.md CHANGED
@@ -2,75 +2,97 @@
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 shared tiered NFT layer for Juicebox V6. It lets projects sell NFT tiers, track reserves, route split payouts, and cash out NFTs without replacing core treasury accounting.
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 runtime hook. `JB721TiersHookStore` is the accounting backend for tiers, supply, reserves, and lookup. The deployers package that hook into reusable flows for existing projects and new project launches.
13
10
 
14
- ## Main Components
11
+ Custom token URI resolvers usually live outside this repo, but they still affect the trusted surface seen by users.
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
+ ## Core Invariants
23
14
 
24
- ## Runtime Model
15
+ - the hook must not create alternate treasury accounting
16
+ - tier supply, burned counts, and reserves must stay coherent
17
+ - cash-out weight must reflect the intended tier economics
18
+ - reserve minting and split routing must not drift from stored tier state
19
+ - store-linked list and bitmap assumptions must stay valid under tier add, remove, and clean operations
20
+ - deployer wiring must preserve the expected ruleset and hook shape
25
21
 
26
- ### Payment Path
22
+ ## Modules
27
23
 
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
34
- ```
24
+ | Module | Responsibility | Notes |
25
+ | --- | --- | --- |
26
+ | `JB721TiersHook` | Pay hook, cash-out hook, permissions, and project-facing execution | Runtime core |
27
+ | `JB721TiersHookStore` | Tier definitions, balances, reserve tracking, and accounting | Shared state |
28
+ | `JB721TiersHookDeployer` | Clone deployer for existing projects | Wiring helper |
29
+ | `JB721TiersHookProjectDeployer` | Project-launch deployer with hook setup | Launch helper |
30
+ | `JB721Hook` | Abstract 721 hook base | Shared behavior |
31
+
32
+ ## Trust Boundaries
35
33
 
36
- ### Reserve Path
34
+ - core accounting, pricing, and terminal authentication remain in `nana-core-v6`
35
+ - metadata resolvers can be project-specific and should be treated as trusted external surfaces
36
+ - the store is trusted by every hook that uses it
37
+ - deployers are trusted to wire the hook into the intended project and ruleset shape
38
+
39
+ ## Critical Flows
40
+
41
+ ### Pay And Mint
37
42
 
38
43
  ```text
39
- tier purchases accumulate reserve entitlement
40
- -> reserves are minted lazily via explicit reserve-mint calls
44
+ payment arrives
45
+ -> hook decodes metadata and tier choices
46
+ -> store records mints, credits, supply changes, and reserve effects
47
+ -> hook may route split payouts from forwarded funds
48
+ -> collection state and balances update for the beneficiary
41
49
  ```
42
50
 
43
- ### Cash-Out Path
51
+ ### Cash Out
44
52
 
45
53
  ```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
54
+ cash out requested
55
+ -> hook checks NFT-specific metadata and selected token IDs
56
+ -> hook burns NFTs
57
+ -> store records burn and supply effects
58
+ -> terminal reclaims value using hook-aware cash-out math
49
59
  ```
50
60
 
51
- ## Critical Invariants
52
-
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.
61
+ ## Accounting Model
58
62
 
59
- ## Where Complexity Lives
63
+ This repo owns tier accounting and NFT lifecycle logic. It does not own the canonical project ledger for balances, fees, or surplus.
60
64
 
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.
65
+ The most important state lives in the store: remaining supply, burned counts, reserve tracking, and per-tier configuration.
64
66
 
65
- ## Dependencies
67
+ ## Security Model
66
68
 
67
- - `nana-core-v6` hooks, permissions, controller, and terminal surfaces
68
- - Optional token URI resolvers such as `banny-retail-v6` and `defifa`
69
+ - store corruption has ecosystem-wide blast radius because many products reuse it
70
+ - reserve logic, discounts, and cash-out weight are the main economic risk surfaces
71
+ - split distribution and fallback behavior are part of correctness, not a secondary concern
72
+ - gas costs matter because some reads and writes scale with tier count
69
73
 
70
74
  ## Safe Change Guide
71
75
 
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.
76
+ - review hook and store behavior together when changing tier lifecycle logic
77
+ - if reserve logic changes, re-check cash-out weight and pending reserve effects together
78
+ - if deployer behavior changes, re-check ruleset wiring and ownership transfer paths
79
+ - do not treat resolver behavior as proof that hook accounting is correct
80
+
81
+ ## Canonical Checks
82
+
83
+ - pay, mint, and redeem end-to-end behavior:
84
+ `test/E2E/Pay_Mint_Redeem_E2E.t.sol`
85
+ - store and lifecycle invariants:
86
+ `test/invariants/TierLifecycleInvariant.t.sol`
87
+ `test/invariants/TieredHookStoreInvariant.t.sol`
88
+ - split-credit and deployer regressions:
89
+ `test/audit/CodexSplitCreditsMismatch.t.sol`
90
+ `test/regression/ProjectDeployerRulesets.t.sol`
91
+
92
+ ## Source Map
93
+
94
+ - `src/JB721TiersHook.sol`
95
+ - `src/JB721TiersHookStore.sol`
96
+ - `src/JB721TiersHookDeployer.sol`
97
+ - `src/JB721TiersHookProjectDeployer.sol`
98
+ - `src/libraries/JB721TiersHookLib.sol`
@@ -1,114 +1,77 @@
1
1
  # Audit Instructions
2
2
 
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.
3
+ This repo adds tiered NFT issuance and cash-out behavior to Juicebox projects. Audit it as a shared accounting layer whose mistakes can affect many downstream products.
4
4
 
5
- ## Objective
5
+ ## Audit Objective
6
6
 
7
7
  Find issues that:
8
- - let users mint tiers more cheaply than intended
9
- - over-mint, under-burn, or miscount reserves, credits, or supply
10
- - route split funds incorrectly or let split paths distort token issuance
11
- - let NFT cash-outs reclaim more value than intended
12
- - corrupt shared store state across different hook instances
8
+
9
+ - corrupt tier supply, reserve state, or burn accounting
10
+ - misprice cash outs or split routing
11
+ - let permissions or deployer wiring create unsafe lifecycle changes
12
+ - create gas or liveness failures in tier-heavy deployments
13
+ - break trust boundaries between hook, store, and resolver behavior
13
14
 
14
15
  ## Scope
15
16
 
16
17
  In scope:
18
+
17
19
  - `src/JB721TiersHook.sol`
18
20
  - `src/JB721TiersHookStore.sol`
19
- - `src/JB721TiersHookDeployer.sol`
20
- - `src/JB721TiersHookProjectDeployer.sol`
21
- - `src/abstract/`
22
- - `src/interfaces/`
23
- - `src/libraries/`
24
- - `src/structs/`
21
+ - deployers, libraries, interfaces, and structs under `src/`
25
22
  - deployment scripts in `script/`
26
23
 
27
- This repo is depended on by Defifa, Croptop, Banny, Revnets, and omnichain deployers. Bugs here often have ecosystem-wide blast radius.
28
-
29
- ## System Model
30
-
31
- The hook can act as:
32
- - a data hook for payment and cash-out accounting inputs
33
- - a pay hook that mints NFTs
34
- - a cash-out hook that burns NFTs and computes reclaim weight
35
-
36
- Key moving parts:
37
- - `JB721TiersHookStore` holds compact tier state
38
- - hook instances read and mutate tier data through the store
39
- - tier prices, discounts, reserves, credits, split percentages, and category order shape mint behavior
40
- - optional token URI resolvers can override metadata generation
41
-
42
- The most important design subtlety is that this repo affects both:
43
- - NFT state
44
- - core Juicebox accounting inputs and fulfillment order
45
-
46
- That combination is why small-looking mistakes here often become ecosystem-wide economic bugs.
24
+ ## Start Here
47
25
 
48
- ## Critical Invariants
49
-
50
- 1. Supply caps hold
51
- No tier may mint beyond its configured total supply once purchases, owner mints, and pending reserves are all considered.
52
-
53
- 2. Reserve accounting is exact
54
- Pending reserves must neither disappear nor inflate reclaim denominators beyond what the design intends.
55
-
56
- 3. Split routing matches accounting
57
- If part of a mint price is routed to splits, token issuance and treasury accounting must reflect only the intended project portion.
26
+ 1. `src/JB721TiersHook.sol`
27
+ 2. `src/JB721TiersHookStore.sol`
28
+ 3. `src/libraries/JB721TiersHookLib.sol`
58
29
 
59
- 4. Cash-out weight is consistent
60
- The reclaim value for NFTs must match documented tier economics and must not be manipulable through discounts, credits, cross-currency inputs, or reserve timing.
30
+ ## Security Model
61
31
 
62
- 5. Shared store isolation
63
- One hook instance must not corrupt or observe mutable state belonging to another project unexpectedly.
32
+ The hook:
64
33
 
65
- 6. Credit semantics remain bounded
66
- Unused payment value that becomes credits must not let a user later mint tiers, trigger splits, or receive project-token issuance on terms they did not actually fund.
34
+ - mints and burns tiered NFTs through Juicebox flows
35
+ - records tier lifecycle state in a shared store
36
+ - can route split payouts from forwarded value
37
+ - composes with project-specific metadata resolvers
67
38
 
68
- 7. Resolver trust stays read-only unless explicitly intended
69
- Token URI resolvers must not become an implicit control plane for mint, burn, or accounting behavior.
39
+ ## Roles And Privileges
70
40
 
71
- ## Threat Model
41
+ | Role | Powers | How constrained |
42
+ |------|--------|-----------------|
43
+ | Project authority | Configure tiers, metadata, and minting policy | Must stay inside explicit permission checks |
44
+ | Store caller | Mutate store state in its own namespace | Must not corrupt tier accounting |
45
+ | Resolver | Serve metadata and URI behavior | Must not be confused with accounting truth |
72
46
 
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
47
+ ## Integration Assumptions
79
48
 
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
49
+ | Dependency | Assumption | What breaks if wrong |
50
+ |------------|------------|----------------------|
51
+ | `nana-core-v6` | Terminal auth and pricing behavior are accurate | Pay and cash-out behavior drift |
52
+ | Resolver repo | Metadata reads behave as expected | UI and marketplace behavior break |
84
53
 
85
- ## Hotspots
54
+ ## Critical Invariants
86
55
 
87
- - `beforePayRecordedWith`, `afterPayRecordedWith`, and cash-out hooks
88
- - credit handling when `payer != beneficiary`
89
- - discount logic versus cash-out pricing
90
- - pending reserve minting and denominator logic
91
- - `splitPercent` handling and hook distribution fallback behavior
92
- - deployers that transfer ownership or queue rulesets around the hook
56
+ 1. Tier supply stays coherent.
57
+ Remaining supply, burned counts, and outstanding ownership must reconcile.
58
+ 2. Reserve logic stays bounded.
59
+ Pending reserves and reserve minting must not over-allocate.
60
+ 3. Cash-out weight is consistent.
61
+ NFT reclaim value must match the tier model the hook and store intend.
62
+ 4. Split and fallback behavior is safe.
63
+ Failed split paths must not silently corrupt value or lifecycle state.
93
64
 
94
- ## Sequences Worth Replaying
65
+ ## Attack Surfaces
95
66
 
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.
67
+ - pay and cash-out hook entrypoints
68
+ - tier add, remove, and clean flows
69
+ - reserve minting
70
+ - split distribution and fallback paths
71
+ - resolver integration
101
72
 
102
- ## Build And Verification
73
+ ## Verification
103
74
 
104
- Standard workflow:
105
75
  - `npm install`
106
76
  - `forge build`
107
77
  - `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.