@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.
- package/ADMINISTRATION.md +38 -11
- package/ARCHITECTURE.md +53 -99
- package/AUDIT_INSTRUCTIONS.md +84 -383
- package/CHANGELOG.md +71 -0
- package/README.md +79 -225
- package/RISKS.md +28 -11
- package/SKILLS.md +29 -296
- package/STYLE_GUIDE.md +57 -18
- package/USER_JOURNEYS.md +57 -501
- package/package.json +1 -1
- package/references/operations.md +28 -0
- package/references/runtime.md +32 -0
- package/script/Deploy.s.sol +5 -4
- package/src/JB721TiersHook.sol +1 -1
- package/src/JB721TiersHookDeployer.sol +1 -1
- package/src/JB721TiersHookProjectDeployer.sol +1 -1
- package/src/JB721TiersHookStore.sol +23 -17
- package/src/libraries/JB721Constants.sol +1 -1
- package/src/libraries/JB721TiersRulesetMetadataResolver.sol +1 -1
- package/src/libraries/JBBitmap.sol +1 -1
- package/src/libraries/JBIpfsDecoder.sol +1 -1
- package/src/structs/JB721Tier.sol +5 -11
- package/src/structs/JB721TierConfig.sol +5 -20
- package/src/structs/JB721TierConfigFlags.sol +26 -0
- package/src/structs/JB721TierFlags.sol +17 -0
- package/test/721HookAttacks.t.sol +22 -17
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +19 -14
- package/test/Fork.t.sol +69 -54
- package/test/TestAuditGaps.sol +73 -56
- package/test/TestSafeTransferReentrancy.t.sol +4 -4
- package/test/TestVotingUnitsLifecycle.t.sol +11 -11
- package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +10 -7
- package/test/audit/CodexSplitCreditsMismatch.t.sol +10 -7
- package/test/fork/ERC20CashOutFork.t.sol +37 -28
- package/test/fork/ERC20TierSplitFork.t.sol +28 -21
- package/test/fork/IssueTokensForSplitsFork.t.sol +10 -7
- package/test/invariants/handlers/TierLifecycleHandler.sol +10 -7
- package/test/invariants/handlers/TierStoreHandler.sol +10 -7
- package/test/regression/ProjectDeployerRulesets.t.sol +10 -7
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +6 -6
- package/test/unit/AuditFixes_Unit.t.sol +37 -28
- package/test/unit/adjustTier_Unit.t.sol +268 -202
- package/test/unit/getters_constructor_Unit.t.sol +20 -14
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +2 -2
- package/test/unit/pay_Unit.t.sol +1 -1
- package/CHANGE_LOG.md +0 -359
package/README.md
CHANGED
|
@@ -1,268 +1,122 @@
|
|
|
1
1
|
# Juicebox 721 Hook
|
|
2
2
|
|
|
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
7
|
|
|
7
|
-
##
|
|
8
|
+
## Overview
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
This package is the standard NFT hook for the V6 ecosystem. Projects use it to:
|
|
10
11
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
33
|
+
## Mental Model
|
|
46
34
|
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
##
|
|
50
|
+
## Read These Files First
|
|
98
51
|
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
npm install @bananapus/721-hook-v6
|
|
103
|
-
```
|
|
58
|
+
## Integration Traps
|
|
104
59
|
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
forge install Bananapus/nana-721-hook-v6
|
|
109
|
-
```
|
|
65
|
+
## Where State Lives
|
|
110
66
|
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
## Install
|
|
116
74
|
|
|
117
75
|
```bash
|
|
118
|
-
|
|
76
|
+
npm install @bananapus/721-hook-v6
|
|
119
77
|
```
|
|
120
78
|
|
|
121
|
-
|
|
79
|
+
## Development
|
|
122
80
|
|
|
123
81
|
```bash
|
|
124
|
-
npm
|
|
82
|
+
npm install
|
|
83
|
+
forge build
|
|
84
|
+
forge test
|
|
125
85
|
```
|
|
126
86
|
|
|
127
|
-
|
|
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
|
-
|
|
89
|
+
- `npm run deploy:mainnets`
|
|
90
|
+
- `npm run deploy:testnets`
|
|
144
91
|
|
|
145
|
-
|
|
92
|
+
## Deployment Notes
|
|
146
93
|
|
|
147
|
-
|
|
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
|
-
|
|
115
|
+
## Risks And Notes
|
|
257
116
|
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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 `
|
|
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
|
|
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 `
|
|
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). `
|
|
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 `
|
|
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
|
-
- **
|
|
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 `
|
|
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
|