@bananapus/721-hook-v6 0.0.19 → 0.0.21
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 +48 -5
- package/AUDIT_INSTRUCTIONS.md +85 -12
- package/CHANGE_LOG.md +15 -1
- package/README.md +211 -210
- package/RISKS.md +18 -1
- package/SKILLS.md +107 -37
- package/STYLE_GUIDE.md +2 -2
- package/USER_JOURNEYS.md +44 -19
- package/foundry.toml +1 -1
- package/package.json +5 -5
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +1 -1
- package/src/JB721TiersHook.sol +1 -1
- 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 +3 -3
- package/src/libraries/JB721TiersHookLib.sol +17 -3
- 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 +1 -1
- package/test/TestSafeTransferReentrancy.t.sol +1 -1
- package/test/TestVotingUnitsLifecycle.t.sol +1 -1
- package/test/audit/AuditRegressions.t.sol +83 -0
- package/test/audit/CodexNemesis_CrossCurrencySplitNoPrices.t.sol +122 -0
- package/test/audit/USDTVoidReturnCompat.t.sol +301 -0
- package/test/fork/ERC20CashOutFork.t.sol +1 -1
- package/test/fork/ERC20TierSplitFork.t.sol +1 -1
- package/test/fork/IssueTokensForSplitsFork.t.sol +1 -1
- 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/SKILLS.md
CHANGED
|
@@ -21,50 +21,50 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
|
|
|
21
21
|
|
|
22
22
|
| Function | Contract | What it does |
|
|
23
23
|
|----------|----------|--------------|
|
|
24
|
-
| `initialize(projectId, name, symbol, baseUri, tokenUriResolver, contractUri, tiersConfig, flags)` | `JB721TiersHook` | One-time setup for a cloned hook
|
|
24
|
+
| `initialize(projectId, name, symbol, baseUri, tokenUriResolver, contractUri, tiersConfig, flags)` | `JB721TiersHook` | One-time setup for a cloned hook. Stores packed pricing context, records tiers and flags in the store, registers tier splits. |
|
|
25
25
|
| `afterPayRecordedWith(context)` | `JB721Hook` | Called by terminal after payment. Validates caller is a project terminal, delegates to virtual `_processPayment`. |
|
|
26
|
-
| `_processPayment(context)` | `JB721TiersHook` | Normalizes payment value
|
|
27
|
-
| `afterCashOutRecordedWith(context)` | `JB721Hook` | Called by terminal during cash out. Decodes token IDs from metadata, validates ownership, burns NFTs, delegates to virtual `_didBurn`.
|
|
28
|
-
| `beforePayRecordedWith(context)` | `JB721TiersHook` | Data hook: calculates per-tier split amounts
|
|
29
|
-
| `beforeCashOutRecordedWith(context)` | `JB721Hook` | Data hook: calculates `cashOutCount`
|
|
30
|
-
| `adjustTiers(tiersToAdd, tierIdsToRemove)` | `JB721TiersHook` | Owner-only. Adds/removes tiers via `JB721TiersHookLib
|
|
31
|
-
| `mintFor(tierIds, beneficiary)` | `JB721TiersHook` | Owner-only manual mint. Requires `MINT_721` permission.
|
|
26
|
+
| `_processPayment(context)` | `JB721TiersHook` | Normalizes payment value, decodes tier IDs from metadata, mints NFTs, manages pay credits. Distributes tier split funds if forwarded. |
|
|
27
|
+
| `afterCashOutRecordedWith(context)` | `JB721Hook` | Called by terminal during cash out. Decodes token IDs from metadata, validates ownership, burns NFTs, delegates to virtual `_didBurn`. |
|
|
28
|
+
| `beforePayRecordedWith(context)` | `JB721TiersHook` | Data hook: calculates per-tier split amounts, adjusts weight proportionally for the amount entering the project, sets this contract as pay hook. |
|
|
29
|
+
| `beforeCashOutRecordedWith(context)` | `JB721Hook` | Data hook: calculates `cashOutCount` and `totalSupply` via virtual overrides. Rejects if fungible tokens are also being cashed out. |
|
|
30
|
+
| `adjustTiers(tiersToAdd, tierIdsToRemove)` | `JB721TiersHook` | Owner-only. Adds/removes tiers via DELEGATECALL to `JB721TiersHookLib`. Requires `ADJUST_721_TIERS` permission. Registers tier splits if configured. |
|
|
31
|
+
| `mintFor(tierIds, beneficiary)` | `JB721TiersHook` | Owner-only manual mint. Requires `MINT_721` permission. |
|
|
32
32
|
| `mintPendingReservesFor(tierId, count)` | `JB721TiersHook` | Public. Mints pending reserve NFTs for a tier to the tier's `reserveBeneficiary`. Checks ruleset metadata for `mintPendingReservesPaused`. |
|
|
33
33
|
| `mintPendingReservesFor(configs[])` | `JB721TiersHook` | Batch variant. Calls `mintPendingReservesFor(tierId, count)` for each config. |
|
|
34
|
-
| `setMetadata(name, symbol, baseUri, contractUri, tokenUriResolver, encodedIPFSUriTierId, encodedIPFSUri)` | `JB721TiersHook` | Owner-only. Updates collection
|
|
34
|
+
| `setMetadata(name, symbol, baseUri, contractUri, tokenUriResolver, encodedIPFSUriTierId, encodedIPFSUri)` | `JB721TiersHook` | Owner-only. Updates collection metadata fields. Empty strings leave values unchanged. Requires `SET_721_METADATA` permission. |
|
|
35
35
|
| `setDiscountPercentOf(tierId, discountPercent)` | `JB721TiersHook` | Owner-only. Sets discount percent for a tier. Requires `SET_721_DISCOUNT_PERCENT` permission. |
|
|
36
36
|
| `setDiscountPercentsOf(configs[])` | `JB721TiersHook` | Batch variant. Sets discount percent for multiple tiers. Requires `SET_721_DISCOUNT_PERCENT` permission. |
|
|
37
|
-
| `tokenURI(tokenId)` | `JB721TiersHook` | Resolves token metadata URI
|
|
38
|
-
| `firstOwnerOf(tokenId)` | `JB721TiersHook` | Returns the first owner of an NFT
|
|
39
|
-
| `pricingContext()` | `JB721TiersHook` | Unpacks and returns
|
|
37
|
+
| `tokenURI(tokenId)` | `JB721TiersHook` | Resolves token metadata URI via `JB721TiersHookLib.resolveTokenURI`. Checks custom resolver first, falls back to IPFS decoding. |
|
|
38
|
+
| `firstOwnerOf(tokenId)` | `JB721TiersHook` | Returns the first owner of an NFT. Stored on first transfer out; returns current owner if never transferred. |
|
|
39
|
+
| `pricingContext()` | `JB721TiersHook` | Unpacks and returns currency and decimals from the packed `_packedPricingContext`. |
|
|
40
40
|
| `balanceOf(owner)` | `JB721TiersHook` | Overrides ERC-721 `balanceOf` to delegate to `STORE.balanceOf`, which sums across all tiers. |
|
|
41
|
-
| `hasMintPermissionFor(...)` | `JB721Hook` | Always returns `false`. Required by `IJBRulesetDataHook
|
|
42
|
-
| `supportsInterface(interfaceId)` | `JB721TiersHook` | Returns `true` for `IJB721TiersHook`, `IJBRulesetDataHook`, `IJBPayHook`, `IJBCashOutHook`, `
|
|
41
|
+
| `hasMintPermissionFor(...)` | `JB721Hook` | Always returns `false`. Required by `IJBRulesetDataHook`. |
|
|
42
|
+
| `supportsInterface(interfaceId)` | `JB721TiersHook` | Returns `true` for `IJB721TiersHook`, `IJBRulesetDataHook`, `IJBPayHook`, `IJBCashOutHook`, `IERC721`, `IERC721Metadata`, `IERC165`. ERC-2981 support was removed. |
|
|
43
43
|
| `deployHookFor(projectId, config, salt)` | `JB721TiersHookDeployer` | Clones the hook implementation, initializes it, transfers ownership to caller, registers in address registry. |
|
|
44
|
-
| `launchProjectFor(owner, deployConfig, launchConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Creates project via controller, deploys hook, wires
|
|
45
|
-
| `launchRulesetsFor(projectId, deployConfig, launchRulesetsConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Deploys a hook for an existing project and launches rulesets. Requires `QUEUE_RULESETS` and `SET_TERMINALS
|
|
46
|
-
| `queueRulesetsOf(projectId, deployConfig, queueRulesetsConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Deploys a hook and queues rulesets for an existing project. Requires `QUEUE_RULESETS
|
|
47
|
-
| `recordMint(amount, tierIds, isOwnerMint)` | `JB721TiersHookStore` | Records minting: validates supply, checks
|
|
48
|
-
| `recordAddTiers(tiers)` | `JB721TiersHookStore` | Adds new tiers sorted by category. Validates
|
|
49
|
-
| `recordRemoveTierIds(tierIds)` | `JB721TiersHookStore` | Marks tiers as removed in the bitmap.
|
|
44
|
+
| `launchProjectFor(owner, deployConfig, launchConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Creates project via controller, deploys hook, wires as data hook, transfers hook ownership to project. |
|
|
45
|
+
| `launchRulesetsFor(projectId, deployConfig, launchRulesetsConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Deploys a hook for an existing project and launches rulesets. Requires `QUEUE_RULESETS` and `SET_TERMINALS`. |
|
|
46
|
+
| `queueRulesetsOf(projectId, deployConfig, queueRulesetsConfig, controller, salt)` | `JB721TiersHookProjectDeployer` | Deploys a hook and queues rulesets for an existing project. Requires `QUEUE_RULESETS`. |
|
|
47
|
+
| `recordMint(amount, tierIds, isOwnerMint)` | `JB721TiersHookStore` | Records minting: validates supply, checks prices, applies discount, generates token IDs, ensures supply covers pending reserves. |
|
|
48
|
+
| `recordAddTiers(tiers)` | `JB721TiersHookStore` | Adds new tiers sorted by category. Validates flags, limits, supply, sort order, and reserve+owner-mint mutual exclusion. |
|
|
49
|
+
| `recordRemoveTierIds(tierIds)` | `JB721TiersHookStore` | Marks tiers as removed in the bitmap. Does NOT update the sorted linked list -- call `cleanTiers()` afterward. |
|
|
50
50
|
| `recordMintReservesFor(tierId, count)` | `JB721TiersHookStore` | Mints reserve NFTs from remaining supply. Validates count does not exceed pending reserves. |
|
|
51
|
-
| `recordSetDiscountPercentOf(tierId, discountPercent)` | `JB721TiersHookStore` | Sets discount percent for a tier. Validates bounds
|
|
52
|
-
| `recordBurn(tokenIds)` | `JB721TiersHookStore` | Increments burn counter per tier. Trusts
|
|
53
|
-
| `cleanTiers(hook)` | `JB721TiersHookStore` | Public. Removes stale entries from the sorted tier linked list after
|
|
51
|
+
| `recordSetDiscountPercentOf(tierId, discountPercent)` | `JB721TiersHookStore` | Sets discount percent for a tier. Validates bounds and `cannotIncreaseDiscountPercent` constraint. |
|
|
52
|
+
| `recordBurn(tokenIds)` | `JB721TiersHookStore` | Increments burn counter per tier. Trusts the hook to have already verified ownership and burned the tokens. |
|
|
53
|
+
| `cleanTiers(hook)` | `JB721TiersHookStore` | Public. Removes stale entries from the sorted tier linked list after `recordRemoveTierIds`. |
|
|
54
54
|
| `tiersOf(hook, categories, includeResolvedUri, startingId, size)` | `JB721TiersHookStore` | Returns an array of active tiers, optionally filtered by categories. Skips removed tiers. |
|
|
55
55
|
| `tierOf(hook, id, includeResolvedUri)` | `JB721TiersHookStore` | Returns a single tier by ID. |
|
|
56
56
|
| `tierOfTokenId(hook, tokenId, includeResolvedUri)` | `JB721TiersHookStore` | Returns the tier for a given token ID. |
|
|
57
57
|
| `totalSupplyOf(hook)` | `JB721TiersHookStore` | Returns total NFTs minted across all tiers (excluding burns). |
|
|
58
|
-
| `totalCashOutWeight(hook)` | `JB721TiersHookStore` | Returns total cash out weight (
|
|
59
|
-
| `cashOutWeightOf(hook, tokenIds)` | `JB721TiersHookStore` | Returns combined cash out weight for specific token IDs. Uses original tier price
|
|
60
|
-
| `votingUnitsOf(hook, account)` | `JB721TiersHookStore` | Returns total voting units for an address across all tiers.
|
|
58
|
+
| `totalCashOutWeight(hook)` | `JB721TiersHookStore` | Returns total cash out weight: `sum(price * (minted + pendingReserves))`. Uses original price, not discounted. |
|
|
59
|
+
| `cashOutWeightOf(hook, tokenIds)` | `JB721TiersHookStore` | Returns combined cash out weight for specific token IDs. Uses original tier price. |
|
|
60
|
+
| `votingUnitsOf(hook, account)` | `JB721TiersHookStore` | Returns total voting units for an address across all tiers. |
|
|
61
61
|
| `tierVotingUnitsOf(hook, account, tierId)` | `JB721TiersHookStore` | Returns voting units for an address within a specific tier. |
|
|
62
|
-
| `calculateSplitAmounts(store, hook, metadataIdTarget, metadata)` | `JB721TiersHookLib` |
|
|
63
|
-
| `convertSplitAmounts(totalSplitAmount, splitMetadata, packedPricingContext, projectId, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts
|
|
64
|
-
| `distributeAll(directory, splits, projectId, hookAddress, token, amount, decimals, encodedSplitData)` | `JB721TiersHookLib` |
|
|
65
|
-
| `adjustTiersFor(store, splits, projectId, hookAddress, caller, tiersToAdd, tierIdsToRemove)` | `JB721TiersHookLib` | Called via DELEGATECALL from `adjustTiers`. Removes tiers, adds tiers, emits events,
|
|
66
|
-
| `normalizePaymentValue(packedPricingContext, projectId, amountValue, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts a payment value to the hook's pricing currency using `JBPrices`.
|
|
67
|
-
| `resolveTokenURI(store, hook, baseUri, tokenId)` | `JB721TiersHookLib` | Resolves token URI: checks for custom
|
|
62
|
+
| `calculateSplitAmounts(store, hook, metadataIdTarget, metadata)` | `JB721TiersHookLib` | Decodes tier IDs from metadata, computes per-tier split amounts from effective price and `splitPercent`. |
|
|
63
|
+
| `convertSplitAmounts(totalSplitAmount, splitMetadata, packedPricingContext, prices, projectId, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts split amounts from tier pricing denomination to payment token denomination via `JBPrices`. |
|
|
64
|
+
| `distributeAll(directory, splits, projectId, hookAddress, token, amount, decimals, encodedSplitData)` | `JB721TiersHookLib` | Distributes forwarded split funds to tier split recipients. Leftover goes to project balance via `addToBalance`. |
|
|
65
|
+
| `adjustTiersFor(store, splits, projectId, hookAddress, caller, tiersToAdd, tierIdsToRemove)` | `JB721TiersHookLib` | Called via DELEGATECALL from `adjustTiers`. Removes tiers, adds tiers, emits events, registers splits. |
|
|
66
|
+
| `normalizePaymentValue(packedPricingContext, prices, projectId, amountValue, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts a payment value to the hook's pricing currency using `JBPrices`. |
|
|
67
|
+
| `resolveTokenURI(store, hook, baseUri, tokenId)` | `JB721TiersHookLib` | Resolves token URI: checks for custom resolver first, otherwise decodes IPFS URI via `JBIpfsDecoder`. |
|
|
68
68
|
|
|
69
69
|
## Integration Points
|
|
70
70
|
|
|
@@ -74,7 +74,7 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
|
|
|
74
74
|
| `@bananapus/ownable-v6` | `JBOwnable` | Project-based ownership for the hook (ownership can be transferred to a project NFT) |
|
|
75
75
|
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission IDs: `ADJUST_721_TIERS`, `MINT_721`, `SET_721_METADATA`, `SET_721_DISCOUNT_PERCENT`, `QUEUE_RULESETS`, `SET_TERMINALS` |
|
|
76
76
|
| `@bananapus/address-registry-v6` | `IJBAddressRegistry` | Registering deployed hook clones |
|
|
77
|
-
| `@openzeppelin/contracts` | `ERC2771Context`, `IERC165`, `
|
|
77
|
+
| `@openzeppelin/contracts` | `ERC2771Context`, `IERC165`, `IERC721`, `SafeERC20` | Meta-transactions (trusted forwarder), interface detection, safe ERC-20 transfers for split distribution |
|
|
78
78
|
| `@prb/math` | `mulDiv` | Safe fixed-point multiplication/division for price normalization and discount/split calculation |
|
|
79
79
|
| `solady` | `LibClone` | Minimal proxy (clone) deployment for hooks |
|
|
80
80
|
|
|
@@ -139,10 +139,23 @@ Each tier has configurable voting power:
|
|
|
139
139
|
- Each tier can route a percentage of its mint price to configured split recipients. The `splitPercent` field (out of `JBConstants.SPLITS_TOTAL_PERCENT` = 1,000,000,000) determines how much of the price is forwarded.
|
|
140
140
|
- Split recipients are stored in `JBSplits` using group IDs computed as `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`.
|
|
141
141
|
- Splits are registered in `JBSplits` both during `initialize()` (for tiers included at launch) and during `adjustTiers()` (for tiers added later), using the hook's `SPLITS` immutable directly.
|
|
142
|
-
- In `beforePayRecordedWith`, `calculateSplitAmounts`
|
|
142
|
+
- In `beforePayRecordedWith`, `calculateSplitAmounts` processes tier splits:
|
|
143
|
+
- Decodes tier IDs from payer metadata.
|
|
144
|
+
- Applies the tier's `discountPercent` to derive the effective price.
|
|
145
|
+
- Computes `mulDiv(effectivePrice, splitPercent, SPLITS_TOTAL_PERCENT)` per tier.
|
|
146
|
+
- Returns the total to be forwarded to the hook.
|
|
147
|
+
- If the payment currency differs from the tier pricing currency, `convertSplitAmounts` converts amounts to the payment token denomination via `JBPrices`.
|
|
148
|
+
- Weight is adjusted down proportionally (`weight = mulDiv(weight, amount - totalSplitAmount, amount)`) unless the `issueTokensForSplits` flag is set, in which case the full `context.weight` is returned.
|
|
143
149
|
- In `afterPayRecordedWith`, `distributeAll` distributes forwarded funds to each tier's split group recipients. Leftover after all splits goes back to the project's balance via `addToBalance`.
|
|
144
|
-
- Split recipients follow the same priority chain as `JBMultiTerminal`: `split.hook` > `split.projectId` > `split.beneficiary
|
|
145
|
-
-
|
|
150
|
+
- Split recipients follow the same priority chain as `JBMultiTerminal`: `split.hook` > `split.projectId` > `split.beneficiary`:
|
|
151
|
+
- **Split hooks**: receive a `JBSplitHookContext` struct (`token`, `amount`, `decimals`, `projectId`, `groupId`, full `split`). Native tokens forwarded via `{value: amount}`; ERC-20s transferred via `SafeERC20.safeTransfer` before calling `processSplitWith`.
|
|
152
|
+
- **Project splits**: route via `terminal.pay` or `terminal.addToBalance`.
|
|
153
|
+
- **Beneficiary splits**: direct ETH transfer or `SafeERC20.safeTransfer`.
|
|
154
|
+
- **Empty splits** (no hook, no project ID, no beneficiary): skipped -- their share stays in the leftover and routes to the project's balance via `addToBalanceOf`, preventing a misconfigured split from bricking the payout distribution.
|
|
155
|
+
- All external calls in `_sendPayoutToSplit` are wrapped in try-catch to prevent a single reverting recipient from bricking all payments to the project:
|
|
156
|
+
- **Native token hooks**: a revert returns `false` (ETH stays in the contract and routes to the project balance).
|
|
157
|
+
- **ERC-20 hooks**: tokens are transferred via `safeTransfer` before the callback; the function always returns `true` regardless of callback outcome because the tokens have already left -- returning `false` would cause double-spend accounting in the leftover calculation.
|
|
158
|
+
- **ERC-20 terminal calls** (`pay`/`addToBalanceOf`): approval is reset to zero on failure to prevent dangling approvals.
|
|
146
159
|
|
|
147
160
|
## Gotchas
|
|
148
161
|
|
|
@@ -161,19 +174,76 @@ Each tier has configurable voting power:
|
|
|
161
174
|
- `setMetadata` uses `address(this)` as the sentinel for "no change" on `tokenUriResolver` (not `address(0)`). Passing `address(0)` will clear the resolver.
|
|
162
175
|
- `JBPayDataHookRulesetConfig` hardcodes `useDataHookForPay: true` when wiring rulesets through the project deployer. All other metadata fields are passed through.
|
|
163
176
|
- The `_update` override in `JB721TiersHook` checks `tier.transfersPausable` and consults the current ruleset's metadata for `transfersPaused`. Transfers to `address(0)` (burns) are never blocked.
|
|
164
|
-
- **
|
|
177
|
+
- **ERC-2981 not supported**: ERC-2981 royalty support was removed. `supportsInterface` returns `false` for `IERC2981`, and no `royaltyInfo` function exists.
|
|
165
178
|
- **Tier splits**: Each tier can route a percentage of its mint price to configured split recipients. `splitPercent` is out of `JBConstants.SPLITS_TOTAL_PERCENT` (1,000,000,000). Split group IDs are `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`.
|
|
166
179
|
- **`useReserveBeneficiaryAsDefault` overwrites globally**: Adding a tier with `useReserveBeneficiaryAsDefault: true` silently overwrites `defaultReserveBeneficiaryOf` for ALL existing tiers that lack a tier-specific beneficiary. A `SetDefaultReserveBeneficiary` event is emitted when the default changes.
|
|
167
180
|
- **Removing tiers does not update the sorted list**: `recordRemoveTierIds` only marks tiers in the bitmap. Call `cleanTiers()` afterward to remove them from the iteration sequence.
|
|
168
181
|
- `JB721TiersHookStore` is a **shared singleton** -- all hook instances on the same chain use the same store, keyed by `address(hook)`.
|
|
169
182
|
- The `ERC721` abstract uses `_initialize(name, symbol)` instead of a constructor, making it clone-compatible. It also exposes `_setName()` and `_setSymbol()` for post-initialization updates. The standard `_owners` mapping is `internal` (not `private`).
|
|
183
|
+
- **Noop hook specifications**: `JBPayHookSpecification` and `JBCashOutHookSpecification` each have a `bool noop` field. When the 721 hook is composed with another data hook (e.g., via `REVDeployer` or `JBOmnichainDeployer`), the outer data hook may return noop specs alongside the 721 hook's active specs. The 721 hook itself always returns active specs (`noop = false`) — it needs the callback to mint NFTs. Noop specs with `amount != 0` revert at the terminal store level.
|
|
170
184
|
- **`hasMintPermissionFor` always returns `false`**: The hook never grants mint permission to any address. This is part of the `IJBRulesetDataHook` interface.
|
|
171
185
|
- **Max tier count is 65,535** (`type(uint16).max`). Adding tiers beyond this limit reverts.
|
|
172
186
|
- **Max initial supply per tier is 999,999,999** (`_ONE_BILLION - 1`). Exceeding this would cause token ID overflow into the next tier's ID space.
|
|
173
187
|
- **`noNewTiersWithVotes` blocks all voting power**: It rejects tiers where voting units would be non-zero, whether from custom `votingUnits` or from a non-zero `price` (when `useVotingUnits` is false).
|
|
174
188
|
- **`firstOwnerOf` is lazy**: The first owner is only stored when the token is first transferred away from its original holder. Before any transfer, `firstOwnerOf` returns the current owner.
|
|
175
189
|
- **Tiers must be sorted by category, NOT price.** `recordAddTiers` reverts with `JB721TiersHookStore_InvalidCategorySortOrder` if tiers aren't in ascending category order. The `JB721InitTiersConfig` struct comment previously said "sorted by price" but the code enforces category ordering. Within the same category, tiers can be in any order.
|
|
176
|
-
- **Always use `JB721TiersHookProjectDeployer.launchProjectFor` even without NFTs.** Pass an empty tiers array to enable future NFT additions without migration. If a project is launched via `JBController.launchProjectFor` instead, adding NFT tiers later requires wiring a new data hook into a new ruleset
|
|
190
|
+
- **Always use `JB721TiersHookProjectDeployer.launchProjectFor` even without NFTs.** Pass an empty tiers array to enable future NFT additions without migration. If a project is launched via `JBController.launchProjectFor` instead, adding NFT tiers later requires wiring a new data hook into a new ruleset -- using the 721 deployer from the start avoids this.
|
|
191
|
+
|
|
192
|
+
## Custom Errors
|
|
193
|
+
|
|
194
|
+
| Error | Contract | When |
|
|
195
|
+
|-------|----------|------|
|
|
196
|
+
| `JB721Hook_InvalidCashOut()` | `JB721Hook` | `afterCashOutRecordedWith` caller is not a project terminal. |
|
|
197
|
+
| `JB721Hook_InvalidPay()` | `JB721Hook` | `afterPayRecordedWith` caller is not a project terminal. |
|
|
198
|
+
| `JB721Hook_UnauthorizedToken(tokenId, holder)` | `JB721Hook` | Cash out attempts to burn a token not owned by `context.holder`. |
|
|
199
|
+
| `JB721Hook_UnexpectedTokenCashedOut()` | `JB721Hook` | `beforeCashOutRecordedWith` called with `cashOutCount > 0` (fungible tokens mixed with NFT cash out). |
|
|
200
|
+
| `JB721TiersHook_AlreadyInitialized(projectId)` | `JB721TiersHook` | `initialize` called on a hook that already has a `PROJECT_ID`. |
|
|
201
|
+
| `JB721TiersHook_CurrencyMismatch(paymentCurrency, tierCurrency)` | `JB721TiersHook` | Payment currency differs from tier pricing currency and no `PRICES` contract is configured for conversion. |
|
|
202
|
+
| `JB721TiersHook_InvalidPricingDecimals(decimals)` | `JB721TiersHook` | `initialize` called with `decimals > 18`. |
|
|
203
|
+
| `JB721TiersHook_MintReserveNftsPaused()` | `JB721TiersHook` | `mintPendingReservesFor` called while `pauseMintPendingReserves` is set in the current ruleset metadata. |
|
|
204
|
+
| `JB721TiersHook_NoProjectId()` | `JB721TiersHook` | `initialize` called with `projectId == 0`. |
|
|
205
|
+
| `JB721TiersHook_Overspending(leftoverAmount)` | `JB721TiersHook` | Payment has leftover funds after minting and `preventOverspending` flag is set. |
|
|
206
|
+
| `JB721TiersHook_TierTransfersPaused()` | `JB721TiersHook` | NFT transfer attempted on a tier with `transfersPausable` while `transfersPaused` is set in ruleset metadata. |
|
|
207
|
+
| `JB721TiersHookStore_CantMintManually(tierId)` | `JB721TiersHookStore` | Owner mint attempted on a tier with `allowOwnerMint: false`. |
|
|
208
|
+
| `JB721TiersHookStore_CantRemoveTier(tierId)` | `JB721TiersHookStore` | Removing a tier that has `cannotBeRemoved: true`. |
|
|
209
|
+
| `JB721TiersHookStore_DiscountPercentExceedsBounds(percent, limit)` | `JB721TiersHookStore` | Discount percent exceeds `DISCOUNT_DENOMINATOR` (200). |
|
|
210
|
+
| `JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(percent, storedPercent)` | `JB721TiersHookStore` | Increasing discount on a tier with `cannotIncreaseDiscountPercent: true`. |
|
|
211
|
+
| `JB721TiersHookStore_InsufficientPendingReserves(count, numberOfPendingReserves)` | `JB721TiersHookStore` | `recordMintReservesFor` called with `count` exceeding available pending reserves. |
|
|
212
|
+
| `JB721TiersHookStore_InsufficientSupplyRemaining(tierId)` | `JB721TiersHookStore` | Tier has no remaining supply, or remaining supply does not cover pending reserves after mint. |
|
|
213
|
+
| `JB721TiersHookStore_InvalidCategorySortOrder(tierCategory, previousTierCategory)` | `JB721TiersHookStore` | Tiers not sorted in ascending `category` order during `recordAddTiers`. |
|
|
214
|
+
| `JB721TiersHookStore_InvalidQuantity(quantity, limit)` | `JB721TiersHookStore` | Tier `initialSupply` exceeds max (999,999,999). |
|
|
215
|
+
| `JB721TiersHookStore_ManualMintingNotAllowed(tierId)` | `JB721TiersHookStore` | Adding a tier with `allowOwnerMint: true` when `noNewTiersWithOwnerMinting` flag is set. |
|
|
216
|
+
| `JB721TiersHookStore_MaxTiersExceeded(numberOfTiers, limit)` | `JB721TiersHookStore` | Total tier count exceeds `type(uint16).max` (65,535). |
|
|
217
|
+
| `JB721TiersHookStore_PriceExceedsAmount(price, leftoverAmount)` | `JB721TiersHookStore` | Tier price exceeds remaining payment amount during `recordMint`. |
|
|
218
|
+
| `JB721TiersHookStore_ReserveFrequencyNotAllowed(tierId)` | `JB721TiersHookStore` | Adding a tier with `reserveFrequency` when `noNewTiersWithReserves` flag is set. |
|
|
219
|
+
| `JB721TiersHookStore_SplitPercentExceedsBounds(percent, limit)` | `JB721TiersHookStore` | Tier `splitPercent` exceeds `SPLITS_TOTAL_PERCENT`. |
|
|
220
|
+
| `JB721TiersHookStore_TierRemoved(tierId)` | `JB721TiersHookStore` | Attempting to mint from a tier that has been removed. |
|
|
221
|
+
| `JB721TiersHookStore_UnrecognizedTier(tierId)` | `JB721TiersHookStore` | Tier ID does not exist (`initialSupply == 0`). |
|
|
222
|
+
| `JB721TiersHookStore_VotingUnitsNotAllowed(tierId)` | `JB721TiersHookStore` | Adding a tier with voting power when `noNewTiersWithVotes` flag is set. |
|
|
223
|
+
| `JB721TiersHookStore_ZeroInitialSupply(tierId)` | `JB721TiersHookStore` | Adding a tier with `initialSupply == 0`. |
|
|
224
|
+
|
|
225
|
+
## Events
|
|
226
|
+
|
|
227
|
+
| Event | Contract | Key Params |
|
|
228
|
+
|-------|----------|------------|
|
|
229
|
+
| `AddToBalanceReverted(projectId, token, amount, reason)` | `IJB721TiersHook` | Emitted when leftover `addToBalanceOf` call reverts during split distribution. Funds remain stranded in hook. |
|
|
230
|
+
| `AddPayCredits(amount, newTotalCredits, account, caller)` | `IJB721TiersHook` | Pay credits added for an account (overspending stored for future mints). |
|
|
231
|
+
| `AddTier(tierId, tier, caller)` | `IJB721TiersHook` | New tier added via `adjustTiers`. `tier` is the full `JB721TierConfig`. |
|
|
232
|
+
| `Mint(tokenId, tierId, beneficiary, totalAmountPaid, caller)` | `IJB721TiersHook` | NFT minted from a payment. |
|
|
233
|
+
| `MintReservedNft(tokenId, tierId, beneficiary, caller)` | `IJB721TiersHook` | Reserve NFT minted via `mintPendingReservesFor`. |
|
|
234
|
+
| `RemoveTier(tierId, caller)` | `IJB721TiersHook` | Tier removed via `adjustTiers`. |
|
|
235
|
+
| `SetName(name, caller)` | `IJB721TiersHook` | Collection name updated via `setMetadata`. |
|
|
236
|
+
| `SetSymbol(symbol, caller)` | `IJB721TiersHook` | Collection symbol updated via `setMetadata`. |
|
|
237
|
+
| `SetBaseUri(baseUri, caller)` | `IJB721TiersHook` | Base URI updated via `setMetadata`. |
|
|
238
|
+
| `SetContractUri(uri, caller)` | `IJB721TiersHook` | Contract URI updated via `setMetadata`. |
|
|
239
|
+
| `SetDiscountPercent(tierId, discountPercent, caller)` | `IJB721TiersHook` | Tier discount percent changed via `setDiscountPercentOf`. |
|
|
240
|
+
| `SetEncodedIPFSUri(tierId, encodedUri, caller)` | `IJB721TiersHook` | Tier IPFS URI updated via `setMetadata`. |
|
|
241
|
+
| `SetTokenUriResolver(resolver, caller)` | `IJB721TiersHook` | Token URI resolver updated via `setMetadata`. |
|
|
242
|
+
| `SplitPayoutReverted(projectId, split, amount, reason, caller)` | `IJB721TiersHook` | A split payout reverted during distribution. Failed split's funds route to project balance. |
|
|
243
|
+
| `UsePayCredits(amount, newTotalCredits, account, caller)` | `IJB721TiersHook` | Pay credits consumed during a payment. |
|
|
244
|
+
| `CleanTiers(hook, caller)` | `JB721TiersHookStore` | Removed tiers cleaned from the sorting linked list. |
|
|
245
|
+
| `SetDefaultReserveBeneficiary(hook, newBeneficiary, caller)` | `JB721TiersHookStore` | Default reserve beneficiary changed (affects all tiers without a tier-specific beneficiary). |
|
|
246
|
+
| `HookDeployed(projectId, hook, caller)` | `JB721TiersHookDeployer` | New hook clone deployed for a project. |
|
|
177
247
|
|
|
178
248
|
## Example Integration
|
|
179
249
|
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
|
|
|
21
21
|
|
|
22
22
|
```solidity
|
|
23
23
|
// Contracts — pin to exact version
|
|
24
|
-
pragma solidity 0.8.
|
|
24
|
+
pragma solidity 0.8.28;
|
|
25
25
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|
|
@@ -326,7 +326,7 @@ Standard config across all repos:
|
|
|
326
326
|
|
|
327
327
|
```toml
|
|
328
328
|
[profile.default]
|
|
329
|
-
solc = '0.8.
|
|
329
|
+
solc = '0.8.28'
|
|
330
330
|
evm_version = 'cancun'
|
|
331
331
|
optimizer_runs = 200
|
|
332
332
|
libs = ["node_modules", "lib"]
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -14,6 +14,8 @@ A user pays a Juicebox project and receives tiered NFTs based on the amount paid
|
|
|
14
14
|
|
|
15
15
|
**Entry point**: `JBMultiTerminal.pay()` (external). The hook is invoked as both a data hook (`beforePayRecordedWith`) and a pay hook (`afterPayRecordedWith`).
|
|
16
16
|
|
|
17
|
+
**Who can call**: Anyone. The terminal's `pay()` is permissionless.
|
|
18
|
+
|
|
17
19
|
**Parameters** (encoded in payment metadata via `JBMetadataResolver`):
|
|
18
20
|
- `bool allowOverspending` -- whether leftover funds after minting should be stored as credits (true) or revert (false). The hook-level `preventOverspending` flag can override this.
|
|
19
21
|
- `uint16[] tierIdsToMint` -- which tier IDs to mint, in order. The same tier can appear multiple times.
|
|
@@ -33,6 +35,8 @@ A user pays a Juicebox project and receives tiered NFTs based on the amount paid
|
|
|
33
35
|
- `Mint(tokenId, tierId, beneficiary, totalAmountPaid, caller)` -- one per NFT minted.
|
|
34
36
|
- `AddPayCredits(amount, newTotalCredits, account, caller)` -- if credits increased.
|
|
35
37
|
- `UsePayCredits(amount, newTotalCredits, account, caller)` -- if credits decreased.
|
|
38
|
+
- `SplitPayoutReverted(projectId, split, amount, reason, caller)` -- if a split recipient's payout reverts during distribution (funds route to project balance instead).
|
|
39
|
+
- `AddToBalanceReverted(projectId, token, amount, reason)` -- if the `addToBalanceOf` call for leftover funds reverts after split distribution.
|
|
36
40
|
|
|
37
41
|
**Edge cases**:
|
|
38
42
|
- **No metadata or metadata not found**: No NFTs minted. If `preventOverspending` is false, the entire payment becomes credits for the beneficiary. If true, reverts.
|
|
@@ -53,6 +57,8 @@ An NFT holder burns their NFTs to reclaim funds from the project's surplus, prop
|
|
|
53
57
|
|
|
54
58
|
**Entry point**: `JBMultiTerminal.cashOutTokensOf()` (external). The hook is invoked as both a data hook (`beforeCashOutRecordedWith`) and a cash out hook (`afterCashOutRecordedWith`).
|
|
55
59
|
|
|
60
|
+
**Who can call**: The NFT holder, or an operator with `CASH_OUT_TOKENS` permission from the holder.
|
|
61
|
+
|
|
56
62
|
**Parameters** (encoded in cash-out metadata via `JBMetadataResolver`):
|
|
57
63
|
- `uint256[] tokenIds` -- the token IDs of the NFTs to burn.
|
|
58
64
|
|
|
@@ -72,7 +78,7 @@ An NFT holder burns their NFTs to reclaim funds from the project's surplus, prop
|
|
|
72
78
|
|
|
73
79
|
**Cash-out weight calculation**:
|
|
74
80
|
- Per-NFT weight: `storedTier.price` (original price, NOT discounted).
|
|
75
|
-
- Total weight: sum of `price * (
|
|
81
|
+
- Total weight: sum of `price * (outstandingCount + pendingReserves)` across ALL tiers (including removed tiers), where `outstandingCount = initialSupply - remainingSupply - numberOfBurned`. Burned tokens are excluded from the total weight, so burning NFTs reduces the denominator and increases the per-NFT reclaim for remaining holders.
|
|
76
82
|
- Reclaim amount is computed by the terminal's bonding curve using `cashOutCount / totalSupply` as the ratio.
|
|
77
83
|
|
|
78
84
|
**Events**:
|
|
@@ -92,7 +98,7 @@ The project owner adds new NFT tiers to the hook.
|
|
|
92
98
|
|
|
93
99
|
**Entry point**: `JB721TiersHook.adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove)` (external).
|
|
94
100
|
|
|
95
|
-
**
|
|
101
|
+
**Who can call**: Hook owner, or an operator with `ADJUST_721_TIERS` permission from the hook owner.
|
|
96
102
|
|
|
97
103
|
**Parameters** (per `JB721TierConfig`):
|
|
98
104
|
- `uint104 price` -- tier price in the hook's pricing currency.
|
|
@@ -124,16 +130,20 @@ The project owner adds new NFT tiers to the hook.
|
|
|
124
130
|
|
|
125
131
|
**Events**:
|
|
126
132
|
- `AddTier(tierId, tierConfig, caller)` -- one per tier added.
|
|
133
|
+
- `SetDefaultReserveBeneficiary(hook, newBeneficiary, caller)` -- if any tier has `useReserveBeneficiaryAsDefault = true` and changes the current default.
|
|
127
134
|
|
|
128
135
|
**Edge cases**:
|
|
129
136
|
- **Categories not sorted ascending**: Reverts with `JB721TiersHookStore_InvalidCategorySortOrder`.
|
|
130
137
|
- **Exceeds 65,535 total tiers**: Reverts with `JB721TiersHookStore_MaxTiersExceeded`.
|
|
131
138
|
- **`initialSupply == 0`**: Reverts with `JB721TiersHookStore_ZeroInitialSupply`.
|
|
132
|
-
- **`
|
|
133
|
-
- **`
|
|
134
|
-
- **`
|
|
139
|
+
- **`initialSupply > 999,999,999`**: Reverts with `JB721TiersHookStore_InvalidQuantity`. Each tier's supply must fit within the token ID encoding scheme (`tierId * 1e9 + tokenNumber`).
|
|
140
|
+
- **`discountPercent > 200`**: Reverts with `JB721TiersHookStore_DiscountPercentExceedsBounds`.
|
|
141
|
+
- **`splitPercent > SPLITS_TOTAL_PERCENT`**: Reverts with `JB721TiersHookStore_SplitPercentExceedsBounds`.
|
|
142
|
+
- **`noNewTiersWithVotes` flag set**: Reverts with `JB721TiersHookStore_VotingUnitsNotAllowed` if the new tier would have any voting power. This means tiers with `useVotingUnits = true` and `votingUnits > 0` are rejected, AND tiers with `useVotingUnits = false` and `price > 0` are also rejected (since price is used as voting power by default when `useVotingUnits` is false).
|
|
143
|
+
- **`noNewTiersWithReserves` flag set**: Reverts with `JB721TiersHookStore_ReserveFrequencyNotAllowed` if tier has `reserveFrequency > 0`.
|
|
144
|
+
- **`noNewTiersWithOwnerMinting` flag set**: Reverts with `JB721TiersHookStore_ManualMintingNotAllowed` if tier has `allowOwnerMint = true`.
|
|
135
145
|
- **`allowOwnerMint` + `reserveFrequency > 0`**: Reverts with `JB721TiersHookStore_ReserveFrequencyNotAllowed`. Owner-mintable tiers cannot have reserves.
|
|
136
|
-
- **`useReserveBeneficiaryAsDefault = true`**:
|
|
146
|
+
- **`useReserveBeneficiaryAsDefault = true`**: Only takes effect when both `reserveBeneficiary != address(0)` AND `reserveFrequency != 0`. When both conditions are met, silently overwrites `defaultReserveBeneficiaryOf[hook]`, affecting all existing tiers that use the default. If either condition is missing, the flag is silently ignored.
|
|
137
147
|
|
|
138
148
|
---
|
|
139
149
|
|
|
@@ -143,7 +153,7 @@ The project owner removes tiers, preventing new mints but preserving existing NF
|
|
|
143
153
|
|
|
144
154
|
**Entry point**: `JB721TiersHook.adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove)` (external). Pass an empty `tiersToAdd` array to only remove.
|
|
145
155
|
|
|
146
|
-
**
|
|
156
|
+
**Who can call**: Hook owner, or an operator with `ADJUST_721_TIERS` permission from the hook owner.
|
|
147
157
|
|
|
148
158
|
**Parameters**:
|
|
149
159
|
- `uint256[] tierIdsToRemove` -- IDs of tiers to remove.
|
|
@@ -172,6 +182,8 @@ Anyone can mint pending reserved NFTs for a tier. Reserves accumulate based on t
|
|
|
172
182
|
|
|
173
183
|
**Batch entry point**: `JB721TiersHook.mintPendingReservesFor(JB721TiersMintReservesConfig[] calldata reserveMintConfigs)` (external, permissionless).
|
|
174
184
|
|
|
185
|
+
**Who can call**: Anyone. Both entry points are permissionless.
|
|
186
|
+
|
|
175
187
|
**Parameters**:
|
|
176
188
|
- `uint256 tierId` -- the tier to mint reserves from.
|
|
177
189
|
- `uint256 count` -- how many reserve NFTs to mint.
|
|
@@ -213,7 +225,7 @@ The project owner adjusts the discount on a tier's mint price. Does not affect c
|
|
|
213
225
|
|
|
214
226
|
**Batch entry point**: `JB721TiersHook.setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs)` (external).
|
|
215
227
|
|
|
216
|
-
**
|
|
228
|
+
**Who can call**: Hook owner, or an operator with `SET_721_DISCOUNT_PERCENT` permission from the hook owner.
|
|
217
229
|
|
|
218
230
|
**Parameters**:
|
|
219
231
|
- `uint256 tierId` -- the tier to update.
|
|
@@ -240,7 +252,7 @@ The project owner directly mints NFTs from tiers that have `allowOwnerMint = tru
|
|
|
240
252
|
|
|
241
253
|
**Entry point**: `JB721TiersHook.mintFor(uint16[] calldata tierIds, address beneficiary)` (external).
|
|
242
254
|
|
|
243
|
-
**
|
|
255
|
+
**Who can call**: Hook owner, or an operator with `MINT_721` permission from the hook owner.
|
|
244
256
|
|
|
245
257
|
**Parameters**:
|
|
246
258
|
- `uint16[] tierIds` -- the tiers to mint from. One NFT per entry. Can repeat tiers.
|
|
@@ -270,7 +282,7 @@ A single call that both adds new tiers and removes existing tiers.
|
|
|
270
282
|
|
|
271
283
|
**Entry point**: `JB721TiersHook.adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove)` (external).
|
|
272
284
|
|
|
273
|
-
**
|
|
285
|
+
**Who can call**: Hook owner, or an operator with `ADJUST_721_TIERS` permission from the hook owner.
|
|
274
286
|
|
|
275
287
|
**Execution order**:
|
|
276
288
|
1. Removals are processed first (bitmap marked).
|
|
@@ -289,6 +301,8 @@ Deploy a 721 tiers hook for an existing project.
|
|
|
289
301
|
|
|
290
302
|
**Entry point**: `JB721TiersHookDeployer.deployHookFor(uint256 projectId, JBDeploy721TiersHookConfig calldata deployTiersHookConfig, bytes32 salt)` (external).
|
|
291
303
|
|
|
304
|
+
**Who can call**: Anyone. The deployer is permissionless. The caller becomes the initial hook owner.
|
|
305
|
+
|
|
292
306
|
**Parameters**:
|
|
293
307
|
- `uint256 projectId` -- the project to associate the hook with.
|
|
294
308
|
- `JBDeploy721TiersHookConfig deployTiersHookConfig`:
|
|
@@ -314,6 +328,7 @@ Deploy a 721 tiers hook for an existing project.
|
|
|
314
328
|
**Events**:
|
|
315
329
|
- `HookDeployed(projectId, newHook, caller)`.
|
|
316
330
|
- `AddTier(tierId, tierConfig, caller)` -- one per initial tier.
|
|
331
|
+
- `SetDefaultReserveBeneficiary(hook, newBeneficiary, caller)` -- if any initial tier has `useReserveBeneficiaryAsDefault = true`.
|
|
317
332
|
|
|
318
333
|
**Edge cases**:
|
|
319
334
|
- **Re-initialization**: Reverts with `JB721TiersHook_AlreadyInitialized` if `PROJECT_ID` is already set.
|
|
@@ -330,6 +345,8 @@ Launch a new Juicebox project with a 721 tiers hook attached, all in one transac
|
|
|
330
345
|
|
|
331
346
|
**Entry point**: `JB721TiersHookProjectDeployer.launchProjectFor(address owner, JBDeploy721TiersHookConfig calldata deployTiersHookConfig, JBLaunchProjectConfig calldata launchProjectConfig, IJBController controller, bytes32 salt)` (external).
|
|
332
347
|
|
|
348
|
+
**Who can call**: Anyone. The deployer is permissionless. The `owner` parameter receives the project ERC-721.
|
|
349
|
+
|
|
333
350
|
**Parameters**:
|
|
334
351
|
- `address owner` -- receives the project ERC-721.
|
|
335
352
|
- `JBDeploy721TiersHookConfig deployTiersHookConfig` -- hook config (see Journey 9).
|
|
@@ -362,9 +379,9 @@ Launch a new Juicebox project with a 721 tiers hook attached, all in one transac
|
|
|
362
379
|
|
|
363
380
|
Set a custom contract that resolves token URIs for all NFTs in the collection.
|
|
364
381
|
|
|
365
|
-
**Entry point**: `JB721TiersHook.setMetadata(...)` (external). Pass `address(0)`
|
|
382
|
+
**Entry point**: `JB721TiersHook.setMetadata(...)` (external). The `tokenUriResolver` parameter is an optional contract that can override the default IPFS-based token URI generation. Pass a contract address to set a custom resolver, `address(0)` to clear it and revert to the default, or the sentinel value `address(this)` to leave it unchanged.
|
|
366
383
|
|
|
367
|
-
**
|
|
384
|
+
**Who can call**: Hook owner, or an operator with `SET_721_METADATA` permission from the hook owner.
|
|
368
385
|
|
|
369
386
|
**Parameters** (relevant subset):
|
|
370
387
|
- `IJB721TokenUriResolver tokenUriResolver` -- the new resolver. Pass `IJB721TokenUriResolver(address(this))` to skip (no change). Pass `IJB721TokenUriResolver(address(0))` to clear.
|
|
@@ -378,7 +395,7 @@ Set a custom contract that resolves token URIs for all NFTs in the collection.
|
|
|
378
395
|
**Behavior**:
|
|
379
396
|
- When set, `tokenURI(tokenId)` calls `resolver.tokenUriOf(nft, tokenId)` instead of using IPFS URIs.
|
|
380
397
|
- When cleared (set to `address(0)`), falls back to IPFS-based URIs via `JBIpfsDecoder`.
|
|
381
|
-
- The sentinel value for "skip" is `address(this)` (the hook's own address), checked
|
|
398
|
+
- The sentinel value for "skip" is `address(this)` (the hook's own address), checked in `tokenURI()`.
|
|
382
399
|
|
|
383
400
|
**Edge cases**:
|
|
384
401
|
- **Malicious resolver**: Could revert (blocking metadata reads for marketplaces) or return misleading URIs. Cannot affect funds.
|
|
@@ -392,6 +409,8 @@ Reorganize the sorted tier linked list to skip removed tiers. Improves iteration
|
|
|
392
409
|
|
|
393
410
|
**Entry point**: `JB721TiersHookStore.cleanTiers(address hook)` (external, permissionless).
|
|
394
411
|
|
|
412
|
+
**Who can call**: Anyone. This function is permissionless.
|
|
413
|
+
|
|
395
414
|
**Parameters**:
|
|
396
415
|
- `address hook` -- the hook contract whose tier list to clean.
|
|
397
416
|
|
|
@@ -414,7 +433,7 @@ Update the collection's name, symbol, base URI, contract URI, or per-tier IPFS U
|
|
|
414
433
|
|
|
415
434
|
**Entry point**: `JB721TiersHook.setMetadata(string calldata name, string calldata symbol, string calldata baseUri, string calldata contractUri, IJB721TokenUriResolver tokenUriResolver, uint256 encodedIPFSUriTierId, bytes32 encodedIPFSUri)` (external).
|
|
416
435
|
|
|
417
|
-
**
|
|
436
|
+
**Who can call**: Hook owner, or an operator with `SET_721_METADATA` permission from the hook owner.
|
|
418
437
|
|
|
419
438
|
**Parameters**:
|
|
420
439
|
- `string name` -- new collection name. Empty string = no change.
|
|
@@ -448,7 +467,7 @@ Transfer an NFT between addresses. Subject to per-tier and per-ruleset pause con
|
|
|
448
467
|
|
|
449
468
|
**Entry point**: Standard ERC-721 `transferFrom(address from, address to, uint256 tokenId)` or `safeTransferFrom(...)`.
|
|
450
469
|
|
|
451
|
-
**
|
|
470
|
+
**Who can call**: Standard ERC-721 (token owner, approved address, or approved-for-all operator).
|
|
452
471
|
|
|
453
472
|
**State changes**:
|
|
454
473
|
1. ERC-721 ownership updated.
|
|
@@ -480,6 +499,8 @@ Configure how a percentage of a tier's effective price is distributed to split r
|
|
|
480
499
|
|
|
481
500
|
**Entry point**: Splits are set during tier creation via `adjustTiers` (see Journey 3). The `splits` field in `JB721TierConfig` defines the split recipients.
|
|
482
501
|
|
|
502
|
+
**Who can call**: Splits are configured as part of `adjustTiers`, so the same permission applies: hook owner or an operator with `ADJUST_721_TIERS` permission. Split distribution during payment is automatic and permissionless.
|
|
503
|
+
|
|
483
504
|
**Split group ID**: `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`. This is stored in `JBSplits` with `rulesetId = 0` (always active).
|
|
484
505
|
|
|
485
506
|
**How it works during payment**:
|
|
@@ -494,14 +515,18 @@ Configure how a percentage of a tier's effective price is distributed to split r
|
|
|
494
515
|
|
|
495
516
|
**Parameters** (per `JBSplit`):
|
|
496
517
|
- `bool preferAddToBalance` -- for project splits, use `addToBalanceOf` instead of `pay`.
|
|
497
|
-
- `
|
|
498
|
-
- `
|
|
518
|
+
- `uint32 percent` -- percentage of the remaining amount (sequential, not parallel).
|
|
519
|
+
- `uint64 projectId` -- target project (0 = no project split).
|
|
499
520
|
- `address beneficiary` -- direct recipient (if no hook and no projectId).
|
|
500
521
|
- `IJBSplitHook hook` -- split hook contract (highest priority).
|
|
501
|
-
- `
|
|
522
|
+
- `uint48 lockedUntil` -- timestamp until which this split is locked and cannot be modified.
|
|
523
|
+
|
|
524
|
+
**Events** (during split distribution on payment):
|
|
525
|
+
- `SplitPayoutReverted(projectId, split, amount, reason, caller)` -- if an individual split recipient's payout reverts (funds become leftover, routed to project balance).
|
|
526
|
+
- `AddToBalanceReverted(projectId, token, amount, reason)` -- if the `addToBalanceOf` call for leftover funds reverts after distribution.
|
|
502
527
|
|
|
503
528
|
**Edge cases**:
|
|
504
|
-
- **`splitPercent` > SPLITS_TOTAL_PERCENT**:
|
|
529
|
+
- **`splitPercent` > SPLITS_TOTAL_PERCENT**: Validated at tier creation in `recordAddTiers`. Reverts with `JB721TiersHookStore_SplitPercentExceedsBounds` if `splitPercent` exceeds `JBConstants.SPLITS_TOTAL_PERCENT` (1e9).
|
|
505
530
|
- **Weight adjustment**: When splits route funds away, `calculateWeight` reduces the mint weight so payers receive fewer project tokens proportional to the split fraction. This can be disabled with `issueTokensForSplits = true`.
|
|
506
531
|
- **Cross-currency**: Split amounts are calculated in the pricing currency, then converted to the payment token denomination. Rounding occurs at each step.
|
|
507
532
|
- **ERC-20 tokens**: The library pulls tokens from the terminal via `safeTransferFrom`, distributes them, and approves terminals for project splits via `forceApprove`.
|
package/foundry.toml
CHANGED
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.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bananapus/address-registry-v6": "^0.0.
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
22
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
23
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
20
|
+
"@bananapus/address-registry-v6": "^0.0.15",
|
|
21
|
+
"@bananapus/core-v6": "^0.0.27",
|
|
22
|
+
"@bananapus/ownable-v6": "^0.0.14",
|
|
23
|
+
"@bananapus/permission-ids-v6": "^0.0.14",
|
|
24
24
|
"@openzeppelin/contracts": "^5.6.1",
|
|
25
25
|
"@prb/math": "^4.1.0",
|
|
26
26
|
"solady": "^0.1.8"
|
package/script/Deploy.s.sol
CHANGED
package/src/JB721TiersHook.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
5
5
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
@@ -26,6 +26,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
26
26
|
|
|
27
27
|
error JB721TiersHookStore_CantMintManually(uint256 tierId);
|
|
28
28
|
error JB721TiersHookStore_CantRemoveTier(uint256 tierId);
|
|
29
|
+
error JB721TiersHookStore_DeadlockedReserve();
|
|
29
30
|
error JB721TiersHookStore_DiscountPercentExceedsBounds(uint256 percent, uint256 limit);
|
|
30
31
|
error JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(uint256 percent, uint256 storedPercent);
|
|
31
32
|
error JB721TiersHookStore_InsufficientPendingReserves(uint256 count, uint256 numberOfPendingReserves);
|
|
@@ -872,6 +873,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
872
873
|
// Make sure the tier has a non-zero supply.
|
|
873
874
|
if (tierToAdd.initialSupply == 0) revert JB721TiersHookStore_ZeroInitialSupply(tierId);
|
|
874
875
|
|
|
876
|
+
// A tier with initialSupply == 1 and reserveFrequency > 0 deadlocks: the single mint is reserved,
|
|
877
|
+
// leaving zero available for paid mints, but reserves only mint after a paid mint triggers them.
|
|
878
|
+
if (tierToAdd.initialSupply == 1 && tierToAdd.reserveFrequency > 0) {
|
|
879
|
+
revert JB721TiersHookStore_DeadlockedReserve();
|
|
880
|
+
}
|
|
881
|
+
|
|
875
882
|
// Store the tier with that ID.
|
|
876
883
|
_storedTierOf[msg.sender][tierId] = JBStored721Tier({
|
|
877
884
|
price: uint104(tierToAdd.price),
|
|
@@ -1181,6 +1188,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1181
1188
|
/// @param tierId The ID of the tier to record a discount for.
|
|
1182
1189
|
/// @param discountPercent The new discount percent being applied.
|
|
1183
1190
|
function recordSetDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
|
|
1191
|
+
// Make sure the tier hasn't been removed.
|
|
1192
|
+
JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[msg.sender].readId(tierId);
|
|
1193
|
+
if (bitmapWord.isTierIdRemoved(tierId)) revert JB721TiersHookStore_TierRemoved(tierId);
|
|
1194
|
+
|
|
1184
1195
|
// Make sure the discount percent is within the bound.
|
|
1185
1196
|
if (discountPercent > JB721Constants.DISCOUNT_DENOMINATOR) {
|
|
1186
1197
|
revert JB721TiersHookStore_DiscountPercentExceedsBounds(
|
package/src/abstract/ERC721.sol
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/ERC721.sol)
|
|
3
3
|
|
|
4
|
-
pragma solidity 0.8.
|
|
4
|
+
pragma solidity 0.8.28;
|
|
5
5
|
|
|
6
6
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
7
7
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|