@bananapus/721-hook-v6 0.0.17 → 0.0.19
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/ARCHITECTURE.md +4 -0
- package/CHANGE_LOG.md +47 -11
- package/README.md +19 -9
- package/RISKS.md +4 -0
- package/USER_JOURNEYS.md +4 -0
- package/package.json +2 -2
- package/src/JB721TiersHook.sol +87 -85
- package/src/abstract/JB721Hook.sol +2 -2
- package/src/libraries/JB721TiersHookLib.sol +4 -8
- package/test/TestAuditGaps.sol +147 -0
- package/test/fork/ERC20CashOutFork.t.sol +612 -0
- package/test/fork/IssueTokensForSplitsFork.t.sol +504 -0
- package/docs/book.css +0 -13
- package/docs/book.toml +0 -12
- package/docs/solidity.min.js +0 -74
- package/docs/src/README.md +0 -253
- package/docs/src/SUMMARY.md +0 -38
- package/docs/src/src/JB721TiersHook.sol/contract.JB721TiersHook.md +0 -645
- package/docs/src/src/JB721TiersHookDeployer.sol/contract.JB721TiersHookDeployer.md +0 -99
- package/docs/src/src/JB721TiersHookProjectDeployer.sol/contract.JB721TiersHookProjectDeployer.md +0 -288
- package/docs/src/src/JB721TiersHookStore.sol/contract.JB721TiersHookStore.md +0 -1096
- package/docs/src/src/README.md +0 -11
- package/docs/src/src/abstract/ERC721.sol/abstract.ERC721.md +0 -430
- package/docs/src/src/abstract/JB721Hook.sol/abstract.JB721Hook.md +0 -309
- package/docs/src/src/abstract/README.md +0 -5
- package/docs/src/src/interfaces/IJB721Hook.sol/interface.IJB721Hook.md +0 -29
- package/docs/src/src/interfaces/IJB721TiersHook.sol/interface.IJB721TiersHook.md +0 -203
- package/docs/src/src/interfaces/IJB721TiersHookDeployer.sol/interface.IJB721TiersHookDeployer.md +0 -25
- package/docs/src/src/interfaces/IJB721TiersHookProjectDeployer.sol/interface.IJB721TiersHookProjectDeployer.md +0 -64
- package/docs/src/src/interfaces/IJB721TiersHookStore.sol/interface.IJB721TiersHookStore.md +0 -265
- package/docs/src/src/interfaces/IJB721TokenUriResolver.sol/interface.IJB721TokenUriResolver.md +0 -12
- package/docs/src/src/interfaces/README.md +0 -9
- package/docs/src/src/libraries/JB721Constants.sol/library.JB721Constants.md +0 -14
- package/docs/src/src/libraries/JB721TiersRulesetMetadataResolver.sol/library.JB721TiersRulesetMetadataResolver.md +0 -68
- package/docs/src/src/libraries/JBBitmap.sol/library.JBBitmap.md +0 -82
- package/docs/src/src/libraries/JBIpfsDecoder.sol/library.JBIpfsDecoder.md +0 -61
- package/docs/src/src/libraries/README.md +0 -7
- package/docs/src/src/structs/JB721InitTiersConfig.sol/struct.JB721InitTiersConfig.md +0 -27
- package/docs/src/src/structs/JB721Tier.sol/struct.JB721Tier.md +0 -59
- package/docs/src/src/structs/JB721TierConfig.sol/struct.JB721TierConfig.md +0 -60
- package/docs/src/src/structs/JB721TiersHookFlags.sol/struct.JB721TiersHookFlags.md +0 -26
- package/docs/src/src/structs/JB721TiersMintReservesConfig.sol/struct.JB721TiersMintReservesConfig.md +0 -16
- package/docs/src/src/structs/JB721TiersRulesetMetadata.sol/struct.JB721TiersRulesetMetadata.md +0 -20
- package/docs/src/src/structs/JB721TiersSetDiscountPercentConfig.sol/struct.JB721TiersSetDiscountPercentConfig.md +0 -16
- package/docs/src/src/structs/JBBitmapWord.sol/struct.JBBitmapWord.md +0 -19
- package/docs/src/src/structs/JBDeploy721TiersHookConfig.sol/struct.JBDeploy721TiersHookConfig.md +0 -34
- package/docs/src/src/structs/JBLaunchProjectConfig.sol/struct.JBLaunchProjectConfig.md +0 -23
- package/docs/src/src/structs/JBLaunchRulesetsConfig.sol/struct.JBLaunchRulesetsConfig.md +0 -22
- package/docs/src/src/structs/JBPayDataHookRulesetConfig.sol/struct.JBPayDataHookRulesetConfig.md +0 -51
- package/docs/src/src/structs/JBPayDataHookRulesetMetadata.sol/struct.JBPayDataHookRulesetMetadata.md +0 -66
- package/docs/src/src/structs/JBQueueRulesetsConfig.sol/struct.JBQueueRulesetsConfig.md +0 -21
- package/docs/src/src/structs/JBStored721Tier.sol/struct.JBStored721Tier.md +0 -42
- package/docs/src/src/structs/README.md +0 -18
package/ARCHITECTURE.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
NFT tier system for Juicebox V6. Allows projects to attach tiered NFT minting to payments and use NFTs as cash-out hooks. Supports on-chain and off-chain metadata, category-based sorting, and configurable pricing with discounts.
|
|
6
6
|
|
|
7
|
+
The design permits a high theoretical tier ceiling, but several important reads and cash-out calculations still scale
|
|
8
|
+
with `maxTierId`. In practice, this should be treated as a curated-catalog hook with an explicit operating envelope,
|
|
9
|
+
not as a guarantee that very large catalogs are comfortable to run on-chain.
|
|
10
|
+
|
|
7
11
|
## Contract Map
|
|
8
12
|
|
|
9
13
|
```
|
package/CHANGE_LOG.md
CHANGED
|
@@ -113,15 +113,27 @@ The ERC721 collection `name` and `symbol` can now be changed after initializatio
|
|
|
113
113
|
| `_setName(string memory)` | Updates the token collection name |
|
|
114
114
|
| `_setSymbol(string memory)` | Updates the token collection symbol |
|
|
115
115
|
|
|
116
|
-
### 2.3 `
|
|
116
|
+
### 2.3 `split.hook` Support in Tier Distribution
|
|
117
|
+
|
|
118
|
+
Tier split payouts now support the `split.hook` field (`IJBSplitHook`). The distribution priority follows the same order as `JBMultiTerminal`: hook > projectId > beneficiary. In v5, tier splits did not exist. In v6, this ensures full parity with core split behavior.
|
|
119
|
+
|
|
120
|
+
### 2.4 DOS Protection on Split Distribution
|
|
121
|
+
|
|
122
|
+
All external calls during tier split distribution (split hooks, terminal `pay`/`addToBalanceOf`, and leftover distribution) are wrapped in try-catch. A reverting split recipient or terminal cannot brick payments to the project. On failure, funds are returned to the project balance and a `SplitPayoutReverted` or `AddToBalanceReverted` event is emitted with the revert reason. For ERC-20 split hook failures, tokens are transferred first and the hook callback is best-effort; for ERC-20 terminal failures, the approval is reset to zero.
|
|
123
|
+
|
|
124
|
+
### 2.5 `splitPercent` Bounds Validation
|
|
125
|
+
|
|
126
|
+
`JB721TiersHookStore.recordAddTiers` now validates that each tier's `splitPercent` does not exceed `JBConstants.SPLITS_TOTAL_PERCENT`. If it does, it reverts with `JB721TiersHookStore_SplitPercentExceedsBounds(uint256 percent, uint256 limit)`.
|
|
127
|
+
|
|
128
|
+
### 2.6 `beforePayRecordedWith` Override in `JB721TiersHook`
|
|
117
129
|
|
|
118
130
|
v6 overrides `beforePayRecordedWith` in `JB721TiersHook` (not just the base `JB721Hook`). The override calculates per-tier split amounts and adjusts the minting weight so the terminal only mints project tokens for the portion of the payment that actually enters the project. It also sets the `JBPayHookSpecification.amount` to the total split amount so the terminal forwards those funds to the hook for distribution.
|
|
119
131
|
|
|
120
|
-
### 2.
|
|
132
|
+
### 2.7 Pricing Decimals Validation
|
|
121
133
|
|
|
122
134
|
`initialize(...)` now reverts with `JB721TiersHook_InvalidPricingDecimals` if `tiersConfig.decimals > 18`.
|
|
123
135
|
|
|
124
|
-
### 2.
|
|
136
|
+
### 2.8 `allowSetCustomToken` Pass-Through
|
|
125
137
|
|
|
126
138
|
The `JBPayDataHookRulesetMetadata` struct gained an `allowSetCustomToken` field. In v5, the project deployer hardcoded this to `false`. In v6, it passes through the value from the config.
|
|
127
139
|
|
|
@@ -133,8 +145,10 @@ The `JBPayDataHookRulesetMetadata` struct gained an `allowSetCustomToken` field.
|
|
|
133
145
|
|
|
134
146
|
| Event | Signature | Description |
|
|
135
147
|
|-------|-----------|-------------|
|
|
148
|
+
| `AddToBalanceReverted` | `AddToBalanceReverted(uint256 indexed projectId, address token, uint256 amount, bytes reason)` | Emitted when leftover distribution's `addToBalanceOf` call fails. Funds remain in the hook contract. |
|
|
136
149
|
| `SetName` | `SetName(string indexed name, address caller)` | Emitted when the collection name is changed |
|
|
137
150
|
| `SetSymbol` | `SetSymbol(string indexed symbol, address caller)` | Emitted when the collection symbol is changed |
|
|
151
|
+
| `SplitPayoutReverted` | `SplitPayoutReverted(uint256 indexed projectId, JBSplit split, uint256 amount, bytes reason, address caller)` | Emitted when a split payout reverts during distribution. The failed split's funds route to the project balance. |
|
|
138
152
|
|
|
139
153
|
### 3.2 New Event on `IJB721TiersHookStore`
|
|
140
154
|
|
|
@@ -144,7 +158,7 @@ The `JBPayDataHookRulesetMetadata` struct gained an `allowSetCustomToken` field.
|
|
|
144
158
|
|
|
145
159
|
### 3.3 Events Unchanged
|
|
146
160
|
|
|
147
|
-
All other events (`AddPayCredits`, `AddTier`, `Mint`, `MintReservedNft`, `RemoveTier`, `SetBaseUri`, `SetContractUri`, `SetDiscountPercent`, `SetEncodedIPFSUri`, `SetTokenUriResolver`, `UsePayCredits`, `CleanTiers`, `HookDeployed`) remain
|
|
161
|
+
All other events (`AddPayCredits`, `AddTier`, `Mint`, `MintReservedNft`, `RemoveTier`, `SetBaseUri`, `SetContractUri`, `SetDiscountPercent`, `SetEncodedIPFSUri`, `SetTokenUriResolver`, `UsePayCredits`, `CleanTiers`, `HookDeployed`) remain unchanged.
|
|
148
162
|
|
|
149
163
|
---
|
|
150
164
|
|
|
@@ -157,11 +171,17 @@ All other events (`AddPayCredits`, `AddTier`, `Mint`, `MintReservedNft`, `Remove
|
|
|
157
171
|
| `JB721TiersHook_CurrencyMismatch` | `(uint256 paymentCurrency, uint256 tierCurrency)` | Reserved for currency mismatch detection |
|
|
158
172
|
| `JB721TiersHook_InvalidPricingDecimals` | `(uint256 decimals)` | Reverts when `tiersConfig.decimals > 18` during initialization |
|
|
159
173
|
|
|
160
|
-
### 4.2
|
|
174
|
+
### 4.2 New Error on `JB721TiersHookStore`
|
|
175
|
+
|
|
176
|
+
| Error | Signature | Description |
|
|
177
|
+
|-------|-----------|-------------|
|
|
178
|
+
| `JB721TiersHookStore_SplitPercentExceedsBounds` | `(uint256 percent, uint256 limit)` | Reverts when a tier's `splitPercent` exceeds `JBConstants.SPLITS_TOTAL_PERCENT` during `recordAddTiers` |
|
|
179
|
+
|
|
180
|
+
### 4.3 Store Errors with Added `tierId` Parameter
|
|
161
181
|
|
|
162
182
|
See Section 1.7 above. Eight store errors gained a `tierId` parameter for improved debuggability.
|
|
163
183
|
|
|
164
|
-
### 4.
|
|
184
|
+
### 4.4 Errors Unchanged
|
|
165
185
|
|
|
166
186
|
All other errors (`JB721Hook_InvalidPay`, `JB721Hook_InvalidCashOut`, `JB721Hook_UnauthorizedToken`, `JB721Hook_UnexpectedTokenCashedOut`, `JB721TiersHook_AlreadyInitialized`, `JB721TiersHook_NoProjectId`, `JB721TiersHook_Overspending`, `JB721TiersHook_MintReserveNftsPaused`, `JB721TiersHook_TierTransfersPaused`) remain unchanged.
|
|
167
187
|
|
|
@@ -240,15 +260,31 @@ In v5, `_packedPricingContext` packed three values: currency (bits 0-31), decima
|
|
|
240
260
|
|
|
241
261
|
v5 reverted if `msg.value != 0` in `afterPayRecordedWith`. v6 removes this check because the hook now accepts forwarded native token payments for tier split distribution.
|
|
242
262
|
|
|
243
|
-
### 6.8 `
|
|
263
|
+
### 6.8 `JB721TiersHookLib._sendPayoutToSplit` — `split.hook` Priority
|
|
264
|
+
|
|
265
|
+
Split payouts follow the same priority order as `JBMultiTerminal`: if `split.hook` is set, funds are sent to the hook via `processSplitWith`; otherwise if `split.projectId` is set, funds go to the project's primary terminal; otherwise funds go to `split.beneficiary`. For ERC-20 payments to hooks, tokens are transferred first and the hook callback is best-effort (failure does not revert).
|
|
266
|
+
|
|
267
|
+
### 6.9 `JB721TiersHookLib` — Try-Catch on All External Calls
|
|
268
|
+
|
|
269
|
+
All external calls during split distribution (split hooks, terminal `pay`, terminal `addToBalanceOf`, and leftover `addToBalanceOf`) are wrapped in try-catch. On failure: native token calls return false (funds stay in the hook), ERC-20 terminal call approvals are reset to zero, and `SplitPayoutReverted` or `AddToBalanceReverted` events are emitted with the revert reason.
|
|
270
|
+
|
|
271
|
+
### 6.10 `JB721TiersHookLib.calculateSplitAmounts` — Discount Applied Before Splits
|
|
272
|
+
|
|
273
|
+
Split amounts are calculated on the discount-adjusted (effective) tier price rather than the base price. This ensures the split amount matches the actual price the minter pays when a tier discount is active.
|
|
274
|
+
|
|
275
|
+
### 6.11 `JB721TiersHookStore.recordAddTiers` — Validates `splitPercent`
|
|
276
|
+
|
|
277
|
+
`recordAddTiers` now reverts with `JB721TiersHookStore_SplitPercentExceedsBounds` if a tier's `splitPercent` exceeds `JBConstants.SPLITS_TOTAL_PERCENT`.
|
|
278
|
+
|
|
279
|
+
### 6.12 `JB721TiersHookStore.recordAddTiers` — Stores `splitPercent`
|
|
244
280
|
|
|
245
281
|
The store now stores `splitPercent` in the `JBStored721Tier` packed struct (replacing the `votingUnits` field in storage). The `votingUnits` value continues to be stored in the `_tierVotingUnitsOf` mapping.
|
|
246
282
|
|
|
247
|
-
### 6.
|
|
283
|
+
### 6.13 `JB721TiersHookStore.recordAddTiers` — Emits `SetDefaultReserveBeneficiary`
|
|
248
284
|
|
|
249
285
|
When a tier config has `useReserveBeneficiaryAsDefault` set and the beneficiary differs from the current default, v6 emits the new `SetDefaultReserveBeneficiary` event.
|
|
250
286
|
|
|
251
|
-
### 6.
|
|
287
|
+
### 6.14 `JB721TiersHookProjectDeployer` — `allowSetCustomToken` Pass-Through
|
|
252
288
|
|
|
253
289
|
In v5, the project deployer hardcoded `allowSetCustomToken: false` when constructing `JBRulesetMetadata`. In v6, it passes through `payDataRulesetConfig.metadata.allowSetCustomToken`.
|
|
254
290
|
|
|
@@ -261,7 +297,7 @@ In v5, the project deployer hardcoded `allowSetCustomToken: false` when construc
|
|
|
261
297
|
| v5 | v6 | Notes |
|
|
262
298
|
|----|----|-------|
|
|
263
299
|
| `IJB721Hook` | `IJB721Hook` | Unchanged (import path updated) |
|
|
264
|
-
| `IJB721TiersHook` | `IJB721TiersHook` | `pricingContext()` return changed; `setMetadata()` signature changed; added `PRICES()`, `SPLITS()`, `SetName`, `SetSymbol` |
|
|
300
|
+
| `IJB721TiersHook` | `IJB721TiersHook` | `pricingContext()` return changed; `setMetadata()` signature changed; added `PRICES()`, `SPLITS()`, `SetName`, `SetSymbol`, `SplitPayoutReverted`, `AddToBalanceReverted` |
|
|
265
301
|
| `IJB721TiersHookDeployer` | `IJB721TiersHookDeployer` | Unchanged |
|
|
266
302
|
| `IJB721TiersHookProjectDeployer` | `IJB721TiersHookProjectDeployer` | Unchanged |
|
|
267
303
|
| `IJB721TiersHookStore` | `IJB721TiersHookStore` | Added `SetDefaultReserveBeneficiary` event; NatSpec added |
|
|
@@ -274,7 +310,7 @@ In v5, the project deployer hardcoded `allowSetCustomToken: false` when construc
|
|
|
274
310
|
| `JB721TiersHook` | `JB721TiersHook` | Constructor gains `prices` and `splits`; tier splits system; code extracted to library |
|
|
275
311
|
| `JB721TiersHookDeployer` | `JB721TiersHookDeployer` | Unchanged (import paths updated) |
|
|
276
312
|
| `JB721TiersHookProjectDeployer` | `JB721TiersHookProjectDeployer` | `allowSetCustomToken` pass-through |
|
|
277
|
-
| `JB721TiersHookStore` | `JB721TiersHookStore` | `splitPercent` storage; error params added; `SetDefaultReserveBeneficiary` event |
|
|
313
|
+
| `JB721TiersHookStore` | `JB721TiersHookStore` | `splitPercent` storage and validation; error params added; `SplitPercentExceedsBounds` error; `SetDefaultReserveBeneficiary` event |
|
|
278
314
|
| `JB721Hook` (abstract) | `JB721Hook` (abstract) | `cashOutWeightOf`/`totalCashOutWeight` signatures simplified; `msg.value` check removed from `afterPayRecordedWith` |
|
|
279
315
|
| `ERC721` (abstract) | `ERC721` (abstract) | Added `_setName()` and `_setSymbol()` |
|
|
280
316
|
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ For projects using `forge` to manage dependencies (not recommended):
|
|
|
55
55
|
forge install Bananapus/nana-721-hook
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
If you're using `forge` to manage dependencies,
|
|
58
|
+
If you're using `forge` to manage dependencies, you'll also need to install `nana-721-hook`'s dependencies. With `libs = ["node_modules", "lib"]` in `foundry.toml`, Foundry auto-resolves import paths — no `remappings.txt` needed.
|
|
59
59
|
|
|
60
60
|
### Develop
|
|
61
61
|
|
|
@@ -136,13 +136,7 @@ This example deploys `nana-721-hook` to the Sepolia testnet using the specified
|
|
|
136
136
|
|
|
137
137
|
To view test coverage, run `npm run coverage` to generate an LCOV test report. You can use an extension like [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) to view coverage in your editor.
|
|
138
138
|
|
|
139
|
-
If you're using Nomic Foundation's [Solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) extension in VSCode, you may run into LSP errors because the extension cannot find dependencies outside of `lib`. You can often fix this by running
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
forge remappings >> remappings.txt
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
This makes the extension aware of default remappings.
|
|
139
|
+
If you're using Nomic Foundation's [Solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) extension in VSCode, you may run into LSP errors because the extension cannot find dependencies outside of `lib`. You can often fix this by running `forge remappings > remappings.txt` to generate a remappings file for the extension. This file is gitignored and only needed for editor support.
|
|
146
140
|
|
|
147
141
|
## Repository Layout
|
|
148
142
|
|
|
@@ -219,7 +213,8 @@ Each pay/cash out hook can then execute custom behavior based on the custom data
|
|
|
219
213
|
|
|
220
214
|
### Mechanism
|
|
221
215
|
|
|
222
|
-
A project using a 721 tiers hook can specify
|
|
216
|
+
A project using a 721 tiers hook can specify a large number of NFT tiers in theory (up to 65,535 total), but the
|
|
217
|
+
practical operating envelope is much smaller because several important paths still scale with `maxTierId`.
|
|
223
218
|
|
|
224
219
|
- NFT tiers can be removed by the project owner as long as they are not locked (`cannotBeRemoved`). After removing tiers, call `cleanTiers()` on the store to optimize tier iteration.
|
|
225
220
|
- NFT tiers can be added by the project owner as long as they respect the hook's `flags`. Tiers must be sorted by category in ascending order — the store reverts with `JB721TiersHookStore_InvalidCategorySortOrder` if not. The flags specify if newly added tiers can have votes (voting units), if new tiers can have non-zero reserve frequencies, if new tiers can allow on-demand minting by the project's owner, and if overspending is allowed.
|
|
@@ -246,6 +241,21 @@ Additional notes:
|
|
|
246
241
|
- If enabled by the project owner, holders can burn their NFTs to reclaim funds from the project. These cash outs are proportional to the NFTs price, relative to the combined price of all the NFTs (including pending reserves in the denominator).
|
|
247
242
|
- NFT cash outs can be enabled by setting `useDataHookForCashOut` to `true` in the project's `JBRulesetMetadata`. If NFT cash outs are enabled, project token cash outs are disabled -- attempting to cash out fungible tokens when the data hook is active will revert.
|
|
248
243
|
- Per-tier voting units can be configured: either custom voting units or the tier's price as the default. Voting power is computed per-address across all tiers.
|
|
244
|
+
|
|
245
|
+
### Supported Operating Envelope
|
|
246
|
+
|
|
247
|
+
The current implementation is best suited to curated catalogs, not effectively unbounded consumer storefronts.
|
|
248
|
+
|
|
249
|
+
- Treat roughly `<= 100` active tiers as the comfortable operating envelope for projects that expect frequent
|
|
250
|
+
`balanceOf`, governance reads, or NFT cash outs.
|
|
251
|
+
- Treat `100-200` tiers as an advanced configuration that should be used only with deliberate gas budgeting and
|
|
252
|
+
frontend/operator awareness.
|
|
253
|
+
- Above that range, the on-chain reads and cash-out accounting are still functionally correct, but several important
|
|
254
|
+
paths become increasingly expensive because they iterate the tier set.
|
|
255
|
+
|
|
256
|
+
The test suite proves survivability at `100` and `200` tiers, and also proves that `balanceOf` and
|
|
257
|
+
`totalCashOutWeight` become materially more expensive at `100` tiers than at `10` tiers. That evidence should be read
|
|
258
|
+
as a scope boundary, not as encouragement to target the `uint16.max` theoretical tier ceiling in production.
|
|
249
259
|
- The hook declares support for ERC-2981 (royalties) via `supportsInterface`, but does not implement the `royaltyInfo` function. This is intended for future extension.
|
|
250
260
|
|
|
251
261
|
### Setup
|
package/RISKS.md
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
|
|
34
34
|
- **`totalCashOutWeight` iterates ALL tier IDs** (1 to `maxTierIdOf`), including removed tiers with minted NFTs. Called during every `beforeCashOutRecordedWith`. At ~2-3k gas per tier, 500+ tiers approaches block gas limits. Could block all NFT cash-outs if an attacker with `ADJUST_721_TIERS` permission adds thousands of tiers.
|
|
35
35
|
- **`balanceOf`, `votingUnitsOf`, `totalSupplyOf` iterate all tiers.** Same pattern: loop from `maxTierIdOf` down to 1. These are view functions but called by governance contracts.
|
|
36
|
+
- **Theoretical max is not the supported operating envelope.** The store permits up to 65,535 tiers, but the practical
|
|
37
|
+
comfort zone is far lower. The test suite demonstrates survivability at 100 to 200 tiers and also demonstrates that
|
|
38
|
+
`balanceOf` and `totalCashOutWeight` become materially more expensive at 100 tiers than at 10 tiers. Treat large
|
|
39
|
+
catalogs as an explicit gas-budgeting exercise, not as a default deployment shape.
|
|
36
40
|
- **`tiersOf` traverses removed tiers.** Removed tiers are skipped via bitmap but still traversed in the linked list. `cleanTiers()` must be called separately to compact. `cleanTiers()` is permissionless and idempotent.
|
|
37
41
|
- **Minting from many tiers in one payment.** `recordMint` loops per tier ID: storage read (stored tier + bitmap check) per iteration. 50 tiers in one payment ~5-7M gas (tested, fits in 30M block). 100+ tiers in a single mint is feasible but consumes most of the block.
|
|
38
42
|
- **`recordAddTiers` sort-insertion cost.** Adding a low-category tier to a hook with many existing higher-category tiers iterates the entire sorted list to find the insertion point. O(n) per added tier.
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Every user-facing operation in the tiered 721 hook system, with exact entry points, parameters, state changes, events, and edge cases.
|
|
4
4
|
|
|
5
|
+
These journeys describe functional behavior, not a promise that the theoretical `uint16.max` tier ceiling is a
|
|
6
|
+
production-ready catalog size. Several important reads and cash-out calculations scale with `maxTierId`, so large-tier
|
|
7
|
+
deployments should be evaluated against the repo's documented operating envelope before launch.
|
|
8
|
+
|
|
5
9
|
---
|
|
6
10
|
|
|
7
11
|
## 1. Pay and Receive NFTs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/721-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@bananapus/address-registry-v6": "^0.0.10",
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
21
|
+
"@bananapus/core-v6": "^0.0.23",
|
|
22
22
|
"@bananapus/ownable-v6": "^0.0.10",
|
|
23
23
|
"@bananapus/permission-ids-v6": "^0.0.10",
|
|
24
24
|
"@openzeppelin/contracts": "^5.6.1",
|
package/src/JB721TiersHook.sol
CHANGED
|
@@ -210,7 +210,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
210
210
|
issueTokensForSplits: STORE.flagsOf(address(this)).issueTokensForSplits
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
-
hookSpecifications[0] =
|
|
213
|
+
hookSpecifications[0] =
|
|
214
|
+
JBPayHookSpecification({hook: this, noop: false, amount: totalSplitAmount, metadata: splitMetadata});
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
/// @notice The combined cash out weight of the NFTs with the specified token IDs.
|
|
@@ -620,104 +621,105 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
620
621
|
}
|
|
621
622
|
}
|
|
622
623
|
|
|
623
|
-
/// @notice
|
|
624
|
-
/// @
|
|
625
|
-
///
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
});
|
|
641
|
-
if (!valid) return;
|
|
624
|
+
/// @notice Mint NFTs from the specified tiers and update the beneficiary's pay credits.
|
|
625
|
+
/// @param value The normalized payment value.
|
|
626
|
+
/// @param context Payment context provided by the terminal.
|
|
627
|
+
function _mintAndUpdateCredits(uint256 value, JBAfterPayRecordedContext calldata context) internal {
|
|
628
|
+
// Keep a reference to the number of NFT credits the beneficiary already has.
|
|
629
|
+
uint256 payCredits = payCreditsOf[context.beneficiary];
|
|
630
|
+
|
|
631
|
+
// Set the leftover amount as the initial value.
|
|
632
|
+
uint256 leftoverAmount = value;
|
|
633
|
+
|
|
634
|
+
// If the payer is the beneficiary, combine their NFT credits with the amount paid.
|
|
635
|
+
uint256 unusedPayCredits;
|
|
636
|
+
if (context.payer == context.beneficiary) {
|
|
637
|
+
leftoverAmount += payCredits;
|
|
638
|
+
} else {
|
|
639
|
+
// Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
|
|
640
|
+
unusedPayCredits = payCredits;
|
|
642
641
|
}
|
|
643
642
|
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
uint256 payCredits = payCreditsOf[context.beneficiary];
|
|
643
|
+
// Keep a reference to the boolean indicating whether paying more than the price of the NFTs being minted
|
|
644
|
+
// is allowed. Defaults to the collection's flag.
|
|
645
|
+
bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending;
|
|
648
646
|
|
|
649
|
-
|
|
650
|
-
|
|
647
|
+
// Resolve the metadata.
|
|
648
|
+
(bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
649
|
+
id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}), metadata: context.payerMetadata
|
|
650
|
+
});
|
|
651
651
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
leftoverAmount += payCredits;
|
|
656
|
-
} else {
|
|
657
|
-
// Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
|
|
658
|
-
unusedPayCredits = payCredits;
|
|
659
|
-
}
|
|
652
|
+
if (found) {
|
|
653
|
+
// Keep a reference to the IDs of the tier be to minted.
|
|
654
|
+
uint16[] memory tierIdsToMint;
|
|
660
655
|
|
|
661
|
-
// Keep a reference to the
|
|
662
|
-
|
|
663
|
-
bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending;
|
|
656
|
+
// Keep a reference to the payer's flag indicating whether overspending is allowed.
|
|
657
|
+
bool payerAllowsOverspending;
|
|
664
658
|
|
|
665
|
-
//
|
|
666
|
-
(
|
|
667
|
-
id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}),
|
|
668
|
-
metadata: context.payerMetadata
|
|
669
|
-
});
|
|
659
|
+
// Decode the metadata.
|
|
660
|
+
(payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
|
|
670
661
|
|
|
671
|
-
if
|
|
672
|
-
|
|
673
|
-
|
|
662
|
+
// Make sure overspending is allowed if requested.
|
|
663
|
+
if (allowOverspending && !payerAllowsOverspending) {
|
|
664
|
+
allowOverspending = false;
|
|
665
|
+
}
|
|
674
666
|
|
|
675
|
-
|
|
676
|
-
|
|
667
|
+
// Mint NFTs from the tiers as specified.
|
|
668
|
+
if (tierIdsToMint.length != 0) {
|
|
669
|
+
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
670
|
+
leftoverAmount =
|
|
671
|
+
_mintAll({amount: leftoverAmount, mintTierIds: tierIdsToMint, beneficiary: context.beneficiary});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
677
674
|
|
|
678
|
-
|
|
679
|
-
|
|
675
|
+
// If overspending isn't allowed, revert.
|
|
676
|
+
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
680
677
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
allowOverspending = false;
|
|
684
|
-
}
|
|
678
|
+
// Update NFT credits if they changed.
|
|
679
|
+
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
685
680
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
681
|
+
if (newPayCredits != payCredits) {
|
|
682
|
+
if (newPayCredits > payCredits) {
|
|
683
|
+
emit AddPayCredits({
|
|
684
|
+
amount: newPayCredits - payCredits,
|
|
685
|
+
newTotalCredits: newPayCredits,
|
|
686
|
+
account: context.beneficiary,
|
|
687
|
+
caller: _msgSender()
|
|
688
|
+
});
|
|
689
|
+
} else {
|
|
690
|
+
emit UsePayCredits({
|
|
691
|
+
amount: payCredits - newPayCredits,
|
|
692
|
+
newTotalCredits: newPayCredits,
|
|
693
|
+
account: context.beneficiary,
|
|
694
|
+
caller: _msgSender()
|
|
695
|
+
});
|
|
693
696
|
}
|
|
694
697
|
|
|
695
|
-
|
|
696
|
-
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
697
|
-
|
|
698
|
-
// Update NFT credits if they changed.
|
|
699
|
-
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
700
|
-
|
|
701
|
-
if (newPayCredits != payCredits) {
|
|
702
|
-
if (newPayCredits > payCredits) {
|
|
703
|
-
emit AddPayCredits({
|
|
704
|
-
amount: newPayCredits - payCredits,
|
|
705
|
-
newTotalCredits: newPayCredits,
|
|
706
|
-
account: context.beneficiary,
|
|
707
|
-
caller: _msgSender()
|
|
708
|
-
});
|
|
709
|
-
} else {
|
|
710
|
-
emit UsePayCredits({
|
|
711
|
-
amount: payCredits - newPayCredits,
|
|
712
|
-
newTotalCredits: newPayCredits,
|
|
713
|
-
account: context.beneficiary,
|
|
714
|
-
caller: _msgSender()
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
719
|
-
}
|
|
698
|
+
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
720
699
|
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/// @notice Process a payment, minting NFTs and updating credits as necessary.
|
|
703
|
+
/// @dev Pay credits are tracked per beneficiary, not per payer. When the payer differs from the beneficiary,
|
|
704
|
+
/// the payer's existing credits are NOT applied to the mint. Only the beneficiary's credits are combined with
|
|
705
|
+
/// the incoming payment value. Leftover funds after minting are stored as credits for the beneficiary.
|
|
706
|
+
/// @param context Payment context provided by the terminal after it has recorded the payment in the terminal store.
|
|
707
|
+
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual override {
|
|
708
|
+
// Normalize the payment value based on the pricing context.
|
|
709
|
+
bool valid;
|
|
710
|
+
uint256 value;
|
|
711
|
+
(value, valid) = JB721TiersHookLib.normalizePaymentValue({
|
|
712
|
+
packedPricingContext: _packedPricingContext,
|
|
713
|
+
prices: PRICES,
|
|
714
|
+
projectId: PROJECT_ID,
|
|
715
|
+
amountValue: context.amount.value,
|
|
716
|
+
amountCurrency: context.amount.currency,
|
|
717
|
+
amountDecimals: context.amount.decimals
|
|
718
|
+
});
|
|
719
|
+
if (!valid) return;
|
|
720
|
+
|
|
721
|
+
// Mint NFTs from the specified tiers and update the beneficiary's pay credits.
|
|
722
|
+
_mintAndUpdateCredits({value: value, context: context});
|
|
721
723
|
|
|
722
724
|
// Distribute any forwarded funds to tier split groups.
|
|
723
725
|
if (context.hookMetadata.length != 0 && context.forwardedAmount.value != 0) {
|
|
@@ -102,7 +102,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
102
102
|
|
|
103
103
|
// Use this contract as the only cash out hook.
|
|
104
104
|
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
105
|
-
hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
105
|
+
hookSpecifications[0] = JBCashOutHookSpecification({hook: this, noop: false, amount: 0, metadata: bytes("")});
|
|
106
106
|
|
|
107
107
|
uint256[] memory decodedTokenIds;
|
|
108
108
|
|
|
@@ -136,7 +136,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
|
|
|
136
136
|
// Forward the received weight and use this contract as the only pay hook.
|
|
137
137
|
weight = context.weight;
|
|
138
138
|
hookSpecifications = new JBPayHookSpecification[](1);
|
|
139
|
-
hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
139
|
+
hookSpecifications[0] = JBPayHookSpecification({hook: this, noop: false, amount: 0, metadata: bytes("")});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
|
|
@@ -175,14 +175,10 @@ library JB721TiersHookLib {
|
|
|
175
175
|
view
|
|
176
176
|
returns (uint256 totalSplitAmount, bytes memory hookMetadata)
|
|
177
177
|
{
|
|
178
|
-
bytes memory data
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
id: JBMetadataResolver.getId({purpose: "pay", target: metadataIdTarget}), metadata: metadata
|
|
183
|
-
});
|
|
184
|
-
if (!found) return (0, bytes(""));
|
|
185
|
-
}
|
|
178
|
+
(bool found, bytes memory data) = JBMetadataResolver.getDataFor({
|
|
179
|
+
id: JBMetadataResolver.getId({purpose: "pay", target: metadataIdTarget}), metadata: metadata
|
|
180
|
+
});
|
|
181
|
+
if (!found) return (0, bytes(""));
|
|
186
182
|
|
|
187
183
|
(, uint16[] memory tierIdsToMint) = abi.decode(data, (bool, uint16[]));
|
|
188
184
|
if (tierIdsToMint.length == 0) return (0, bytes(""));
|