@bananapus/721-hook-v6 0.0.18 → 0.0.20
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 +39 -29
- package/ARCHITECTURE.md +52 -5
- package/AUDIT_INSTRUCTIONS.md +85 -12
- package/CHANGE_LOG.md +15 -1
- package/README.md +209 -198
- package/RISKS.md +22 -1
- package/SKILLS.md +107 -37
- package/STYLE_GUIDE.md +1 -1
- package/USER_JOURNEYS.md +48 -19
- package/package.json +5 -5
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +1 -1
- package/src/JB721TiersHook.sol +88 -86
- package/src/JB721TiersHookDeployer.sol +1 -1
- package/src/JB721TiersHookProjectDeployer.sol +1 -1
- package/src/JB721TiersHookStore.sol +12 -1
- package/src/abstract/ERC721.sol +1 -1
- package/src/abstract/JB721Hook.sol +5 -5
- package/src/libraries/JB721TiersHookLib.sol +21 -11
- package/test/721HookAttacks.t.sol +1 -1
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +1 -1
- package/test/Fork.t.sol +1 -1
- package/test/TestAuditGaps.sol +148 -1
- package/test/TestSafeTransferReentrancy.t.sol +1 -1
- package/test/TestVotingUnitsLifecycle.t.sol +1 -1
- package/test/audit/AuditRegressions.t.sol +82 -0
- package/test/audit/CodexNemesis_CrossCurrencySplitNoPrices.t.sol +121 -0
- package/test/audit/USDTVoidReturnCompat.t.sol +301 -0
- package/test/fork/ERC20CashOutFork.t.sol +612 -0
- package/test/fork/ERC20TierSplitFork.t.sol +1 -1
- package/test/fork/IssueTokensForSplitsFork.t.sol +504 -0
- package/test/invariants/TierLifecycleInvariant.t.sol +1 -1
- package/test/invariants/TieredHookStoreInvariant.t.sol +1 -1
- package/test/invariants/handlers/TierLifecycleHandler.sol +1 -1
- package/test/invariants/handlers/TierStoreHandler.sol +1 -1
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +1 -1
- package/test/regression/CacheTierLookup.t.sol +1 -1
- package/test/regression/ProjectDeployerRulesets.t.sol +1 -1
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +1 -1
- package/test/regression/SplitDistributionBugs.t.sol +1 -1
- package/test/regression/SplitNoBeneficiary.t.sol +1 -1
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +1 -1
- package/test/unit/JBBitmap.t.sol +1 -1
- package/test/unit/JBIpfsDecoder.t.sol +1 -1
- package/test/unit/TierSupplyReserveCheck.t.sol +1 -1
- package/test/unit/adjustTier_Unit.t.sol +1 -1
- package/test/unit/deployer_Unit.t.sol +1 -1
- package/test/unit/getters_constructor_Unit.t.sol +4 -1
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +1 -1
- package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -1
- package/test/unit/pay_Unit.t.sol +1 -1
- package/test/unit/redeem_Unit.t.sol +1 -1
- package/test/unit/splitHookDistribution_Unit.t.sol +1 -1
- package/test/unit/tierSplitRouting_Unit.t.sol +1 -1
package/README.md
CHANGED
|
@@ -1,257 +1,268 @@
|
|
|
1
1
|
# Juicebox 721 Hook
|
|
2
2
|
|
|
3
|
-
`nana-721-hook`
|
|
4
|
-
|
|
5
|
-
1. A pay hook for Juicebox projects to sell tiered NFTs (ERC-721s) with different prices and artwork.
|
|
6
|
-
2. (Optionally) a cash out hook which allows holders to burn their NFTs to reclaim funds from the project, in proportion to the NFT's price.
|
|
7
|
-
|
|
8
|
-
<details>
|
|
9
|
-
<summary>Table of Contents</summary>
|
|
10
|
-
<ol>
|
|
11
|
-
<li><a href="#usage">Usage</a></li>
|
|
12
|
-
<ul>
|
|
13
|
-
<li><a href="#install">Install</a></li>
|
|
14
|
-
<li><a href="#develop">Develop</a></li>
|
|
15
|
-
<li><a href="#scripts">Scripts</a></li>
|
|
16
|
-
<li><a href="#deployments">Deployments</a></li>
|
|
17
|
-
<ul>
|
|
18
|
-
<li><a href="#with-sphinx">With Sphinx</a></li>
|
|
19
|
-
<li><a href="#without-sphinx">Without Sphinx</a></li>
|
|
20
|
-
</ul>
|
|
21
|
-
<li><a href="#tips">Tips</a></li>
|
|
22
|
-
</ul>
|
|
23
|
-
<li><a href="#repository-layout">Repository Layout</a></li>
|
|
24
|
-
<li><a href="#architecture">Architecture</a></li>
|
|
25
|
-
<ul>
|
|
26
|
-
<li><a href="#contracts">Contracts</a></li>
|
|
27
|
-
</ul>
|
|
28
|
-
<li><a href="#description">Description</a></li>
|
|
29
|
-
<ul>
|
|
30
|
-
<li><a href="#hooks">Hooks</a></li>
|
|
31
|
-
<li><a href="#mechanism">Mechanism</a></li>
|
|
32
|
-
<li><a href="#setup">Setup</a></li>
|
|
33
|
-
</ul>
|
|
34
|
-
</ul>
|
|
35
|
-
</ol>
|
|
36
|
-
</details>
|
|
37
|
-
|
|
38
|
-
_If you're having trouble understanding this contract, take a look at the [core protocol contracts](https://github.com/Bananapus/nana-core) and the [documentation](https://docs.juicebox.money/) first. If you have questions, reach out on [Discord](https://discord.com/invite/ErQYmth4dS)._
|
|
39
|
-
|
|
40
|
-
## Usage
|
|
41
|
-
|
|
42
|
-
### Install
|
|
43
|
-
|
|
44
|
-
How to install `nana-721-hook` in another project.
|
|
3
|
+
Juicebox projects accept payments through terminals, but by default those payments only mint fungible project tokens. `nana-721-hook-v6` extends that flow with a tiered NFT system: project owners define tiers -- each with a price, supply cap, artwork, and optional per-tier features like reserve minting, voting power, discount schedules, and split-based fund routing -- and when someone pays the project, the hook mints the corresponding NFTs. Optionally, holders can burn their NFTs to reclaim funds from the project in proportion to each NFT's price relative to all outstanding NFTs.
|
|
45
4
|
|
|
46
|
-
|
|
5
|
+
_If you're having trouble understanding this contract, take a look at the [core protocol contracts](https://github.com/Bananapus/nana-core-v6) and the [documentation](https://docs.juicebox.money/) first. If you have questions, reach out on [Discord](https://discord.com/invite/ErQYmth4dS)._
|
|
47
6
|
|
|
48
|
-
|
|
49
|
-
npm install @bananapus/721-hook
|
|
50
|
-
```
|
|
7
|
+
## Conceptual Overview
|
|
51
8
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
forge install Bananapus/nana-721-hook
|
|
56
|
-
```
|
|
9
|
+
### How it works
|
|
57
10
|
|
|
58
|
-
|
|
11
|
+
A `JB721TiersHook` is simultaneously a **data hook**, a **pay hook**, and a **cash out hook**. When a project using this hook is paid through a Juicebox terminal, the terminal asks the data hook what should happen. The data hook inspects the payment amount and any tier IDs the payer specified in the payment metadata, then returns instructions for the pay hook to mint the appropriate NFTs.
|
|
59
12
|
|
|
60
|
-
|
|
13
|
+
1. **Payment arrives** at the terminal. The terminal calls the data hook with details about the payment.
|
|
14
|
+
2. **Tier selection.** The payer can specify which tier IDs to mint in the payment metadata. If no tiers are specified and the hook allows credits, the payment amount is stored as NFT credits for future purchases.
|
|
15
|
+
3. **Price check.** Each requested tier's price (after any discount) is checked against the payment amount. If the tier and payment use different currencies, the hook's immutable `PRICES` contract converts between them. If `PRICES` is the zero address and currencies differ, the payment is silently ignored (no mint, no revert).
|
|
16
|
+
4. **Minting.** The pay hook mints one NFT per requested tier to the payment beneficiary, decrementing each tier's remaining supply.
|
|
17
|
+
5. **Reserve minting.** If a tier has a `reserveFrequency` of N, one extra NFT is minted to the tier's `reserveBeneficiary` for every N NFTs purchased. Reserve NFTs are minted lazily via `mintPendingReservesFor`.
|
|
18
|
+
6. **Credits.** Any leftover payment amount (the portion not spent on tier prices) is stored as NFT credits on the payer's address, redeemable toward future NFT purchases. Credits are only combined with the payment when `payer == beneficiary`. The hook can reject leftover funds entirely via the `preventOverspending` flag.
|
|
19
|
+
7. **Cash outs.** If the project owner enables `useDataHookForCashOut` in the ruleset metadata, NFT holders can burn their NFTs to reclaim funds. The reclaim amount is proportional to the NFT's original tier price (not the discounted price) relative to the total price of all outstanding NFTs (including pending reserves in the denominator). When NFT cash outs are enabled, fungible token cash outs are disabled -- attempting to cash out fungible tokens when the data hook is active will revert.
|
|
61
20
|
|
|
62
|
-
|
|
21
|
+
### Tier properties
|
|
63
22
|
|
|
64
|
-
|
|
65
|
-
curl -L https://foundry.paradigm.xyz | sh
|
|
66
|
-
```
|
|
23
|
+
Each tier is configured with a `JB721TierConfig` and stored compactly as a `JBStored721Tier`. Key properties:
|
|
67
24
|
|
|
68
|
-
|
|
25
|
+
- **Price** (`uint104`). The cost to mint one NFT from this tier, denominated in the currency specified in the hook's `JB721InitTiersConfig`.
|
|
26
|
+
- **Initial supply** (`uint32`). The maximum number of NFTs that can ever be minted from this tier (up to 999,999,999).
|
|
27
|
+
- **Category** (`uint24`). Groups tiers for organizational and access purposes. Tiers must be sorted by category in ascending order when added -- the store reverts with `JB721TiersHookStore_InvalidCategorySortOrder` otherwise.
|
|
28
|
+
- **Discount percent** (`uint8`). Reduces the effective purchase price. The denominator is 200, so a `discountPercent` of 100 means 50% off and 200 means free. Can be changed later via `setDiscountPercentOf`. Tiers configured with `cannotIncreaseDiscountPercent` only allow discounts to decrease. Cash out weight always uses the original tier price, not the discounted price.
|
|
29
|
+
- **Reserve frequency** (`uint16`). With a value of N, one extra NFT is minted to the `reserveBeneficiary` for every N NFTs purchased. Tiers with `allowOwnerMint` enabled cannot have reserves.
|
|
30
|
+
- **Voting units** (`uint104`). By default, each NFT's voting power equals its tier price. When `useVotingUnits` is true, a custom `votingUnits` value is used instead. Voting power is computed per-address across all tiers.
|
|
31
|
+
- **Split percent** (`uint32`). Routes a percentage of the tier's mint price to configured split recipients each time an NFT from the tier is purchased, out of `JBConstants.SPLITS_TOTAL_PERCENT` (1,000,000,000). The remaining funds stay in the project's balance. Split recipients follow the same priority as `JBMultiTerminal`: `split.hook` (receives funds via `IJBSplitHook.processSplitWith`) > `split.projectId` (routed via the project's primary terminal) > `split.beneficiary` (direct transfer).
|
|
32
|
+
- **Weight adjustment.** When splits are active, the hook adjusts the returned weight so the terminal only mints fungible project tokens proportional to the amount that actually enters the project treasury. For example, a 50% `splitPercent` on a 1 ETH payment results in half the normal token issuance. This weight adjustment can be disabled with the `issueTokensForSplits` flag, which gives payers full token credit regardless of where the funds go.
|
|
33
|
+
- **Flags.** `allowOwnerMint` (project owner can mint on-demand), `transfersPausable` (transfers can be paused per-ruleset), `cannotBeRemoved` (tier is permanent once added), `cannotIncreaseDiscountPercent` (discount can only decrease).
|
|
34
|
+
- **Token URI.** Each tier has an `encodedIPFSUri` for artwork/metadata, which can be overridden by an `IJB721TokenUriResolver`. The resolver can return unique values for each NFT within a tier.
|
|
69
35
|
|
|
70
|
-
|
|
71
|
-
npm ci && forge install
|
|
72
|
-
```
|
|
36
|
+
### Per-ruleset options
|
|
73
37
|
|
|
74
|
-
|
|
38
|
+
The hook packs two boolean flags into the ruleset's `metadata` field via `JB721TiersRulesetMetadata`:
|
|
75
39
|
|
|
76
|
-
|
|
40
|
+
- `pauseTransfers` -- prevents all NFT transfers during this ruleset (only affects tiers with `transfersPausable` set).
|
|
41
|
+
- `pauseMintPendingReserves` -- prevents minting pending reserve NFTs during this ruleset.
|
|
77
42
|
|
|
78
|
-
|
|
79
|
-
| --------------------- | --------------------------------------------------- |
|
|
80
|
-
| `forge build` | Compile the contracts and write artifacts to `out`. |
|
|
81
|
-
| `forge fmt` | Lint. |
|
|
82
|
-
| `forge test` | Run the tests. |
|
|
83
|
-
| `forge build --sizes` | Get contract sizes. |
|
|
84
|
-
| `forge coverage` | Generate a test coverage report. |
|
|
85
|
-
| `foundryup` | Update foundry. Run this periodically. |
|
|
86
|
-
| `forge clean` | Remove the build artifacts and cache directories. |
|
|
43
|
+
### Operating envelope
|
|
87
44
|
|
|
88
|
-
|
|
45
|
+
The contract supports up to 65,535 tiers (`uint16.max`), but the practical operating envelope is much smaller because several important read paths iterate the tier set:
|
|
89
46
|
|
|
90
|
-
|
|
47
|
+
- **Up to ~100 active tiers**: comfortable for projects that expect frequent `balanceOf` reads, governance queries, or NFT cash outs.
|
|
48
|
+
- **100--200 tiers**: an advanced configuration requiring deliberate gas budgeting and frontend/operator awareness.
|
|
49
|
+
- **Above 200 tiers**: on-chain reads and cash-out accounting remain functionally correct, but gas costs scale linearly with `maxTierId`.
|
|
91
50
|
|
|
92
|
-
|
|
51
|
+
The test suite proves survivability at 100 and 200 tiers, and demonstrates that `balanceOf` and `totalCashOutWeight` become materially more expensive at 100 tiers than at 10. Treat this evidence as a scope boundary, not encouragement to target the theoretical ceiling.
|
|
93
52
|
|
|
94
|
-
|
|
95
|
-
| ------------------- | ------------------------------------------------------- |
|
|
96
|
-
| `npm test` | Run local tests. |
|
|
97
|
-
| `npm run coverage` | Generate an LCOV test coverage report. |
|
|
98
|
-
| `npm run artifacts` | Fetch Sphinx artifacts and write them to `deployments/` |
|
|
53
|
+
## Architecture
|
|
99
54
|
|
|
100
|
-
|
|
55
|
+
```mermaid
|
|
56
|
+
graph TD;
|
|
57
|
+
A[JB721TiersHookProjectDeployer] -->|launches project + queues rulesets| B[Juicebox Project]
|
|
58
|
+
D[JB721TiersHookDeployer] -->|deploys hook for existing project| C[JB721TiersHook]
|
|
59
|
+
A -->|deploys via| D
|
|
60
|
+
B -->|calls on pay / cash out| C
|
|
61
|
+
C -->|reads and writes tier data| E[JB721TiersHookStore]
|
|
62
|
+
B -->|uses| F[JBMultiTerminal]
|
|
63
|
+
C -->|mints NFTs on payment through| F
|
|
64
|
+
C -->|burns NFTs to reclaim funds through| F
|
|
65
|
+
```
|
|
101
66
|
|
|
102
|
-
|
|
67
|
+
| Contract | Lines | Description |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `JB721TiersHook` | 794 | The core tiered NFT hook. Implements pay hook (mint NFTs), cash out hook (burn NFTs for reclaim), and data hook (compute weight adjustments and split routing). Manages credits, tier adjustments, reserve minting, discount changes, and metadata. Inherits `JB721Hook` and `JBOwnable`. |
|
|
70
|
+
| `JB721TiersHookStore` | 1,240 | Shared data store for all `JB721TiersHook` instances. Manages tier storage (packed `JBStored721Tier`), supply tracking, bitmap-based tier removal, token ID encoding/decoding, and enforces tier sort order, supply limits, and flag constraints. |
|
|
71
|
+
| `JB721TiersHookProjectDeployer` | 418 | Convenience deployer that creates a new Juicebox project with a 721 tiers hook already configured. Exposes `launchProjectFor` and `queueRulesetsOf` for ongoing ruleset management. |
|
|
72
|
+
| `JB721TiersHookDeployer` | 115 | Deploys a `JB721TiersHook` clone for an existing project via `deployHookFor`. Uses Solady `LibClone` for minimal proxy deployment and registers the clone in `IJBAddressRegistry`. |
|
|
73
|
+
| `JB721Hook` (abstract) | 268 | Abstract base for 721 hooks. Handles pay/cash out lifecycle dispatch, terminal validation, metadata ID resolution, and ERC-2981/ERC-165 interface declaration. |
|
|
74
|
+
| `ERC721` (abstract) | 506 | Clone-compatible ERC-721 implementation with mutable `name` and `symbol` (set during `initialize` rather than constructor, since hooks are deployed as clones). |
|
|
75
|
+
| `JB721TiersHookLib` | 634 | Library for tier adjustment logic, split distribution, price normalization across currencies, and token URI resolution. |
|
|
76
|
+
| `JB721TiersRulesetMetadataResolver` | 47 | Packs and unpacks `JB721TiersRulesetMetadata` (transfer pause + reserve mint pause) into the 14-bit metadata field of `JBRulesetMetadata`. |
|
|
77
|
+
| `JBBitmap` | 58 | Word-based bitmap for tracking removed tiers. Each word covers 256 tier IDs. |
|
|
78
|
+
| `JBIpfsDecoder` | 98 | Decodes `bytes32`-encoded IPFS CIDv0 hashes into `Qm...` URI strings. |
|
|
79
|
+
| `JB721Constants` | 7 | Defines `DISCOUNT_DENOMINATOR = 200`. |
|
|
80
|
+
|
|
81
|
+
### Supporting Types
|
|
82
|
+
|
|
83
|
+
Key structs used to configure and interact with the hook:
|
|
84
|
+
|
|
85
|
+
| Struct | Purpose |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `JB721TierConfig` | Full configuration for a single tier: price, supply, voting units, reserve frequency, discount, category, splits, and boolean flags. |
|
|
88
|
+
| `JB721Tier` | Read-only view of a tier's current state, including remaining supply and resolved URI. |
|
|
89
|
+
| `JBStored721Tier` | Packed on-chain storage layout for a tier (price, supply, category, discount, reserve frequency, split percent, and packed boolean flags). |
|
|
90
|
+
| `JB721InitTiersConfig` | Wraps an array of `JB721TierConfig` with the currency and decimal precision for tier prices. |
|
|
91
|
+
| `JBDeploy721TiersHookConfig` | Full deployment config: collection name/symbol, base URI, token URI resolver, contract URI, tiers config, default reserve beneficiary, and flags. |
|
|
92
|
+
| `JB721TiersHookFlags` | Hook-wide boolean flags: `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting`, `preventOverspending`, `issueTokensForSplits`. |
|
|
93
|
+
| `JB721TiersRulesetMetadata` | Per-ruleset options packed into `JBRulesetMetadata.metadata`: `pauseTransfers`, `pauseMintPendingReserves`. |
|
|
94
|
+
| `JB721TiersMintReservesConfig` | Specifies which tier to mint reserves for and how many to mint. |
|
|
95
|
+
| `JB721TiersSetDiscountPercentConfig` | Specifies a tier ID and the new discount percent to set. |
|
|
96
|
+
|
|
97
|
+
## Install
|
|
103
98
|
|
|
104
|
-
`
|
|
99
|
+
For projects using `npm` to manage dependencies (recommended):
|
|
105
100
|
|
|
106
101
|
```bash
|
|
107
|
-
|
|
102
|
+
npm install @bananapus/721-hook-v6
|
|
108
103
|
```
|
|
109
104
|
|
|
110
|
-
|
|
105
|
+
For projects using `forge` to manage dependencies (not recommended):
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
| `npm run deploy:testnets` | Propose testnet deployments. |
|
|
107
|
+
```bash
|
|
108
|
+
forge install Bananapus/nana-721-hook-v6
|
|
109
|
+
```
|
|
116
110
|
|
|
117
|
-
|
|
111
|
+
If you're using `forge` to manage dependencies, add `libs = ["node_modules", "lib"]` to `foundry.toml` so Foundry resolves imports from both locations. No `remappings.txt` needed.
|
|
118
112
|
|
|
119
|
-
|
|
113
|
+
## Develop
|
|
120
114
|
|
|
121
|
-
|
|
115
|
+
`nana-721-hook-v6` uses [npm](https://www.npmjs.com/) (version >=20.0.0) for package management and the [Foundry](https://github.com/foundry-rs/foundry) development toolchain for builds, tests, and deployments. To get set up, [install Node.js](https://nodejs.org/en/download) and install [Foundry](https://github.com/foundry-rs/foundry):
|
|
122
116
|
|
|
123
117
|
```bash
|
|
124
|
-
|
|
118
|
+
curl -L https://foundry.paradigm.xyz | sh
|
|
125
119
|
```
|
|
126
120
|
|
|
127
|
-
You can
|
|
121
|
+
You can download and install dependencies with:
|
|
128
122
|
|
|
129
123
|
```bash
|
|
130
|
-
|
|
124
|
+
npm ci && forge install
|
|
131
125
|
```
|
|
132
126
|
|
|
133
|
-
|
|
127
|
+
If you run into trouble with `forge install`, try using `git submodule update --init --recursive` to ensure that nested submodules have been properly initialized.
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
Some useful commands:
|
|
136
130
|
|
|
137
|
-
|
|
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. |
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
To learn more, visit the [Foundry Book](https://book.getfoundry.sh/) docs.
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
forge remappings >> remappings.txt
|
|
143
|
-
```
|
|
143
|
+
### Configuration
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
Key settings from `foundry.toml`:
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
| Setting | Value |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| Solidity compiler | ^0.8.26 |
|
|
150
|
+
| EVM target | cancun |
|
|
151
|
+
| Optimizer runs | 200 |
|
|
152
|
+
| Fuzz runs | 4,096 |
|
|
153
|
+
| Invariant runs | 1,024 |
|
|
154
|
+
| Invariant depth | 100 |
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
## Repository Layout
|
|
150
157
|
|
|
151
158
|
```
|
|
152
|
-
nana-721-hook/
|
|
159
|
+
nana-721-hook-v6/
|
|
153
160
|
├── script/
|
|
154
|
-
│ ├── Deploy.s.sol - Deploys
|
|
155
|
-
│ ├── LaunchProjectFor.s.sol - (DEPRECATED) Deploys a project with a 721 tiers hook.
|
|
161
|
+
│ ├── Deploy.s.sol - Deploys the hook store, hook deployer, and project deployer.
|
|
156
162
|
│ └── helpers/
|
|
157
163
|
│ └── Hook721DeploymentLib.sol - Internal helpers for deployment scripts.
|
|
158
|
-
├── src/
|
|
159
|
-
│ ├── JB721TiersHook.sol - The core tiered NFT pay/cash out hook.
|
|
160
|
-
│ ├── JB721TiersHookDeployer.sol - Deploys
|
|
161
|
-
│ ├── JB721TiersHookProjectDeployer.sol - Deploys a project with a
|
|
162
|
-
│ ├── JB721TiersHookStore.sol -
|
|
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).
|
|
163
169
|
│ ├── abstract/
|
|
164
|
-
│ │ ├── JB721Hook.sol - Abstract base
|
|
165
|
-
│ │ └── ERC721.sol - Clone-compatible
|
|
166
|
-
│ ├── interfaces/
|
|
167
|
-
│ ├──
|
|
168
|
-
│
|
|
169
|
-
|
|
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/
|
|
170
202
|
├── E2E/
|
|
171
|
-
│ └── Pay_Mint_Redeem_E2E.t.sol - End-to-end
|
|
172
|
-
├── unit/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
├── .
|
|
181
|
-
│
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
198
239
|
```
|
|
199
240
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
| Contract | Description |
|
|
203
|
-
| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
204
|
-
| [`JB721Hook.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/abstract/JB721Hook.sol) | Abstract base for 721 hooks: handles pay/cash out lifecycle, terminal validation, and metadata resolution. |
|
|
205
|
-
| [`JB721TiersHook.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHook.sol) | The core tiered NFT pay/cash out hook implementation, extending `JB721Hook`. |
|
|
206
|
-
| [`JB721TiersHookDeployer.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHookDeployer.sol) | Exposes a `deployHookFor(…)` function which allows deploys an NFT hook for a project. |
|
|
207
|
-
| [`JB721TiersHookProjectDeployer.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHookProjectDeployer.sol) | Exposes a `launchProjectFor(…)` function which deploys a project with a tiered NFT hook already set up. |
|
|
208
|
-
| [`JB721TiersHookStore.sol`](https://github.com/Bananapus/nana-721-hook/blob/main/src/JB721TiersHookStore.sol) | Stores and manages data for tiered NFT hooks. |
|
|
209
|
-
|
|
210
|
-
## Description
|
|
211
|
-
|
|
212
|
-
### Hooks
|
|
213
|
-
|
|
214
|
-
This contract is a _data hook_, a _pay hook_, and a _cash out hook_. Data hooks receive information about a payment or a cash out, and put together a payload for the pay/cash out hook to execute.
|
|
215
|
-
|
|
216
|
-
Juicebox projects can specify a data hook in their `JBRulesetMetadata`. When someone attempts to pay or cash out from the project, the project's terminal records the payment in the terminal store, passing information about the payment/cash out to the data hook in the process. The data hook responds with a list of payloads – each payload specifies the address of a pay/cash out hook, as well as some custom data and an amount of funds to send to that pay/cash out hook.
|
|
217
|
-
|
|
218
|
-
Each pay/cash out hook can then execute custom behavior based on the custom data (and funds) they receive.
|
|
219
|
-
|
|
220
|
-
### Mechanism
|
|
221
|
-
|
|
222
|
-
A project using a 721 tiers hook can specify any number of NFT tiers (up to 65,535 total).
|
|
223
|
-
|
|
224
|
-
- 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
|
-
- 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.
|
|
226
|
-
|
|
227
|
-
Each tier has the following properties:
|
|
228
|
-
|
|
229
|
-
- A price (up to `uint104`).
|
|
230
|
-
- A supply (the maximum number of NFTs which can be minted from the tier, up to 999,999,999).
|
|
231
|
-
- A token URI (artwork and metadata), which can be overridden by a URI resolver. The URI resolver can return unique values for each NFT in the tier.
|
|
232
|
-
- A category, so tiers can be organized and accessed for different purposes.
|
|
233
|
-
- A discount percent (optional). Reduces the effective purchase price. The discount is out of 200, so a `discountPercent` of 100 means 50% off, and 200 means free. The discount can be changed later via `setDiscountPercentOf`, and tiers can be configured with `cannotIncreaseDiscountPercent` to only allow discounts to decrease. Cash out weight is always based on the original tier price, not the discounted price.
|
|
234
|
-
- A reserve frequency (optional). With a reserve frequency of 5, an extra NFT will be minted to a pre-specified beneficiary address for every 5 NFTs purchased and minted from the tier. Tiers with owner minting enabled cannot have reserves.
|
|
235
|
-
- Voting units (optional). By default, each NFT's voting power equals its tier price. If `useVotingUnits` is true, a custom `votingUnits` value is used instead.
|
|
236
|
-
- A flag to specify whether the NFTs in the tier can always be transferred, or if transfers can be paused depending on the project's ruleset.
|
|
237
|
-
- A flag to specify whether the contract's owner can mint NFTs from the tier on-demand.
|
|
238
|
-
- A split percent and a set of splits (optional). Each tier can route a percentage of its mint price to configured split recipients every time an NFT from the tier is purchased. The remaining funds stay in the project's balance. The `splitPercent` is out of `JBConstants.SPLITS_TOTAL_PERCENT` (1,000,000,000). Split recipients follow the same priority as JBMultiTerminal: `split.hook` (receives funds via `IJBSplitHook.processSplitWith` with full context including token, amount, decimals, project ID, and group ID) > `split.projectId` (routed via the project's primary terminal) > `split.beneficiary` (direct transfer). When splits are active, the hook adjusts the returned weight so the terminal only mints tokens proportional to the amount that actually enters the project treasury (e.g., a 50% split 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.
|
|
239
|
-
- A set of flags which restrict tiers added in the future (the votes/reserved frequency/on-demand minting/overspending/issueTokensForSplits flags noted above).
|
|
241
|
+
36 test files. 5,167 lines of source code across `src/`.
|
|
240
242
|
|
|
241
|
-
|
|
243
|
+
## Permissions
|
|
242
244
|
|
|
243
|
-
|
|
244
|
-
- If the payment and a tier's price are specified in different currencies, the hook's immutable `PRICES` contract is used to normalize the values. If `PRICES` is the zero address and the currencies differ, the payment is silently ignored (no mint, no revert).
|
|
245
|
-
- If some of a payment does not go towards purchasing an NFT, those extra funds will be stored as "NFT credits" which can be used for future purchases. Credits are only combined with the payment when `payer == beneficiary`. Optionally, the hook can disallow credits and reject payments with leftover funds (via `preventOverspending`).
|
|
246
|
-
- 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
|
-
- 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
|
-
- 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.
|
|
249
|
-
- The hook declares support for ERC-2981 (royalties) via `supportsInterface`, but does not implement the `royaltyInfo` function. This is intended for future extension.
|
|
245
|
+
The following `JBPermissionIds` are checked by this hook:
|
|
250
246
|
|
|
251
|
-
|
|
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. |
|
|
252
255
|
|
|
253
|
-
|
|
256
|
+
All permissions are checked against the project owner via `_requirePermissionFrom`.
|
|
254
257
|
|
|
255
|
-
|
|
258
|
+
## Risks
|
|
256
259
|
|
|
257
|
-
|
|
260
|
+
- **Gas scaling with tier count.** Several read paths (`balanceOf`, `totalCashOutWeight`, `tierOfTokenId`) iterate over all tier IDs up to `maxTierId`, including removed tiers. Projects with more than ~100 active tiers should budget gas carefully for governance reads and cash outs. The `cleanTiers` function on the store can help by compacting the tier linked list after removals.
|
|
261
|
+
- **Cash out value dilution from pending reserves.** Pending reserve NFTs are included in the `totalCashOutWeight` denominator even before they are minted. In extreme cases (high reserve frequency, many unminted reserves), this can reduce cash out value for existing holders. This is by design -- it prevents a race condition where holders cash out before reserves are minted to capture a larger share.
|
|
262
|
+
- **ERC-2981 not supported.** ERC-2981 royalty support was removed. `supportsInterface` returns `false` for `IERC2981`, and no `royaltyInfo` function exists.
|
|
263
|
+
- **No reentrancy guard.** The hook does not use OpenZeppelin's `ReentrancyGuard`. Safety relies on state ordering: tokens are minted and balances are updated before any external calls. The `safeTransferFrom` callback in ERC-721 is a potential reentrancy surface, but state is already settled by the time it fires.
|
|
264
|
+
- **Immutable price feeds.** The `PRICES` contract is set at deployment and cannot be changed. If a price feed reverts (e.g., stale Chainlink data), all cross-currency minting operations for that pair will also revert until the feed recovers. Same-currency operations are unaffected.
|
|
265
|
+
- **Clone architecture.** Each `JB721TiersHook` is deployed as a minimal proxy (clone) of a reference implementation. The reference implementation's immutable storage slots (DIRECTORY, PRICES, RULESETS, STORE, SPLITS) are shared across all clones. If the reference implementation has a bug, all clones are affected.
|
|
266
|
+
- **Silent skip on currency mismatch without PRICES.** If the hook's `PRICES` is the zero address and a payment arrives in a different currency than the tier prices, the payment is silently ignored -- no NFTs are minted and no revert occurs. The payment funds still enter the project treasury.
|
|
267
|
+
- **Discount percent denominator.** The discount denominator is 200, not 100 or 10,000. A `discountPercent` of 100 means 50% off, not 100% off. Setting it to 200 makes the tier free.
|
|
268
|
+
- **Removed tiers still occupy ID space.** Removing a tier marks it in a bitmap but does not reclaim the tier ID. Iteration still traverses removed IDs (skipping them), so removing tiers does not reduce gas costs for reads until `cleanTiers` is called.
|
package/RISKS.md
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
- **Tier configuration is partially immutable.** Once created: `price`, `initialSupply`, `reserveFrequency`, `category`, `votingUnits`, `splitPercent` are permanent. Mutable: `discountPercent` (owner-controlled, subject to `cannotIncreaseDiscountPercent`), `encodedIPFSUri` (owner-controlled).
|
|
7
7
|
- **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
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.
|
|
9
|
-
- **Clone initialization is one-shot, atomic.** `initialize()` guards via `PROJECT_ID != 0`. Deployer contracts call deploy+initialize in a single transaction, preventing front-running. Ownership transfers to `_msgSender()` at the end of `initialize`.
|
|
9
|
+
- **Clone initialization is one-shot, atomic.** `initialize()` guards via `PROJECT_ID != 0`. Since `PROJECT_ID` is a storage variable (not immutable), both the implementation and fresh clones start at zero. After `initialize()` sets it, any subsequent call reverts. Deployer contracts call deploy+initialize in a single transaction, preventing front-running. Ownership transfers to `_msgSender()` at the end of `initialize`.
|
|
10
|
+
- **`balanceOf(address(0))` reverts.** Unlike the standard ERC-721 `balanceOf` which may return zero, the hook overrides `balanceOf` to revert with `JB721TiersHook_ZeroAddress` when called with `address(0)`. This guards against misleading zero-balance results for the zero address.
|
|
11
|
+
- **`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)`.
|
|
10
12
|
- **JBDirectory is trusted for terminal authentication.** `afterPayRecordedWith` and `afterCashOutRecordedWith` check `DIRECTORY.isTerminalOf()`. If the directory is compromised, arbitrary addresses can invoke pay/cashout hooks.
|
|
11
13
|
- **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.
|
|
12
14
|
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
- **Currency mismatch silently skips minting.** If payment currency differs from tier pricing currency and `PRICES == address(0)`, `_processPayment` returns without minting or reverting. Funds enter the project balance, no NFTs issued, no credits created (the normalized value is 0).
|
|
21
23
|
- **`splitPercent` reduces minting weight.** `beforePayRecordedWith` scales down the weight returned to the terminal by `(amountValue - totalSplitAmount) / amountValue`. Payers receive fewer fungible tokens for the split portion. `issueTokensForSplits` flag overrides this to give full weight.
|
|
22
24
|
- **Reserved NFT minting is permissionless.** Anyone can call `mintPendingReservesFor` to mint pending reserves to the tier's beneficiary. Only gated by the `mintPendingReservesPaused` ruleset flag. Timing of reserve minting is not owner-controlled.
|
|
25
|
+
- **Cross-reference: `PRICES == address(0)` behavior.** When `address(prices) == address(0)`, cross-currency payments skip minting silently (see Trust Assumptions section 1 and Accepted Behaviors section 8.3). This is the same address stored during `initialize()` — clones that omit the prices parameter get `address(0)` permanently.
|
|
23
26
|
|
|
24
27
|
## 3. Reentrancy Surface
|
|
25
28
|
|
|
@@ -33,6 +36,10 @@
|
|
|
33
36
|
|
|
34
37
|
- **`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
38
|
- **`balanceOf`, `votingUnitsOf`, `totalSupplyOf` iterate all tiers.** Same pattern: loop from `maxTierIdOf` down to 1. These are view functions but called by governance contracts.
|
|
39
|
+
- **Theoretical max is not the supported operating envelope.** The store permits up to 65,535 tiers, but the practical
|
|
40
|
+
comfort zone is far lower. The test suite demonstrates survivability at 100 to 200 tiers and also demonstrates that
|
|
41
|
+
`balanceOf` and `totalCashOutWeight` become materially more expensive at 100 tiers than at 10 tiers. Treat large
|
|
42
|
+
catalogs as an explicit gas-budgeting exercise, not as a default deployment shape.
|
|
36
43
|
- **`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
44
|
- **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
45
|
- **`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.
|
|
@@ -75,3 +82,17 @@
|
|
|
75
82
|
- **Cash out weight uses full price regardless of discount:** `cashOutWeightOf` for any token returns the tier's stored `price`, not the discounted purchase price.
|
|
76
83
|
- **Discount monotonicity when locked:** If `cannotIncreaseDiscountPercent` is set, `discountPercent` can only decrease or stay the same.
|
|
77
84
|
- **Flags are append-only restrictions:** `noNewTiersWithReserves`, `noNewTiersWithVotes`, `noNewTiersWithOwnerMinting` prevent future tiers from using those features but do not retroactively affect existing tiers.
|
|
85
|
+
|
|
86
|
+
## 8. Accepted Behaviors
|
|
87
|
+
|
|
88
|
+
### 8.1 Pending reserves inflate `totalCashOutWeight` denominator (by design)
|
|
89
|
+
|
|
90
|
+
`totalCashOutWeight` includes `price * pendingReserves` for unminted reserve NFTs. This dilutes per-NFT reclaim value before reserves are actually minted. This is intentional: if pending reserves were excluded, a holder could front-run `mintPendingReservesFor` to cash out at an inflated per-NFT value, then the reserve mint would reduce the remaining holders' share. Including pending reserves in the denominator ensures that the reserve allocation is priced in at all times, preventing front-running. The trade-off is that minting reserves (via the permissionless `mintPendingReservesFor`) does not change individual cash-out values — the reserves are already accounted for.
|
|
91
|
+
|
|
92
|
+
### 8.2 Cash-out weight uses full price regardless of discount (by design)
|
|
93
|
+
|
|
94
|
+
`cashOutWeightOf` returns the tier's stored `price`, not the discounted purchase price. An NFT bought at 50% discount has the same cash-out weight as one bought at full price. This is intentional: the cash-out weight represents the NFT's share of the project's treasury, not the purchase price paid. Changing cash-out weight based on discount would require per-token storage of purchase price, adding significant gas cost. The discount mechanism is designed for promotional pricing, not for creating tiered cash-out classes.
|
|
95
|
+
|
|
96
|
+
### 8.3 Currency mismatch silently skips minting (accepted degradation)
|
|
97
|
+
|
|
98
|
+
If the payment currency differs from the tier pricing currency and `PRICES == address(0)`, `_processPayment` returns without minting NFTs or creating credits. Funds enter the project balance but no NFTs are issued. This is accepted because: (1) reverting would block all payments in the mismatched currency, (2) the project owner chose not to configure price feeds (by not setting `PRICES`), and (3) the funds are not lost — they increase the project's surplus and are reclaimable via cash-out. Projects that need cross-currency NFT minting must configure `JBPrices`.
|