@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/README.md CHANGED
@@ -1,268 +1,122 @@
1
1
  # Juicebox 721 Hook
2
2
 
3
- Juicebox projects accept payments through terminals, but by default those payments only mint fungible project tokens. `nana-721-hook-v6` extends that flow with a tiered NFT system: project owners define tiers -- each with a price, supply cap, artwork, and optional per-tier features like reserve minting, voting power, discount schedules, and split-based fund routing -- and when someone pays the project, the hook mints the corresponding NFTs. Optionally, holders can burn their NFTs to reclaim funds from the project in proportion to each NFT's price relative to all outstanding NFTs.
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
- _If you're having trouble understanding this contract, take a look at the [core protocol contracts](https://github.com/Bananapus/nana-core-v6) and the [documentation](https://docs.juicebox.money/) first. If you have questions, reach out on [Discord](https://discord.com/invite/ErQYmth4dS)._
5
+ Docs: <https://docs.juicebox.money>
6
+ Architecture: [ARCHITECTURE.md](./ARCHITECTURE.md)
6
7
 
7
- ## Conceptual Overview
8
+ ## Overview
8
9
 
9
- ### How it works
10
+ This package is the standard NFT hook for the V6 ecosystem. Projects use it to:
10
11
 
11
- A `JB721TiersHook` is simultaneously a **data hook**, a **pay hook**, and a **cash out hook**. When a project using this hook is paid through a Juicebox terminal, the terminal asks the data hook what should happen. The data hook inspects the payment amount and any tier IDs the payer specified in the payment metadata, then returns instructions for the pay hook to mint the appropriate NFTs.
12
+ - sell fixed-price NFT tiers through Juicebox payments
13
+ - apply tier supply, reserve frequency, voting unit, and discount rules
14
+ - cash out tiers through the Juicebox terminal surface
15
+ - compose custom metadata resolvers such as Banny or Defifa
12
16
 
13
- 1. **Payment arrives** at the terminal. The terminal calls the data hook with details about the payment.
14
- 2. **Tier selection.** The payer can specify which tier IDs to mint in the payment metadata. If no tiers are specified and the hook allows credits, the payment amount is stored as NFT credits for future purchases.
15
- 3. **Price check.** Each requested tier's price (after any discount) is checked against the payment amount. If the tier and payment use different currencies, the hook's immutable `PRICES` contract converts between them. If `PRICES` is the zero address and currencies differ, the payment is silently ignored (no mint, no revert).
16
- 4. **Minting.** The pay hook mints one NFT per requested tier to the payment beneficiary, decrementing each tier's remaining supply.
17
- 5. **Reserve minting.** If a tier has a `reserveFrequency` of N, one extra NFT is minted to the tier's `reserveBeneficiary` for every N NFTs purchased. Reserve NFTs are minted lazily via `mintPendingReservesFor`.
18
- 6. **Credits.** Any leftover payment amount (the portion not spent on tier prices) is stored as NFT credits on the payer's address, redeemable toward future NFT purchases. Credits are only combined with the payment when `payer == beneficiary`. The hook can reject leftover funds entirely via the `preventOverspending` flag.
19
- 7. **Cash outs.** If the project owner enables `useDataHookForCashOut` in the ruleset metadata, NFT holders can burn their NFTs to reclaim funds. The reclaim amount is proportional to the NFT's original tier price (not the discounted price) relative to the total price of all outstanding NFTs (including pending reserves in the denominator). When NFT cash outs are enabled, fungible token cash outs are disabled -- attempting to cash out fungible tokens when the data hook is active will revert.
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.
20
18
 
21
- ### Tier properties
19
+ 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.
22
20
 
23
- Each tier is configured with a `JB721TierConfig` and stored compactly as a `JBStored721Tier`. Key properties:
21
+ The important architectural point is that this repo does not just "mint NFTs on pay." It changes how payment value, tier state, reserves, and cash-out behavior interact.
24
22
 
