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