25
- - **Price** (`uint104`). The cost to mint one NFT from this tier, denominated in the currency specified in the hook's `JB721InitTiersConfig`.
26
- - **Initial supply** (`uint32`). The maximum number of NFTs that can ever be minted from this tier (up to 999,999,999).
27
- - **Category** (`uint24`). Groups tiers for organizational and access purposes. Tiers must be sorted by category in ascending order when added -- the store reverts with `JB721TiersHookStore_InvalidCategorySortOrder` otherwise.
28
- - **Discount percent** (`uint8`). Reduces the effective purchase price. The denominator is 200, so a `discountPercent` of 100 means 50% off and 200 means free. Can be changed later via `setDiscountPercentOf`. Tiers configured with `cannotIncreaseDiscountPercent` only allow discounts to decrease. Cash out weight always uses the original tier price, not the discounted price.
29
- - **Reserve frequency** (`uint16`). With a value of N, one extra NFT is minted to the `reserveBeneficiary` for every N NFTs purchased. Tiers with `allowOwnerMint` enabled cannot have reserves.
30
- - **Voting units** (`uint104`). By default, each NFT's voting power equals its tier price. When `useVotingUnits` is true, a custom `votingUnits` value is used instead. Voting power is computed per-address across all tiers.
31
- - **Split percent** (`uint32`). Routes a percentage of the tier's mint price to configured split recipients each time an NFT from the tier is purchased, out of `JBConstants.SPLITS_TOTAL_PERCENT` (1,000,000,000). The remaining funds stay in the project's balance. Split recipients follow the same priority as `JBMultiTerminal`: `split.hook` (receives funds via `IJBSplitHook.processSplitWith`) > `split.projectId` (routed via the project's primary terminal) > `split.beneficiary` (direct transfer).
32
- - **Weight adjustment.** When splits are active, the hook adjusts the returned weight so the terminal only mints fungible project tokens proportional to the amount that actually enters the project treasury. For example, a 50% `splitPercent` on a 1 ETH payment results in half the normal token issuance. This weight adjustment can be disabled with the `issueTokensForSplits` flag, which gives payers full token credit regardless of where the funds go.
33
- - **Flags.** `allowOwnerMint` (project owner can mint on-demand), `transfersPausable` (transfers can be paused per-ruleset), `cannotBeRemoved` (tier is permanent once added), `cannotIncreaseDiscountPercent` (discount can only decrease).
34
- - **Token URI.** Each tier has an `encodedIPFSUri` for artwork/metadata, which can be overridden by an `IJB721TokenUriResolver`. The resolver can return unique values for each NFT within a tier.
23
+ ## Key Contracts
35
24
 
36
- ### Per-ruleset options
37
-
38
- The hook packs two boolean flags into the ruleset's `metadata` field via `JB721TiersRulesetMetadata`:
39
-
40
- - `pauseTransfers` -- prevents all NFT transfers during this ruleset (only affects tiers with `transfersPausable` set).
41
- - `pauseMintPendingReserves` -- prevents minting pending reserve NFTs during this ruleset.
42
-
43
- ### Operating envelope
25
+ | Contract | Role |
26
+ | --- | --- |
27
+ | `JB721TiersHook` | Main ERC-721 tiers hook that manages minting, cash outs, metadata, and ruleset-aware behavior. |
28
+ | `JB721TiersHookStore` | Shared storage contract for tier data and accounting. |
29
+ | `JB721TiersHookDeployer` | Clone factory for deploying a hook for an existing project. |
30
+ | `JB721TiersHookProjectDeployer` | Convenience deployer for launching a project with a hook already wired in. |
31
+ | `JB721Hook` | Abstract base for 721 pay and cash-out hook behavior. |
44
32
 
45
- The contract supports up to 65,535 tiers (`uint16.max`), but the practical operating envelope is much smaller because several important read paths iterate the tier set:
33
+ ## Mental Model
46
34
 
47
- - **Up to ~100 active tiers**: comfortable for projects that expect frequent `balanceOf` reads, governance queries, or NFT cash outs.
48
- - **100--200 tiers**: an advanced configuration requiring deliberate gas budgeting and frontend/operator awareness.
49
- - **Above 200 tiers**: on-chain reads and cash-out accounting remain functionally correct, but gas costs scale linearly with `maxTierId`.
35
+ Think about the repo in three pieces:
50
36
 
51
- The test suite proves survivability at 100 and 200 tiers, and demonstrates that `balanceOf` and `totalCashOutWeight` become materially more expensive at 100 tiers than at 10. Treat this evidence as a scope boundary, not encouragement to target the theoretical ceiling.
37
+ 1. `JB721TiersHook` defines behavior at the project edge
38
+ 2. `JB721TiersHookStore` is the tier accounting backend
39
+ 3. deployers package the hook into reusable project-launch and clone flows
52
40
 
53
- ## Architecture
41
+ If a bug affects supply, reserve minting, or tier lookup, it usually lives in the hook-store interaction. If it affects project wiring, it usually lives in the deployer path or in how the hook is attached to rulesets.
54
42
 
55
- ```mermaid
56
- graph TD;
57
- A[JB721TiersHookProjectDeployer] -->|launches project + queues rulesets| B[Juicebox Project]
58
- D[JB721TiersHookDeployer] -->|deploys hook for existing project| C[JB721TiersHook]
59
- A -->|deploys via| D
60
- B -->|calls on pay / cash out| C
61
- C -->|reads and writes tier data| E[JB721TiersHookStore]
62
- B -->|uses| F[JBMultiTerminal]
63
- C -->|mints NFTs on payment through| F
64
- C -->|burns NFTs to reclaim funds through| F
65
- ```
43
+ The shortest useful reading order is:
66
44
 
67
- | Contract | Lines | Description |
68
- | --- | --- | --- |
69
- | `JB721TiersHook` | 794 | The core tiered NFT hook. Implements pay hook (mint NFTs), cash out hook (burn NFTs for reclaim), and data hook (compute weight adjustments and split routing). Manages credits, tier adjustments, reserve minting, discount changes, and metadata. Inherits `JB721Hook` and `JBOwnable`. |
70
- | `JB721TiersHookStore` | 1,240 | Shared data store for all `JB721TiersHook` instances. Manages tier storage (packed `JBStored721Tier`), supply tracking, bitmap-based tier removal, token ID encoding/decoding, and enforces tier sort order, supply limits, and flag constraints. |
71
- | `JB721TiersHookProjectDeployer` | 418 | Convenience deployer that creates a new Juicebox project with a 721 tiers hook already configured. Exposes `launchProjectFor` and `queueRulesetsOf` for ongoing ruleset management. |
72
- | `JB721TiersHookDeployer` | 115 | Deploys a `JB721TiersHook` clone for an existing project via `deployHookFor`. Uses Solady `LibClone` for minimal proxy deployment and registers the clone in `IJBAddressRegistry`. |
73
- | `JB721Hook` (abstract) | 268 | Abstract base for 721 hooks. Handles pay/cash out lifecycle dispatch, terminal validation, metadata ID resolution, and ERC-2981/ERC-165 interface declaration. |
74
- | `ERC721` (abstract) | 506 | Clone-compatible ERC-721 implementation with mutable `name` and `symbol` (set during `initialize` rather than constructor, since hooks are deployed as clones). |
75
- | `JB721TiersHookLib` | 634 | Library for tier adjustment logic, split distribution, price normalization across currencies, and token URI resolution. |
76
- | `JB721TiersRulesetMetadataResolver` | 47 | Packs and unpacks `JB721TiersRulesetMetadata` (transfer pause + reserve mint pause) into the 14-bit metadata field of `JBRulesetMetadata`. |
77
- | `JBBitmap` | 58 | Word-based bitmap for tracking removed tiers. Each word covers 256 tier IDs. |
78
- | `JBIpfsDecoder` | 98 | Decodes `bytes32`-encoded IPFS CIDv0 hashes into `Qm...` URI strings. |
79
- | `JB721Constants` | 7 | Defines `DISCOUNT_DENOMINATOR = 200`. |
80
-
81
- ### Supporting Types
82
-
83
- Key structs used to configure and interact with the hook:
84
-
85
- | Struct | Purpose |
86
- | --- | --- |
87
- | `JB721TierConfig` | Full configuration for a single tier: price, supply, voting units, reserve frequency, discount, category, splits, and boolean flags. |
88
- | `JB721Tier` | Read-only view of a tier's current state, including remaining supply and resolved URI. |
89
- | `JBStored721Tier` | Packed on-chain storage layout for a tier (price, supply, category, discount, reserve frequency, split percent, and packed boolean flags). |
90
- | `JB721InitTiersConfig` | Wraps an array of `JB721TierConfig` with the currency and decimal precision for tier prices. |
91
- | `JBDeploy721TiersHookConfig` | Full deployment config: collection name/symbol, base URI, token URI resolver, contract URI, tiers config, default reserve beneficiary, and flags. |
92
- | `JB721TiersHookFlags` | Hook-wide boolean flags: `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending`, `issueTokensForSplits`. |
93
- | `JB721TiersRulesetMetadata` | Per-ruleset options packed into `JBRulesetMetadata.metadata`: `pauseTransfers`, `pauseMintPendingReserves`. |
94
- | `JB721TiersMintReservesConfig` | Specifies which tier to mint reserves for and how many to mint. |
95
- | `JB721TiersSetDiscountPercentConfig` | Specifies a tier ID and the new discount percent to set. |
45
+ 1. `JB721TiersHook`
46
+ 2. `JB721TiersHookStore`
47
+ 3. the relevant deployer
48
+ 4. the resolver plugged into the hook, if the project uses one
96
49
 
97
- ## Install
50
+ ## Read These Files First
98
51
 
99
- For projects using `npm` to manage dependencies (recommended):
52
+ 1. `src/JB721TiersHook.sol`
53
+ 2. `src/JB721TiersHookStore.sol`
54
+ 3. `src/libraries/JB721TiersHookLib.sol`
55
+ 4. `src/JB721TiersHookDeployer.sol` or `src/JB721TiersHookProjectDeployer.sol`
56
+ 5. the resolver contract in the downstream repo, if present
100
57
 
101
- ```bash
102
- npm install @bananapus/721-hook-v6
103
- ```
58
+ ## Integration Traps
104
59
 
105
- For projects using `forge` to manage dependencies (not recommended):
60
+ - this hook participates in treasury-facing execution, not only metadata. Teams often underestimate the economic implications of tier splits, reserve behavior, and weight adjustments.
61
+ - custom token URI resolvers should be treated as part of the project's trusted surface.
62
+ - adding a 721 hook through a deployer is easy; carrying forward the right ruleset behavior over time is where mistakes happen.
63
+ - projects should be explicit about whether the hook should affect pay, cash out, or only metadata-facing paths.
106
64
 
107
- ```bash
108
- forge install Bananapus/nana-721-hook-v6
109
- ```
65
+ ## Where State Lives
110
66
 
111
- If you're using `forge` to manage dependencies, add `libs = ["node_modules", "lib"]` to `foundry.toml` so Foundry resolves imports from both locations. No `remappings.txt` needed.
67
+ - tier definitions and accounting live primarily in `JB721TiersHookStore`
68
+ - project-facing execution and permission checks live in `JB721TiersHook`
69
+ - collection-specific presentation usually lives outside this repo in a resolver contract
112
70
 
113
- ## Develop
71
+ 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."
114
72
 
115
- `nana-721-hook-v6` uses [npm](https://www.npmjs.com/) (version >=20.0.0) for package management and the [Foundry](https://github.com/foundry-rs/foundry) development toolchain for builds, tests, and deployments. To get set up, [install Node.js](https://nodejs.org/en/download) and install [Foundry](https://github.com/foundry-rs/foundry):
73
+ ## Install
116
74
 
117
75
  ```bash
118
- curl -L https://foundry.paradigm.xyz | sh
76
+ npm install @bananapus/721-hook-v6
119
77
  ```
120
78
 
121
- You can download and install dependencies with:
79
+ ## Development
122
80
 
123
81
  ```bash
124
- npm ci && forge install
82
+ npm install
83
+ forge build
84
+ forge test
125
85
  ```
126
86
 
127
- If you run into trouble with `forge install`, try using `git submodule update --init --recursive` to ensure that nested submodules have been properly initialized.
128
-
129
- Some useful commands:
130
-
131
- | Command | Description |
132
- | --- | --- |
133
- | `forge build` | Compile the contracts and write artifacts to `out`. |
134
- | `forge fmt` | Lint. |
135
- | `forge test` | Run the tests. |
136
- | `forge build --sizes` | Get contract sizes. |
137
- | `forge coverage` | Generate a test coverage report. |
138
- | `foundryup` | Update Foundry. Run this periodically. |
139
- | `forge clean` | Remove the build artifacts and cache directories. |
140
-
141
- To learn more, visit the [Foundry Book](https://book.getfoundry.sh/) docs.
87
+ Useful scripts:
142
88
 
143
- ### Configuration
89
+ - `npm run deploy:mainnets`
90
+ - `npm run deploy:testnets`
144
91
 
145
- Key settings from `foundry.toml`:
92
+ ## Deployment Notes
146
93
 
147
- | Setting | Value |
148
- | --- | --- |
149
- | Solidity compiler | 0.8.28 |
150
- | EVM target | cancun |
151
- | Optimizer runs | 200 |
152
- | Fuzz runs | 4,096 |
153
- | Invariant runs | 1,024 |
154
- | Invariant depth | 100 |
94
+ Hooks are deployed as clones and typically registered in the address registry. The package is designed to compose with Omnichain, Croptop, Defifa, Banny, and other ecosystem packages that rely on tier-aware NFT issuance.
155
95
 
156
96
  ## Repository Layout
157
97
 
98
+ ```text
99
+ src/
100
+ JB721TiersHook.sol
101
+ JB721TiersHookStore.sol
102
+ JB721TiersHookDeployer.sol
103
+ JB721TiersHookProjectDeployer.sol
104
+ abstract/
105
+ interfaces/
106
+ libraries/
107
+ structs/
108
+ test/
109
+ unit, E2E, fork, invariant, audit, and regression coverage
110
+ script/
111
+ Deploy.s.sol
112
+ helpers/
158
113
  ```
159
- nana-721-hook-v6/
160
- ├── script/
161
- │ ├── Deploy.s.sol - Deploys the hook store, hook deployer, and project deployer.
162
- │ └── helpers/
163
- │ └── Hook721DeploymentLib.sol - Internal helpers for deployment scripts.
164
- ├── src/
165
- │ ├── JB721TiersHook.sol - The core tiered NFT pay/cash out hook (794 lines).
166
- │ ├── JB721TiersHookDeployer.sol - Deploys a hook clone for an existing project (115 lines).
167
- │ ├── JB721TiersHookProjectDeployer.sol - Deploys a project with a hook (418 lines).
168
- │ ├── JB721TiersHookStore.sol - Shared data store for all hooks (1,240 lines).
169
- │ ├── abstract/
170
- │ │ ├── JB721Hook.sol - Abstract base: pay/cash out lifecycle, metadata, terminal validation.
171
- │ │ └── ERC721.sol - Clone-compatible ERC-721 with mutable name/symbol.
172
- │ ├── interfaces/
173
- │ │ ├── IJB721Hook.sol
174
- │ │ ├── IJB721TiersHook.sol
175
- │ │ ├── IJB721TiersHookDeployer.sol
176
- │ │ ├── IJB721TiersHookProjectDeployer.sol
177
- │ │ ├── IJB721TiersHookStore.sol
178
- │ │ └── IJB721TokenUriResolver.sol
179
- │ ├── libraries/
180
- │ │ ├── JB721TiersHookLib.sol - Tier adjustments, split distribution, price normalization, URI resolution.
181
- │ │ ├── JB721TiersRulesetMetadataResolver.sol - Pack/unpack per-ruleset metadata.
182
- │ │ ├── JB721Constants.sol - DISCOUNT_DENOMINATOR = 200.
183
- │ │ ├── JBBitmap.sol - Word-based bitmap for removed tier tracking.
184
- │ │ └── JBIpfsDecoder.sol - Decodes bytes32-encoded IPFS CIDv0 hashes.
185
- │ └── structs/
186
- │ ├── JB721Tier.sol - Read-only tier view.
187
- │ ├── JB721TierConfig.sol - Tier configuration input.
188
- │ ├── JBStored721Tier.sol - Packed on-chain tier storage.
189
- │ ├── JB721InitTiersConfig.sol - Tiers + currency + decimals.
190
- │ ├── JBDeploy721TiersHookConfig.sol - Full hook deployment config.
191
- │ ├── JB721TiersHookFlags.sol - Hook-wide boolean flags.
192
- │ ├── JB721TiersRulesetMetadata.sol - Per-ruleset options.
193
- │ ├── JB721TiersMintReservesConfig.sol - Reserve minting input.
194
- │ ├── JB721TiersSetDiscountPercentConfig.sol - Discount change input.
195
- │ ├── JBBitmapWord.sol - Single bitmap word.
196
- │ ├── JBLaunchProjectConfig.sol - Project launch config.
197
- │ ├── JBLaunchRulesetsConfig.sol - Ruleset launch config.
198
- │ ├── JBPayDataHookRulesetConfig.sol - Ruleset config with data hook.
199
- │ ├── JBPayDataHookRulesetMetadata.sol - Ruleset metadata with data hook fields.
200
- │ └── JBQueueRulesetsConfig.sol - Queue rulesets config.
201
- └── test/
202
- ├── E2E/
203
- │ └── Pay_Mint_Redeem_E2E.t.sol - End-to-end payment, minting, and cash out test.
204
- ├── unit/
205
- │ ├── pay_Unit.t.sol - Payment and minting logic.
206
- │ ├── pay_CrossCurrency_Unit.t.sol - Cross-currency price normalization.
207
- │ ├── redeem_Unit.t.sol - Cash out (burn-to-reclaim) logic.
208
- │ ├── adjustTier_Unit.t.sol - Tier addition and removal.
209
- │ ├── deployer_Unit.t.sol - Deployer contract tests.
210
- │ ├── getters_constructor_Unit.t.sol - View functions and initialization.
211
- │ ├── mintFor_mintReservesFor_Unit.t.sol - Owner minting and reserve minting.
212
- │ ├── splitHookDistribution_Unit.t.sol - Split hook fund routing.
213
- │ ├── tierSplitRouting_Unit.t.sol - Per-tier split recipient routing.
214
- │ ├── JB721TiersRulesetMetadataResolver.t.sol - Metadata pack/unpack.
215
- │ ├── JBBitmap.t.sol - Bitmap operations.
216
- │ ├── JBIpfsDecoder.t.sol - IPFS URI decoding.
217
- │ └── TierSupplyReserveCheck.t.sol - Supply and reserve boundary checks.
218
- ├── fork/
219
- │ ├── ERC20CashOutFork.t.sol - ERC-20 token cash out on fork.
220
- │ ├── ERC20TierSplitFork.t.sol - ERC-20 tier split distribution on fork.
221
- │ └── IssueTokensForSplitsFork.t.sol - Token issuance with split flag on fork.
222
- ├── invariants/
223
- │ ├── TierLifecycleInvariant.t.sol - Tier add/remove/mint lifecycle invariants.
224
- │ ├── TieredHookStoreInvariant.t.sol - Store-level data consistency invariants.
225
- │ └── handlers/ - Invariant test handler contracts.
226
- ├── regression/
227
- │ ├── BrokenTerminalDoesNotDos.t.sol - Broken terminal does not cause DoS.
228
- │ ├── CacheTierLookup.t.sol - Tier cache regression.
229
- │ ├── ProjectDeployerRulesets.t.sol - Project deployer ruleset handling.
230
- │ ├── ReserveBeneficiaryOverwrite.t.sol - Reserve beneficiary overwrite edge case.
231
- │ ├── SplitDistributionBugs.t.sol - Split distribution edge cases.
232
- │ └── SplitNoBeneficiary.t.sol - Split with no beneficiary.
233
- ├── 721HookAttacks.t.sol - Attack vector tests (reentrancy, manipulation).
234
- ├── Fork.t.sol - General fork tests.
235
- ├── TestAuditGaps.sol - Audit gap coverage.
236
- ├── TestSafeTransferReentrancy.t.sol - safeTransferFrom reentrancy tests.
237
- ├── TestVotingUnitsLifecycle.t.sol - Voting unit lifecycle tests.
238
- └── utils/ - Shared test helpers and base workflows.
239
- ```
240
-
241
- 36 test files. 5,167 lines of source code across `src/`.
242
-
243
- ## Permissions
244
-
245
- The following `JBPermissionIds` are checked by this hook:
246
-
247
- | Permission ID | Used by | Purpose |
248
- | --- | --- | --- |
249
- | `ADJUST_721_TIERS` | `JB721TiersHook.adjustTiers` | Add or remove NFT tiers. |
250
- | `MINT_721` | `JB721TiersHook.mintFor` | Mint NFTs on-demand (only for tiers with `allowOwnerMint`). |
251
- | `SET_721_DISCOUNT_PERCENT` | `JB721TiersHook.setDiscountPercentOf` | Change a tier's discount percent. |
252
- | `SET_721_METADATA` | `JB721TiersHook.setMetadataOf` | Update the hook's base URI, contract URI, token URI resolver, or encoded IPFS URIs. |
253
- | `QUEUE_RULESETS` | `JB721TiersHookProjectDeployer.queueRulesetsOf` | Queue new rulesets for the project through the project deployer. |
254
- | `SET_TERMINALS` | `JB721TiersHookProjectDeployer.launchRulesetsFor` | Set terminals when launching rulesets. |
255
114
 
256
- All permissions are checked against the project owner via `_requirePermissionFrom`.
115
+ ## Risks And Notes
257
116
 
258
- ## Risks
117
+ - tier accounting is sensitive to reserve minting, split routing, and cross-currency normalization
118
+ - custom token URI resolvers are part of the security surface because they define how metadata is served
119
+ - projects need to be deliberate about whether the hook participates in pay, cash-out, or both paths
120
+ - tier mutations after launch are powerful and should be permissioned carefully
259
121
 
260
- - **Gas scaling with tier count.** Several read paths (`balanceOf`, `totalCashOutWeight`, `tierOfTokenId`) iterate over all tier IDs up to `maxTierId`, including removed tiers. Projects with more than ~100 active tiers should budget gas carefully for governance reads and cash outs. The `cleanTiers` function on the store can help by compacting the tier linked list after removals.
261
- - **Cash out value dilution from pending reserves.** Pending reserve NFTs are included in the `totalCashOutWeight` denominator even before they are minted. In extreme cases (high reserve frequency, many unminted reserves), this can reduce cash out value for existing holders. This is by design -- it prevents a race condition where holders cash out before reserves are minted to capture a larger share.
262
- - **ERC-2981 not supported.** ERC-2981 royalty support was removed. `supportsInterface` returns `false` for `IERC2981`, and no `royaltyInfo` function exists.
263
- - **No reentrancy guard.** The hook does not use OpenZeppelin's `ReentrancyGuard`. Safety relies on state ordering: tokens are minted and balances are updated before any external calls. The `safeTransferFrom` callback in ERC-721 is a potential reentrancy surface, but state is already settled by the time it fires.
264
- - **Immutable price feeds.** The `PRICES` contract is set at deployment and cannot be changed. If a price feed reverts (e.g., stale Chainlink data), all cross-currency minting operations for that pair will also revert until the feed recovers. Same-currency operations are unaffected.
265
- - **Clone architecture.** Each `JB721TiersHook` is deployed as a minimal proxy (clone) of a reference implementation. The reference implementation's immutable storage slots (DIRECTORY, PRICES, RULESETS, STORE, SPLITS) are shared across all clones. If the reference implementation has a bug, all clones are affected.
266
- - **Silent skip on currency mismatch without PRICES.** If the hook's `PRICES` is the zero address and a payment arrives in a different currency than the tier prices, the payment is silently ignored -- no NFTs are minted and no revert occurs. The payment funds still enter the project treasury.
267
- - **Discount percent denominator.** The discount denominator is 200, not 100 or 10,000. A `discountPercent` of 100 means 50% off, not 100% off. Setting it to 200 makes the tier free.
268
- - **Removed tiers still occupy ID space.** Removing a tier marks it in a bitmap but does not reclaim the tier ID. Iteration still traverses removed IDs (skipping them), so removing tiers does not reduce gas costs for reads until `cleanTiers` is called.
122
+ 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.
package/RISKS.md CHANGED
@@ -1,20 +1,37 @@
1
- # RISKS.md -- nana-721-hook-v6
1
+ # Juicebox 721 Hook Risk Register
2
+
3
+ This file focuses on the tiered-NFT accounting, reserve-mint, and cash-out risks in the shared 721 hook used across multiple higher-level products.
4
+
5
+ ## How to use this file
6
+
7
+ - Read `Priority risks` first; they summarize the shared 721-hook risks with the widest blast radius.
8
+ - Use the detailed sections below for reentrancy, gas, tier accounting, and integration reasoning.
9
+ - Treat `Invariants to Verify` as required regression coverage for any hook or store change.
10
+
11
+ ## Priority risks
12
+
13
+ | Priority | Risk | Why it matters | Primary controls |
14
+ |----------|------|----------------|------------------|
15
+ | P0 | Shared store corruption or accounting drift | `JB721TiersHookStore` is reused across products; a tier-accounting bug can affect Defifa, Croptop, Banny, revnets, and standalone hooks simultaneously. | Heavy store testing, invariant coverage, and cautious upgrades or deployments. |
16
+ | P1 | Gas and iteration ceilings around tier state | Tier operations can iterate over reserves, pricing state, and cash-out weights; poorly bounded use can become a liveness issue. | Explicit gas tests, tier-count limits, and section 4 DoS analysis. |
17
+ | P1 | Cash-out and reserve math mismatch | Fair redemption depends on tier supply, pending reserves, and pricing state staying aligned. | Detailed invariants, fuzzing, and integration tests with downstream consumers. |
18
+
2
19
 
3
20
  ## 1. Trust Assumptions
4
21
 
5
22
  - **Store contract (`JB721TiersHookStore`) is fully trusted.** Record functions (`recordMint`, `recordBurn`, `recordAddTiers`, `recordTransferForTier`, `recordFlags`) have no access control -- they key state by `msg.sender`. Any address can call the store to manipulate state for a hook address it controls, but cannot affect other hooks.
6
- - **Tier configuration is partially immutable.** Once created: `price`, `initialSupply`, `reserveFrequency`, `category`, `votingUnits`, `splitPercent` are permanent. Mutable: `discountPercent` (owner-controlled, subject to `cannotIncreaseDiscountPercent`), `encodedIPFSUri` (owner-controlled).
23
+ - **Tier configuration is partially immutable.** Once created: `price`, `initialSupply`, `reserveFrequency`, `category`, `votingUnits`, `splitPercent` are permanent. Mutable: `discountPercent` (owner-controlled, subject to `flags.cantIncreaseDiscountPercent`), `encodedIPFSUri` (owner-controlled).
7
24
  - **Category sort order is enforced only at insertion.** `recordAddTiers` reverts `InvalidCategorySortOrder` if tiers are not ascending by category. The sorted linked list (`_tierIdAfter`) depends on this invariant across all `adjustTiers` calls. Direct store callers could corrupt the list.
8
- - **`useReserveBeneficiaryAsDefault` has global side effects.** Setting this on ANY new tier overwrites `defaultReserveBeneficiaryOf` for ALL existing tiers that lack a tier-specific `_reserveBeneficiaryOf` entry. Documented but dangerous when calling `adjustTiers` on hooks with existing tiers.
25
+ - **`flags.useReserveBeneficiaryAsDefault` has global side effects.** Setting this on ANY new tier overwrites `defaultReserveBeneficiaryOf` for ALL existing tiers that lack a tier-specific `_reserveBeneficiaryOf` entry. Documented but dangerous when calling `adjustTiers` on hooks with existing tiers.
9
26
  - **Clone initialization is one-shot, atomic.** `initialize()` guards via an `_initialized` bool flag. The implementation contract's constructor sets `_initialized = true`, blocking direct initialization. Clones start with `_initialized = false` and set it to `true` during `initialize()`. After `initialize()` sets it, any subsequent call reverts. Deployer contracts call deploy+initialize in a single transaction, preventing front-running. Ownership transfers to `_msgSender()` at the end of `initialize`.
10
- - **`balanceOf(address(0))` reverts.** Unlike the standard ERC-721 `balanceOf` which may return zero, the hook overrides `balanceOf` to revert with `JB721TiersHook_ZeroAddress` when called with `address(0)`. This guards against misleading zero-balance results for the zero address.
27
+ - **`balanceOf(address(0))` reverts with a hook-specific error.** The hook explicitly reverts with `JB721TiersHook_ZeroAddress` when called with `address(0)`. This matches standard ERC-721 semantics but is still relevant for integrators that key off the custom error surface.
11
28
  - **`tokenURI` reverts for nonexistent tokens.** Calling `tokenURI` with a token ID that has never been minted reverts with `ERC721NonexistentToken(tokenId)`. The check is `_ownerOf(tokenId) == address(0)`.
12
29
  - **JBDirectory is trusted for terminal authentication.** `afterPayRecordedWith` and `afterCashOutRecordedWith` check `DIRECTORY.isTerminalOf()`. If the directory is compromised, arbitrary addresses can invoke pay/cashout hooks.
13
30
  - **JBPrices is trusted for cross-currency conversion.** A reverting price feed blocks all payments in non-matching currencies (DoS, not fund loss). If `address(prices) == address(0)`, cross-currency payments silently skip minting.
14
31
 
15
32
  ## 2. Economic Risks
16
33
 
17
- - **Cash out weight uses full undiscounted price.** `cashOutWeightOf` and `totalCashOutWeight` always use `storedTier.price`, not the discounted price. NFTs bought at a discount have cash-out value proportional to the full tier price. A `discountPercent=200` (100% off, denominator is 200) enables free minting with full cash-out weight. Mitigated by `cannotIncreaseDiscountPercent` flag.
34
+ - **Cash out weight uses full undiscounted price.** `cashOutWeightOf` and `totalCashOutWeight` always use `storedTier.price`, not the discounted price. NFTs bought at a discount have cash-out value proportional to the full tier price. A `discountPercent=200` (100% off, denominator is 200) enables free minting with full cash-out weight. Mitigated by `flags.cantIncreaseDiscountPercent` flag.
18
35
  - **Pending reserves inflate the `totalCashOutWeight` denominator.** The total includes `price * pendingReserves` for unminted reserve NFTs. This dilutes per-NFT reclaim value before reserves are actually minted. Effect is proportional to reserve frequency and number of unminted reserves.
19
36
  - **Pay credits accumulate without cap.** `payCreditsOf` grows from leftover amounts after minting. Credits are per-beneficiary, not per-payer. When `payer != beneficiary`, overspend accrues to the beneficiary's credits; the payer's existing credits are not applied. No upper bound on accumulation.
20
37
  - **Zero-price tiers are valid.** A tier with `price=0` allows free minting. Cash-out weight for price-0 tiers is zero, so no value extraction risk. However, they still consume supply and generate pending reserves if `reserveFrequency > 0`.
@@ -49,11 +66,11 @@
49
66
 
50
67
  ## 5. Access Control
51
68
 
52
- - **`adjustTiers` (add/remove):** Requires `ADJUST_721_TIERS` permission from `owner()`. Respects `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting` flags (append-only restrictions). `cannotBeRemoved` flag on individual tiers is enforced by the store.
53
- - **`mintFor` (owner minting):** Requires `MINT_721` permission. Bypasses price checks (`amount: type(uint256).max`). Still requires per-tier `allowOwnerMint` flag. Tiers with `reserveFrequency > 0` cannot have `allowOwnerMint` (enforced at creation).
54
- - **`setDiscountPercentOf`:** Requires `SET_721_DISCOUNT_PERCENT` permission. Cannot increase discount if `cannotIncreaseDiscountPercent` is set on the tier. Can always decrease.
69
+ - **`adjustTiers` (add/remove):** Requires `ADJUST_721_TIERS` permission from `owner()`. Respects `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting` flags (append-only restrictions). `flags.cantBeRemoved` flag on individual tiers is enforced by the store.
70
+ - **`mintFor` (owner minting):** Requires `MINT_721` permission. Bypasses price checks (`amount: type(uint256).max`). Still requires per-tier `flags.allowOwnerMint` flag. Tiers with `reserveFrequency > 0` cannot have `flags.allowOwnerMint` (enforced at creation).
71
+ - **`setDiscountPercentOf`:** Requires `SET_721_DISCOUNT_PERCENT` permission. Cannot increase discount if `flags.cantIncreaseDiscountPercent` is set on the tier. Can always decrease.
55
72
  - **`setMetadata`:** Requires `SET_721_METADATA` permission. Can change name, symbol, baseURI, contractURI, tokenUriResolver, and per-tier IPFS URIs. Sentinel value `IJB721TokenUriResolver(address(this))` means "no change" for resolver.
56
- - **Transfer pause:** Ruleset-level flag (`transfersPaused` in 721-specific metadata, bit 0). Only applies to tiers with `transfersPausable = true`. Burns (transfer to address(0)) are never paused. Tiers created with `transfersPausable = false` can never be paused.
73
+ - **Transfer pause:** Ruleset-level flag (`transfersPaused` in 721-specific metadata, bit 0). Only applies to tiers with `flags.transfersPausable = true`. Burns (transfer to address(0)) are never paused. Tiers created with `flags.transfersPausable = false` can never be paused.
57
74
  - **`mintPendingReservesFor`:** Permissionless. Only gated by `mintPendingReservesPaused` ruleset flag (bit 1 of 721 metadata).
58
75
  - **`cleanTiers`:** Permissionless, idempotent. Compacts the sorted tier list by removing gaps from deleted tiers. No economic impact.
59
76
  - **Store `recordFlags`:** No access control -- stores against `msg.sender`. Safe because the store keys by caller address, but a compromised hook can freely change its own flags.
@@ -65,7 +82,7 @@
65
82
  - **`beforeCashOutRecordedWith` rejects fungible tokens.** Reverts with `JB721Hook_UnexpectedTokenCashedOut` if `context.cashOutCount > 0`. Cannot simultaneously cash out NFTs and fungible tokens in the same terminal call.
66
83
  - **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.
67
84
  - **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.
68
- - **ETH sent with `afterPayRecordedWith` is used for split distribution.** If no splits are configured but ETH is forwarded (non-zero `forwardedAmount`), the `hookMetadata` is empty and `distributeAll` is not called. The ETH remains in the hook contract with no recovery mechanism.
85
+ - **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.
69
86
  - **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).
70
87
 
71
88
  ## 7. Invariants to Verify
@@ -80,7 +97,7 @@
80
97
  - **`maxTierIdOf` monotonically increases:** Tier removal marks a bitmap, does not decrement `maxTierIdOf`.
81
98
  - **Balance consistency:** `sum(tierBalanceOf[hook][owner][tierId])` across all tiers equals `ERC721._balances[owner]` for each owner.
82
99
  - **Cash out weight uses full price regardless of discount:** `cashOutWeightOf` for any token returns the tier's stored `price`, not the discounted purchase price.
83
- - **Discount monotonicity when locked:** If `cannotIncreaseDiscountPercent` is set, `discountPercent` can only decrease or stay the same.
100
+ - **Discount monotonicity when locked:** If `flags.cantIncreaseDiscountPercent` is set, `discountPercent` can only decrease or stay the same.
84
101
  - **Flags are append-only restrictions:** `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting` prevent future tiers from using those features but do not retroactively affect existing tiers.
85
102
 
86
103
  ## 8. Accepted Behaviors