@bananapus/721-hook-v6 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +38 -11
- package/ARCHITECTURE.md +53 -99
- package/AUDIT_INSTRUCTIONS.md +84 -383
- package/CHANGELOG.md +71 -0
- package/README.md +79 -225
- package/RISKS.md +28 -11
- package/SKILLS.md +29 -296
- package/STYLE_GUIDE.md +57 -18
- package/USER_JOURNEYS.md +57 -501
- package/package.json +1 -1
- package/references/operations.md +28 -0
- package/references/runtime.md +32 -0
- package/script/Deploy.s.sol +5 -4
- package/src/JB721TiersHook.sol +1 -1
- package/src/JB721TiersHookDeployer.sol +1 -1
- package/src/JB721TiersHookProjectDeployer.sol +1 -1
- package/src/JB721TiersHookStore.sol +23 -17
- package/src/libraries/JB721Constants.sol +1 -1
- package/src/libraries/JB721TiersRulesetMetadataResolver.sol +1 -1
- package/src/libraries/JBBitmap.sol +1 -1
- package/src/libraries/JBIpfsDecoder.sol +1 -1
- package/src/structs/JB721Tier.sol +5 -11
- package/src/structs/JB721TierConfig.sol +5 -20
- package/src/structs/JB721TierConfigFlags.sol +26 -0
- package/src/structs/JB721TierFlags.sol +17 -0
- package/test/721HookAttacks.t.sol +22 -17
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +19 -14
- package/test/Fork.t.sol +69 -54
- package/test/TestAuditGaps.sol +73 -56
- package/test/TestSafeTransferReentrancy.t.sol +4 -4
- package/test/TestVotingUnitsLifecycle.t.sol +11 -11
- package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +10 -7
- package/test/audit/CodexSplitCreditsMismatch.t.sol +10 -7
- package/test/fork/ERC20CashOutFork.t.sol +37 -28
- package/test/fork/ERC20TierSplitFork.t.sol +28 -21
- package/test/fork/IssueTokensForSplitsFork.t.sol +10 -7
- package/test/invariants/handlers/TierLifecycleHandler.sol +10 -7
- package/test/invariants/handlers/TierStoreHandler.sol +10 -7
- package/test/regression/ProjectDeployerRulesets.t.sol +10 -7
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +6 -6
- package/test/unit/AuditFixes_Unit.t.sol +37 -28
- package/test/unit/adjustTier_Unit.t.sol +268 -202
- package/test/unit/getters_constructor_Unit.t.sol +20 -14
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +2 -2
- package/test/unit/pay_Unit.t.sol +1 -1
- package/CHANGE_LOG.md +0 -359
package/SKILLS.md
CHANGED
|
@@ -1,309 +1,42 @@
|
|
|
1
1
|
# Juicebox 721 Hook
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid and optionally allows NFT holders to burn them to reclaim project funds proportional to tier price.
|
|
6
|
-
|
|
7
|
-
## Contracts
|
|
8
|
-
|
|
9
|
-
| Contract | Role |
|
|
10
|
-
|----------|------|
|
|
11
|
-
| `JB721Hook` (abstract) | Abstract base hook: owns `DIRECTORY`, `METADATA_ID_TARGET`, `PROJECT_ID`. Implements `afterPayRecordedWith` (terminal validation + delegates to virtual `_processPayment`), `afterCashOutRecordedWith` (terminal validation, burn loop, delegates to virtual `_didBurn`), `beforeCashOutRecordedWith` (metadata decoding, delegates to virtual `cashOutWeightOf`/`totalCashOutWeight`), `beforePayRecordedWith` (default: forward weight), `hasMintPermissionFor` (returns false), `supportsInterface`, and `_initialize`. |
|
|
12
|
-
| `JB721TiersHook` | Core hook: extends `JB721Hook`. Manages tiers, reserves, credits, metadata, and discount percents. Deployed as minimal clones. Inherits `JBOwnable`, `ERC2771Context`, `JB721Hook`, `IJB721TiersHook`. Overrides `cashOutWeightOf`, `totalCashOutWeight`, `_didBurn`, `_processPayment`, and `beforePayRecordedWith` (adds tier split calculation). |
|
|
13
|
-
| `JB721TiersHookStore` | Shared singleton storage for all hook instances. Stores tiers (`JBStored721Tier`), balances, reserves, bitmaps for removed tiers, flags, and token URI resolvers. |
|
|
14
|
-
| `JB721TiersHookDeployer` | Factory: clones `JB721TiersHook` via `LibClone.clone` / `cloneDeterministic`, initializes, registers in address registry. |
|
|
15
|
-
| `JB721TiersHookProjectDeployer` | Convenience deployer: creates a Juicebox project + hook in one transaction. Also supports `launchRulesetsFor` and `queueRulesetsOf`. Wires the hook as the data hook with `useDataHookForPay: true`. |
|
|
16
|
-
| `JB721TiersHookLib` (library) | External library called via DELEGATECALL from the hook. Handles tier adjustments (`adjustTiersFor`), split amount calculation (`calculateSplitAmounts`), split fund distribution (`distributeAll`), price normalization (`normalizePaymentValue`), and token URI resolution (`resolveTokenURI`). Extracted to stay within EIP-170 contract size limit. |
|
|
17
|
-
| `IJB721Hook` (interface) | Interface for `JB721Hook`: extends `IJBRulesetDataHook`, `IJBPayHook`, `IJBCashOutHook`. Declares `DIRECTORY()`, `METADATA_ID_TARGET()`, `PROJECT_ID()`. |
|
|
18
|
-
| `ERC721` (abstract) | Clone-compatible ERC-721 with `_initialize(name, symbol)` instead of constructor args. Exposes `_setName()` and `_setSymbol()` for post-initialization updates. |
|
|
19
|
-
|
|
20
|
-
## Key Functions
|
|
21
|
-
|
|
22
|
-
| Function | Contract | What it does |
|
|
23
|
-
|----------|----------|--------------|
|
|
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
|
-
| `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, 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
|
-
| `mintPendingReservesFor(tierId, count)` | `JB721TiersHook` | Public. Mints pending reserve NFTs for a tier to the tier's `reserveBeneficiary`. Checks ruleset metadata for `mintPendingReservesPaused`. |
|
|
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 metadata fields. Empty strings leave values unchanged. Requires `SET_721_METADATA` permission. |
|
|
35
|
-
| `setDiscountPercentOf(tierId, discountPercent)` | `JB721TiersHook` | Owner-only. Sets discount percent for a tier. Requires `SET_721_DISCOUNT_PERCENT` permission. |
|
|
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 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
|
-
| `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`, `IERC721`, `IERC721Metadata`, `IERC165`. ERC-2981 support was removed. |
|
|
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 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
|
-
| `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 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
|
-
| `tiersOf(hook, categories, includeResolvedUri, startingId, size)` | `JB721TiersHookStore` | Returns an array of active tiers, optionally filtered by categories. Skips removed tiers. |
|
|
55
|
-
| `tierOf(hook, id, includeResolvedUri)` | `JB721TiersHookStore` | Returns a single tier by ID. |
|
|
56
|
-
| `tierOfTokenId(hook, tokenId, includeResolvedUri)` | `JB721TiersHookStore` | Returns the tier for a given token ID. |
|
|
57
|
-
| `totalSupplyOf(hook)` | `JB721TiersHookStore` | Returns total NFTs minted across all tiers (excluding burns). |
|
|
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
|
-
| `tierVotingUnitsOf(hook, account, tierId)` | `JB721TiersHookStore` | Returns voting units for an address within a specific tier. |
|
|
62
|
-
| `calculateSplitAmounts(store, hook, metadataIdTarget, metadata)` | `JB721TiersHookLib` | Decodes tier IDs from metadata, computes per-tier split amounts from effective price and `splitPercent`. |
|
|
63
|
-
| `convertAndCapSplitAmounts(totalSplitAmount, splitMetadata, packedPricingContext, prices, projectId, amountCurrency, amountDecimals)` | `JB721TiersHookLib` | Converts split amounts from tier pricing denomination to payment token denomination via `JBPrices`, capped at the actual payment value. |
|
|
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
|
-
|
|
69
|
-
## Integration Points
|
|
70
|
-
|
|
71
|
-
| Dependency | Import | Used For |
|
|
72
|
-
|------------|--------|----------|
|
|
73
|
-
| `@bananapus/core-v6` | `IJBDirectory`, `IJBRulesets`, `IJBPrices`, `IJBSplits`, `IJBTerminal`, `JBRuleset`, `JBRulesetMetadata`, `JBAfterPayRecordedContext`, `JBBeforeCashOutRecordedContext`, `JBSplit`, `JBSplitGroup`, `JBConstants`, etc. | Terminal validation, ruleset metadata, pricing, payment/cash-out contexts, splits |
|
|
74
|
-
| `@bananapus/ownable-v6` | `JBOwnable` | Project-based ownership for the hook (ownership can be transferred to a project NFT) |
|
|
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
|
-
| `@bananapus/address-registry-v6` | `IJBAddressRegistry` | Registering deployed hook clones |
|
|
77
|
-
| `@openzeppelin/contracts` | `ERC2771Context`, `IERC165`, `IERC721`, `SafeERC20` | Meta-transactions (trusted forwarder), interface detection, safe ERC-20 transfers for split distribution |
|
|
78
|
-
| `@prb/math` | `mulDiv` | Safe fixed-point multiplication/division for price normalization and discount/split calculation |
|
|
79
|
-
| `solady` | `LibClone` | Minimal proxy (clone) deployment for hooks |
|
|
80
|
-
|
|
81
|
-
## Key Types
|
|
82
|
-
|
|
83
|
-
| Struct/Enum | Key Fields | Used In |
|
|
84
|
-
|-------------|------------|---------|
|
|
85
|
-
| `JB721TierConfig` | `uint104 price`, `uint32 initialSupply`, `uint32 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool useReserveBeneficiaryAsDefault`, `bool transfersPausable`, `bool useVotingUnits`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent`, `uint32 splitPercent`, `JBSplit[] splits` | `adjustTiers`, `initialize`, `recordAddTiers` |
|
|
86
|
-
| `JB721Tier` | `uint32 id`, `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint104 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool transfersPausable`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent`, `uint32 splitPercent`, `string resolvedUri` | Return type from `tierOf`, `tiersOf`, `tierOfTokenId` |
|
|
87
|
-
| `JBStored721Tier` | `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint32 splitPercent`, `uint24 category`, `uint8 discountPercent`, `uint16 reserveFrequency`, `uint8 packedBools` (allowOwnerMint, transfersPausable, useVotingUnits, cannotBeRemoved, cannotIncreaseDiscountPercent) | Internal storage in `JB721TiersHookStore`. Voting units stored separately in `_tierVotingUnitsOf` when `useVotingUnits` is true. |
|
|
88
|
-
| `JB721InitTiersConfig` | `JB721TierConfig[] tiers`, `uint32 currency`, `uint8 decimals` | `initialize` -- defines tiers and pricing context. The prices contract is an immutable on the hook, not passed per-config. |
|
|
89
|
-
| `JBDeploy721TiersHookConfig` | `string name`, `string symbol`, `string baseUri`, `IJB721TokenUriResolver tokenUriResolver`, `string contractUri`, `JB721InitTiersConfig tiersConfig`, `JB721TiersHookFlags flags` | `deployHookFor`, `launchProjectFor` |
|
|
90
|
-
| `JB721TiersHookFlags` | `bool noNewTiersWithReserves`, `bool noNewTiersWithVotes`, `bool noNewTiersWithOwnerMinting`, `bool preventOverspending`, `bool issueTokensForSplits` | `initialize`, `recordFlags` |
|
|
91
|
-
| `JB721TiersRulesetMetadata` | `bool pauseTransfers`, `bool pauseMintPendingReserves` | Packed into `JBRulesetMetadata.metadata` per-ruleset (bit 0 = pauseTransfers, bit 1 = pauseMintPendingReserves) |
|
|
92
|
-
| `JBPayDataHookRulesetConfig` | `uint48 mustStartAtOrAfter`, `uint32 duration`, `uint112 weight`, `uint32 weightCutPercent`, `IJBRulesetApprovalHook approvalHook`, `JBPayDataHookRulesetMetadata metadata`, `JBSplitGroup[] splitGroups`, `JBFundAccessLimitGroup[] fundAccessLimitGroups` | `JB721TiersHookProjectDeployer` -- wraps core ruleset config with `useDataHookForPay: true` hardcoded |
|
|
93
|
-
| `JBPayDataHookRulesetMetadata` | Same as `JBRulesetMetadata` minus `useDataHookForPay` (hardcoded true) and `dataHook` (set to deployed hook). | `launchProjectFor`, `launchRulesetsFor`, `queueRulesetsOf` |
|
|
94
|
-
| `JBLaunchProjectConfig` | `string projectUri`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `JBTerminalConfig[] terminalConfigurations`, `string memo` | `launchProjectFor` |
|
|
95
|
-
| `JBLaunchRulesetsConfig` | `uint56 projectId`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `JBTerminalConfig[] terminalConfigurations`, `string memo` | `launchRulesetsFor` |
|
|
96
|
-
| `JBQueueRulesetsConfig` | `uint56 projectId`, `JBPayDataHookRulesetConfig[] rulesetConfigurations`, `string memo` | `queueRulesetsOf` |
|
|
97
|
-
| `JB721TiersMintReservesConfig` | `uint32 tierId`, `uint16 count` | `mintPendingReservesFor` batch variant |
|
|
98
|
-
| `JB721TiersSetDiscountPercentConfig` | `uint32 tierId`, `uint16 discountPercent` | `setDiscountPercentsOf` batch variant |
|
|
99
|
-
| `JBBitmapWord` | `uint256 currentWord`, `uint256 currentDepth` | Internal tier removal tracking in store |
|
|
100
|
-
|
|
101
|
-
## Constants
|
|
3
|
+
## Use This File For
|
|
102
4
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
| `DISCOUNT_DENOMINATOR` | `200` | `JB721Constants` | Max `discountPercent` value. A `discountPercent` of 200 = 100% discount (free). A `discountPercent` of 100 = 50% off. Formula: `price -= mulDiv(price, discountPercent, 200)`. |
|
|
106
|
-
| `_ONE_BILLION` | `1_000_000_000` | `JB721TiersHookStore` | Used for token ID generation: `tokenId = tierId * 1_000_000_000 + tokenNumber`. Also caps max initial supply per tier at 999,999,999. |
|
|
107
|
-
| Max tier count | `type(uint16).max` (65,535) | `JB721TiersHookStore` | Maximum total number of tiers across all `recordAddTiers` calls for a single hook. |
|
|
5
|
+
- Use this file when the task involves tiered NFT issuance, reserve minting, voting units, tier splits, or token URI resolver integration for Juicebox projects.
|
|
6
|
+
- Start here, then open the hook, store, deployer, or tests that own the exact behavior you are changing.
|
|
108
7
|
|
|
109
|
-
##
|
|
8
|
+
## Read This Next
|
|
110
9
|
|
|
111
|
-
|
|
10
|
+
| If you need... | Open this next |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Repo overview and integration model | [`README.md`](./README.md), [`ARCHITECTURE.md`](./ARCHITECTURE.md) |
|
|
13
|
+
| Runtime hook behavior | [`src/JB721TiersHook.sol`](./src/JB721TiersHook.sol), [`src/abstract/JB721Hook.sol`](./src/abstract/JB721Hook.sol) |
|
|
14
|
+
| Tier storage and accounting | [`src/JB721TiersHookStore.sol`](./src/JB721TiersHookStore.sol) |
|
|
15
|
+
| Deployment or project launch helpers | [`src/JB721TiersHookDeployer.sol`](./src/JB721TiersHookDeployer.sol), [`src/JB721TiersHookProjectDeployer.sol`](./src/JB721TiersHookProjectDeployer.sol), [`script/Deploy.s.sol`](./script/Deploy.s.sol) |
|
|
16
|
+
| Shared libraries, interfaces, and resolver surface | [`src/libraries/`](./src/libraries/), [`src/interfaces/`](./src/interfaces/), [`src/structs/`](./src/structs/) |
|
|
17
|
+
| Invariants, E2E flows, and regressions | [`test/invariants/`](./test/invariants/), [`test/E2E/`](./test/E2E/), [`test/regression/`](./test/regression/), [`test/TestVotingUnitsLifecycle.t.sol`](./test/TestVotingUnitsLifecycle.t.sol) |
|
|
112
18
|
|
|
113
|
-
|
|
114
|
-
- `DISCOUNT_DENOMINATOR` is 200, so `discountPercent = 100` means 50% off, `discountPercent = 200` means free.
|
|
115
|
-
- Discount can be changed via `setDiscountPercentOf` / `setDiscountPercentsOf` (requires `SET_721_DISCOUNT_PERCENT` permission).
|
|
116
|
-
- If `cannotIncreaseDiscountPercent` is set on the tier, the discount can only be decreased or kept the same -- increases are rejected by the store.
|
|
117
|
-
- Cash out weight always uses the **original tier price**, not the discounted price. This prevents discount changes from retroactively altering the cash-out value of already-minted NFTs.
|
|
19
|
+
## Repo Map
|
|
118
20
|
|
|
119
|
-
|
|
21
|
+
| Area | Where to look |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Main contracts | [`src/`](./src/) |
|
|
24
|
+
| Abstract bases, interfaces, structs, and libraries | [`src/abstract/`](./src/abstract/), [`src/interfaces/`](./src/interfaces/), [`src/structs/`](./src/structs/), [`src/libraries/`](./src/libraries/) |
|
|
25
|
+
| Scripts | [`script/`](./script/) |
|
|
26
|
+
| Tests | [`test/`](./test/) |
|
|
120
27
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
- If `useVotingUnits` is `true` on the tier config, voting power per NFT is the custom `votingUnits` value (stored in `_tierVotingUnitsOf`).
|
|
124
|
-
- If `useVotingUnits` is `false`, voting power per NFT defaults to the tier's `price`.
|
|
125
|
-
- The `noNewTiersWithVotes` flag blocks adding new tiers with any voting power -- this means blocking tiers where `(useVotingUnits && votingUnits != 0)` OR `(!useVotingUnits && price != 0)`.
|
|
126
|
-
- Total voting units for an address are computed by `votingUnitsOf(hook, account)`, which sums `balance * votingPower` across all tiers.
|
|
127
|
-
|
|
128
|
-
## Reserve Minting
|
|
129
|
-
|
|
130
|
-
- Reserves accumulate as NFTs are purchased: for every `reserveFrequency` non-reserve mints, one reserve NFT becomes available.
|
|
131
|
-
- Pending count: `ceil(numberOfNonReserveMints / reserveFrequency) - numberOfReservesMintedFor`.
|
|
132
|
-
- Reserves are minted to the tier's `reserveBeneficiary` (or the hook's `defaultReserveBeneficiaryOf` as fallback).
|
|
133
|
-
- Reserve minting is permissionless (`mintPendingReservesFor`), but can be paused per-ruleset via `pauseMintPendingReserves` in `JB721TiersRulesetMetadata`.
|
|
134
|
-
- Supply is protected: `recordMint` ensures remaining supply covers pending reserves after each purchase.
|
|
135
|
-
- Tiers with `allowOwnerMint: true` cannot have a `reserveFrequency` -- the store rejects this combination.
|
|
136
|
-
|
|
137
|
-
## Tier Splits
|
|
138
|
-
|
|
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
|
-
- Split recipients are stored in `JBSplits` using group IDs computed as `uint256(uint160(hookAddress)) | (uint256(tierId) << 160)`.
|
|
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` 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, `convertAndCapSplitAmounts` converts amounts to the payment token denomination via `JBPrices` and caps the total at the actual payment value.
|
|
148
|
-
- **Pay credits cap**: `totalSplitAmount` is capped at `context.amount.value` before weight calculation and before being returned in the hook specification. Pay credits fund NFT minting (virtual -- they reduce the price threshold in `recordMint`), but splits require real tokens to distribute. Without this cap, a payer with sufficient pay credits but insufficient actual payment value would cause the terminal to attempt forwarding more tokens than were actually received, reverting the transaction. This means the project receiving the NFT payment must have received the full amount including what would have gone to splits -- split recipients are paid from the project's balance, not directly from the payer's contribution.
|
|
149
|
-
- 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.
|
|
150
|
-
- 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`.
|
|
151
|
-
- **Leftover accounting in `_distributeSingleSplit`**: `leftoverAmount` is always decremented **before** the send attempt. If `_sendPayoutToSplit` returns `false` (i.e., the send reverted or had no valid recipient), the failed amount is accumulated in a separate variable (not added back to `leftoverAmount`). After the loop completes, accumulated failed amounts are added to `leftoverAmount` and routed to the project's balance. This "decrement-first, accumulate-failures-separately" pattern uses proportional redistribution: a failed split does not inflate subsequent split recipients' shares because the failed amount is withheld from `leftoverAmount` during the loop, and `leftoverPercentage` still decreases regardless of success.
|
|
152
|
-
- Split recipients follow the same priority chain as `JBMultiTerminal`: `split.hook` > `split.projectId` > `split.beneficiary`:
|
|
153
|
-
- **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`.
|
|
154
|
-
- **Project splits**: route via `terminal.pay` or `terminal.addToBalance`.
|
|
155
|
-
- **Beneficiary splits**: direct ETH transfer or `SafeERC20.safeTransfer`.
|
|
156
|
-
- **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.
|
|
157
|
-
- All external calls in `_sendPayoutToSplit` are wrapped in try-catch to prevent a single reverting recipient from bricking all payments to the project:
|
|
158
|
-
- **Native token hooks**: a revert returns `false` (ETH stays in the contract and routes to the project balance).
|
|
159
|
-
- **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 this contract. Since leftoverAmount was already decremented before the send, returning `true` is required to prevent the caller from adding the amount back -- which would create a double-spend (tokens sent to the hook AND counted toward leftover routed to the project).
|
|
160
|
-
- **ERC-20 terminal calls** (`pay`/`addToBalanceOf`): approval is reset to zero on failure to prevent dangling approvals.
|
|
161
|
-
|
|
162
|
-
## Gotchas
|
|
163
|
-
|
|
164
|
-
- `JB721TiersHook` is deployed as a **minimal clone** (not a full deployment). The constructor sets immutables (`PRICES`, `RULESETS`, `STORE`, `SPLITS`, `DIRECTORY`, `METADATA_ID_TARGET`), and `initialize()` sets per-instance state. Calling `initialize()` twice reverts with `JB721TiersHook_AlreadyInitialized`.
|
|
165
|
-
- **`JB721Hook` abstract base**: `JB721TiersHook` extends `JB721Hook`, which handles generic 721 hook lifecycle (terminal validation, burn loop, metadata decoding). `JB721TiersHook` overrides `cashOutWeightOf`, `totalCashOutWeight`, `_didBurn`, `_processPayment`, and `beforePayRecordedWith`. Errors like `JB721Hook_InvalidPay` and `JB721Hook_InvalidCashOut` are defined on the abstract class, not `JB721TiersHook`.
|
|
166
|
-
- **Pricing context is bit-packed** into a single `uint256`: currency (bits 0-31) and decimals (bits 32-39). The prices contract is the `PRICES` immutable (set in constructor). Read pricing context via `pricingContext()`.
|
|
167
|
-
- **Pricing decimals must be <= 18**: `initialize` reverts with `JB721TiersHook_InvalidPricingDecimals` otherwise.
|
|
168
|
-
- **Token IDs encode tier ID**: `tokenId = tierId * 1_000_000_000 + mintNumber`. Use `STORE.tierIdOfToken(tokenId)` to extract the tier ID.
|
|
169
|
-
- **Pay credits**: If a payer overpays (amount > total tier prices), the excess is stored as `payCreditsOf[beneficiary]` and can be applied to future mints. This only works when `preventOverspending` flag is `false`. Credits are only combined with payment when `payer == beneficiary`.
|
|
170
|
-
- **Cash outs reject fungible tokens**: `beforeCashOutRecordedWith` reverts with `JB721TiersHook_UnexpectedTokenCashedOut` if `context.cashOutCount > 0`. NFT cash outs and fungible token cash outs are mutually exclusive.
|
|
171
|
-
- **Cash out weight uses original price**: `cashOutWeightOf` and `totalCashOutWeight` use the full tier `price`, not the discounted price. This prevents discount changes from altering the cash-out value of already-minted NFTs.
|
|
172
|
-
- **Pending reserves inflate totalCashOutWeight**: `totalCashOutWeight` includes pending reserves in the denominator (`price * (minted + pendingReserves)`). This dilutes cash-out value before reserves are minted, preventing early cashers from extracting more than their fair share.
|
|
173
|
-
- **Reserve minting is permissionless** but governed by ruleset metadata. Anyone can call `mintPendingReservesFor` as long as `mintPendingReservesPaused` is not set in the current ruleset's metadata.
|
|
174
|
-
- **Reserve + owner-mint mutual exclusion**: Tiers with `allowOwnerMint: true` cannot have a `reserveFrequency`. The store rejects this combination during `recordAddTiers`.
|
|
175
|
-
- `setMetadata` accepts `name` and `symbol` as the first two parameters. Empty strings leave the current values unchanged.
|
|
176
|
-
- `setMetadata` uses `address(this)` as the sentinel for "no change" on `tokenUriResolver` (not `address(0)`). Passing `address(0)` will clear the resolver.
|
|
177
|
-
- `JBPayDataHookRulesetConfig` hardcodes `useDataHookForPay: true` when wiring rulesets through the project deployer. All other metadata fields are passed through.
|
|
178
|
-
- 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.
|
|
179
|
-
- **ERC-2981 not supported**: ERC-2981 royalty support was removed. `supportsInterface` returns `false` for `IERC2981`, and no `royaltyInfo` function exists.
|
|
180
|
-
- **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)`.
|
|
181
|
-
- **`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.
|
|
182
|
-
- **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.
|
|
183
|
-
- `JB721TiersHookStore` is a **shared singleton** -- all hook instances on the same chain use the same store, keyed by `address(hook)`.
|
|
184
|
-
- 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`).
|
|
185
|
-
- **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.
|
|
186
|
-
- **`hasMintPermissionFor` always returns `false`**: The hook never grants mint permission to any address. This is part of the `IJBRulesetDataHook` interface.
|
|
187
|
-
- **Max tier count is 65,535** (`type(uint16).max`). Adding tiers beyond this limit reverts.
|
|
188
|
-
- **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.
|
|
189
|
-
- **`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).
|
|
190
|
-
- **`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.
|
|
191
|
-
- **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.
|
|
192
|
-
- **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.
|
|
193
|
-
|
|
194
|
-
## Custom Errors
|
|
195
|
-
|
|
196
|
-
| Error | Contract | When |
|
|
197
|
-
|-------|----------|------|
|
|
198
|
-
| `JB721Hook_InvalidCashOut()` | `JB721Hook` | `afterCashOutRecordedWith` caller is not a project terminal. |
|
|
199
|
-
| `JB721Hook_InvalidPay()` | `JB721Hook` | `afterPayRecordedWith` caller is not a project terminal. |
|
|
200
|
-
| `JB721Hook_UnauthorizedToken(tokenId, holder)` | `JB721Hook` | Cash out attempts to burn a token not owned by `context.holder`. |
|
|
201
|
-
| `JB721Hook_UnexpectedTokenCashedOut()` | `JB721Hook` | `beforeCashOutRecordedWith` called with `cashOutCount > 0` (fungible tokens mixed with NFT cash out). |
|
|
202
|
-
| `JB721TiersHook_AlreadyInitialized(projectId)` | `JB721TiersHook` | `initialize` called on a hook where `_initialized` is already true. |
|
|
203
|
-
| `JB721TiersHook_CurrencyMismatch(paymentCurrency, tierCurrency)` | `JB721TiersHook` | Payment currency differs from tier pricing currency and no `PRICES` contract is configured for conversion. |
|
|
204
|
-
| `JB721TiersHook_InvalidPricingDecimals(decimals)` | `JB721TiersHook` | `initialize` called with `decimals > 18`. |
|
|
205
|
-
| `JB721TiersHook_MintReserveNftsPaused()` | `JB721TiersHook` | `mintPendingReservesFor` called while `pauseMintPendingReserves` is set in the current ruleset metadata. |
|
|
206
|
-
| `JB721TiersHook_NoProjectId()` | `JB721TiersHook` | `initialize` called with `projectId == 0`. |
|
|
207
|
-
| `JB721TiersHook_Overspending(leftoverAmount)` | `JB721TiersHook` | Payment has leftover funds after minting and `preventOverspending` flag is set. |
|
|
208
|
-
| `JB721TiersHook_TierTransfersPaused()` | `JB721TiersHook` | NFT transfer attempted on a tier with `transfersPausable` while `transfersPaused` is set in ruleset metadata. |
|
|
209
|
-
| `JB721TiersHookStore_CantMintManually(tierId)` | `JB721TiersHookStore` | Owner mint attempted on a tier with `allowOwnerMint: false`. |
|
|
210
|
-
| `JB721TiersHookStore_CantRemoveTier(tierId)` | `JB721TiersHookStore` | Removing a tier that has `cannotBeRemoved: true`. |
|
|
211
|
-
| `JB721TiersHookStore_DiscountPercentExceedsBounds(percent, limit)` | `JB721TiersHookStore` | Discount percent exceeds `DISCOUNT_DENOMINATOR` (200). |
|
|
212
|
-
| `JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(percent, storedPercent)` | `JB721TiersHookStore` | Increasing discount on a tier with `cannotIncreaseDiscountPercent: true`. |
|
|
213
|
-
| `JB721TiersHookStore_InsufficientPendingReserves(count, numberOfPendingReserves)` | `JB721TiersHookStore` | `recordMintReservesFor` called with `count` exceeding available pending reserves. |
|
|
214
|
-
| `JB721TiersHookStore_InsufficientSupplyRemaining(tierId)` | `JB721TiersHookStore` | Tier has no remaining supply, or remaining supply does not cover pending reserves after mint. |
|
|
215
|
-
| `JB721TiersHookStore_InvalidCategorySortOrder(tierCategory, previousTierCategory)` | `JB721TiersHookStore` | Tiers not sorted in ascending `category` order during `recordAddTiers`. |
|
|
216
|
-
| `JB721TiersHookStore_InvalidQuantity(quantity, limit)` | `JB721TiersHookStore` | Tier `initialSupply` exceeds max (999,999,999). |
|
|
217
|
-
| `JB721TiersHookStore_ManualMintingNotAllowed(tierId)` | `JB721TiersHookStore` | Adding a tier with `allowOwnerMint: true` when `noNewTiersWithOwnerMinting` flag is set. |
|
|
218
|
-
| `JB721TiersHookStore_MaxTiersExceeded(numberOfTiers, limit)` | `JB721TiersHookStore` | Total tier count exceeds `type(uint16).max` (65,535). |
|
|
219
|
-
| `JB721TiersHookStore_PriceExceedsAmount(price, leftoverAmount)` | `JB721TiersHookStore` | Tier price exceeds remaining payment amount during `recordMint`. |
|
|
220
|
-
| `JB721TiersHookStore_ReserveFrequencyNotAllowed(tierId)` | `JB721TiersHookStore` | Adding a tier with `reserveFrequency` when `noNewTiersWithReserves` flag is set. |
|
|
221
|
-
| `JB721TiersHookStore_SplitPercentExceedsBounds(percent, limit)` | `JB721TiersHookStore` | Tier `splitPercent` exceeds `SPLITS_TOTAL_PERCENT`. |
|
|
222
|
-
| `JB721TiersHookStore_TierRemoved(tierId)` | `JB721TiersHookStore` | Attempting to mint from a tier that has been removed. |
|
|
223
|
-
| `JB721TiersHookStore_UnrecognizedTier(tierId)` | `JB721TiersHookStore` | Tier ID does not exist (`initialSupply == 0`). |
|
|
224
|
-
| `JB721TiersHookStore_VotingUnitsNotAllowed(tierId)` | `JB721TiersHookStore` | Adding a tier with voting power when `noNewTiersWithVotes` flag is set. |
|
|
225
|
-
| `JB721TiersHookStore_ZeroInitialSupply(tierId)` | `JB721TiersHookStore` | Adding a tier with `initialSupply == 0`. |
|
|
226
|
-
|
|
227
|
-
## Events
|
|
28
|
+
## Purpose
|
|
228
29
|
|
|
229
|
-
|
|
230
|
-
|-------|----------|------------|
|
|
231
|
-
| `AddToBalanceReverted(projectId, token, amount, reason)` | `IJB721TiersHook` | Emitted when leftover `addToBalanceOf` call reverts during split distribution. Funds remain stranded in hook. |
|
|
232
|
-
| `AddPayCredits(amount, newTotalCredits, account, caller)` | `IJB721TiersHook` | Pay credits added for an account (overspending stored for future mints). |
|
|
233
|
-
| `AddTier(tierId, tier, caller)` | `IJB721TiersHook` | New tier added via `adjustTiers`. `tier` is the full `JB721TierConfig`. |
|
|
234
|
-
| `Mint(tokenId, tierId, beneficiary, totalAmountPaid, caller)` | `IJB721TiersHook` | NFT minted from a payment. |
|
|
235
|
-
| `MintReservedNft(tokenId, tierId, beneficiary, caller)` | `IJB721TiersHook` | Reserve NFT minted via `mintPendingReservesFor`. |
|
|
236
|
-
| `RemoveTier(tierId, caller)` | `IJB721TiersHook` | Tier removed via `adjustTiers`. |
|
|
237
|
-
| `SetName(name, caller)` | `IJB721TiersHook` | Collection name updated via `setMetadata`. |
|
|
238
|
-
| `SetSymbol(symbol, caller)` | `IJB721TiersHook` | Collection symbol updated via `setMetadata`. |
|
|
239
|
-
| `SetBaseUri(baseUri, caller)` | `IJB721TiersHook` | Base URI updated via `setMetadata`. |
|
|
240
|
-
| `SetContractUri(uri, caller)` | `IJB721TiersHook` | Contract URI updated via `setMetadata`. |
|
|
241
|
-
| `SetDiscountPercent(tierId, discountPercent, caller)` | `IJB721TiersHook` | Tier discount percent changed via `setDiscountPercentOf`. |
|
|
242
|
-
| `SetEncodedIPFSUri(tierId, encodedUri, caller)` | `IJB721TiersHook` | Tier IPFS URI updated via `setMetadata`. |
|
|
243
|
-
| `SetTokenUriResolver(resolver, caller)` | `IJB721TiersHook` | Token URI resolver updated via `setMetadata`. |
|
|
244
|
-
| `SplitPayoutReverted(projectId, split, amount, reason, caller)` | `IJB721TiersHook` | A split payout reverted during distribution. Failed split's funds route to project balance. |
|
|
245
|
-
| `UsePayCredits(amount, newTotalCredits, account, caller)` | `IJB721TiersHook` | Pay credits consumed during a payment. |
|
|
246
|
-
| `CleanTiers(hook, caller)` | `JB721TiersHookStore` | Removed tiers cleaned from the sorting linked list. |
|
|
247
|
-
| `SetDefaultReserveBeneficiary(hook, newBeneficiary, caller)` | `JB721TiersHookStore` | Default reserve beneficiary changed (affects all tiers without a tier-specific beneficiary). |
|
|
248
|
-
| `HookDeployed(projectId, hook, caller)` | `JB721TiersHookDeployer` | New hook clone deployed for a project. |
|
|
30
|
+
Tiered ERC-721 NFT issuance and cash-out hook for Juicebox V6. This repo controls tier pricing, reserve issuance, voting units, split forwarding, and deployer flows for projects that mint NFTs on pay.
|
|
249
31
|
|
|
250
|
-
##
|
|
32
|
+
## Reference Files
|
|
251
33
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
|
|
255
|
-
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
256
|
-
import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
|
|
257
|
-
import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721TiersHookFlags.sol";
|
|
258
|
-
import {JBLaunchProjectConfig} from "@bananapus/721-hook-v6/src/structs/JBLaunchProjectConfig.sol";
|
|
259
|
-
import {JBPayDataHookRulesetConfig} from "@bananapus/721-hook-v6/src/structs/JBPayDataHookRulesetConfig.sol";
|
|
260
|
-
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
34
|
+
- Open [`references/runtime.md`](./references/runtime.md) when you need the contract roles, payment and cash-out path, reserve math, or the main invariants that should hold while editing.
|
|
35
|
+
- Open [`references/operations.md`](./references/operations.md) when you need deployer behavior, metadata and permission surfaces, test breadcrumbs, or the common failure modes to verify before shipping.
|
|
261
36
|
|
|
262
|
-
|
|
263
|
-
JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
|
|
264
|
-
tiers[0] = JB721TierConfig({
|
|
265
|
-
price: 0.1 ether,
|
|
266
|
-
initialSupply: 100,
|
|
267
|
-
votingUnits: 0,
|
|
268
|
-
reserveFrequency: 0,
|
|
269
|
-
reserveBeneficiary: address(0),
|
|
270
|
-
encodedIPFSUri: 0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89, // example CID
|
|
271
|
-
category: 1,
|
|
272
|
-
discountPercent: 0,
|
|
273
|
-
allowOwnerMint: false,
|
|
274
|
-
useReserveBeneficiaryAsDefault: false,
|
|
275
|
-
transfersPausable: false,
|
|
276
|
-
useVotingUnits: false,
|
|
277
|
-
cannotBeRemoved: false,
|
|
278
|
-
cannotIncreaseDiscountPercent: false,
|
|
279
|
-
splitPercent: 0,
|
|
280
|
-
splits: new JBSplit[](0)
|
|
281
|
-
});
|
|
37
|
+
## Working Rules
|
|
282
38
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
name: "My NFT Collection",
|
|
288
|
-
symbol: "MNFT",
|
|
289
|
-
baseUri: "ipfs://",
|
|
290
|
-
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
291
|
-
contractUri: "",
|
|
292
|
-
tiersConfig: JB721InitTiersConfig({
|
|
293
|
-
tiers: tiers,
|
|
294
|
-
currency: 1, // ETH
|
|
295
|
-
decimals: 18
|
|
296
|
-
}),
|
|
297
|
-
flags: JB721TiersHookFlags({
|
|
298
|
-
noNewTiersWithReserves: false,
|
|
299
|
-
noNewTiersWithVotes: false,
|
|
300
|
-
noNewTiersWithOwnerMinting: false,
|
|
301
|
-
preventOverspending: false,
|
|
302
|
-
issueTokensForSplits: false
|
|
303
|
-
})
|
|
304
|
-
}),
|
|
305
|
-
launchProjectConfig: launchConfig, // JBLaunchProjectConfig with rulesets, terminals, etc.
|
|
306
|
-
controller: IJBController(controllerAddress),
|
|
307
|
-
salt: bytes32(0)
|
|
308
|
-
});
|
|
309
|
-
```
|
|
39
|
+
- Start in [`src/JB721TiersHook.sol`](./src/JB721TiersHook.sol) for pay and cash-out behavior, but verify storage-side assumptions in [`src/JB721TiersHookStore.sol`](./src/JB721TiersHookStore.sol) before changing mint, burn, reserve, or supply logic.
|
|
40
|
+
- Treat tier splits, reserve minting, and discounted pricing as treasury-sensitive. Check both runtime code and regression coverage before assuming a change is local.
|
|
41
|
+
- When a task mentions token metadata or rendering, confirm whether the behavior lives in this repo or in an external resolver. Do not over-edit the hook when the real change belongs downstream.
|
|
42
|
+
- When changing deployers or initialization, verify the hook, store, and project-launch path stay aligned. These flows are tightly coupled.
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -26,8 +26,8 @@ pragma solidity 0.8.28;
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|
|
28
28
|
|
|
29
|
-
// Libraries —
|
|
30
|
-
pragma solidity
|
|
29
|
+
// Libraries — pin to exact version like contracts
|
|
30
|
+
pragma solidity 0.8.28;
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Imports
|
|
@@ -86,12 +86,20 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
86
86
|
|
|
87
87
|
uint256 internal constant _FEE_BENEFICIARY_PROJECT_ID = 1;
|
|
88
88
|
|
|
89
|
+
//*********************************************************************//
|
|
90
|
+
// ------------------------ private constants ------------------------ //
|
|
91
|
+
//*********************************************************************//
|
|
92
|
+
|
|
89
93
|
//*********************************************************************//
|
|
90
94
|
// --------------- public immutable stored properties ---------------- //
|
|
91
95
|
//*********************************************************************//
|
|
92
96
|
|
|
93
97
|
IJBDirectory public immutable override DIRECTORY;
|
|
94
98
|
|
|
99
|
+
//*********************************************************************//
|
|
100
|
+
// -------------- internal immutable stored properties -------------- //
|
|
101
|
+
//*********************************************************************//
|
|
102
|
+
|
|
95
103
|
//*********************************************************************//
|
|
96
104
|
// --------------------- public stored properties -------------------- //
|
|
97
105
|
//*********************************************************************//
|
|
@@ -100,10 +108,26 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
100
108
|
// -------------------- internal stored properties ------------------- //
|
|
101
109
|
//*********************************************************************//
|
|
102
110
|
|
|
111
|
+
//*********************************************************************//
|
|
112
|
+
// -------------------- private stored properties -------------------- //
|
|
113
|
+
//*********************************************************************//
|
|
114
|
+
|
|
115
|
+
//*********************************************************************//
|
|
116
|
+
// ------------------- transient stored properties ------------------- //
|
|
117
|
+
//*********************************************************************//
|
|
118
|
+
|
|
103
119
|
//*********************************************************************//
|
|
104
120
|
// -------------------------- constructor ---------------------------- //
|
|
105
121
|
//*********************************************************************//
|
|
106
122
|
|
|
123
|
+
//*********************************************************************//
|
|
124
|
+
// ---------------------------- modifiers ---------------------------- //
|
|
125
|
+
//*********************************************************************//
|
|
126
|
+
|
|
127
|
+
//*********************************************************************//
|
|
128
|
+
// ------------------------- receive / fallback ---------------------- //
|
|
129
|
+
//*********************************************************************//
|
|
130
|
+
|
|
107
131
|
//*********************************************************************//
|
|
108
132
|
// ---------------------- external transactions ---------------------- //
|
|
109
133
|
//*********************************************************************//
|
|
@@ -112,10 +136,18 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
112
136
|
// ----------------------- external views ---------------------------- //
|
|
113
137
|
//*********************************************************************//
|
|
114
138
|
|
|
139
|
+
//*********************************************************************//
|
|
140
|
+
// -------------------------- public views --------------------------- //
|
|
141
|
+
//*********************************************************************//
|
|
142
|
+
|
|
115
143
|
//*********************************************************************//
|
|
116
144
|
// ----------------------- public transactions ----------------------- //
|
|
117
145
|
//*********************************************************************//
|
|
118
146
|
|
|
147
|
+
//*********************************************************************//
|
|
148
|
+
// ---------------------- internal transactions ---------------------- //
|
|
149
|
+
//*********************************************************************//
|
|
150
|
+
|
|
119
151
|
//*********************************************************************//
|
|
120
152
|
// ----------------------- internal helpers -------------------------- //
|
|
121
153
|
//*********************************************************************//
|
|
@@ -134,17 +166,28 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
134
166
|
1. Custom errors
|
|
135
167
|
2. Public constants
|
|
136
168
|
3. Internal constants
|
|
137
|
-
4.
|
|
138
|
-
5.
|
|
139
|
-
6.
|
|
140
|
-
7.
|
|
141
|
-
8.
|
|
142
|
-
9.
|
|
143
|
-
10.
|
|
144
|
-
11.
|
|
145
|
-
12.
|
|
146
|
-
13.
|
|
147
|
-
14.
|
|
169
|
+
4. Private constants
|
|
170
|
+
5. Public immutable stored properties
|
|
171
|
+
6. Internal immutable stored properties
|
|
172
|
+
7. Public stored properties
|
|
173
|
+
8. Internal stored properties
|
|
174
|
+
9. Private stored properties
|
|
175
|
+
10. Transient stored properties
|
|
176
|
+
11. Constructor
|
|
177
|
+
12. Modifiers
|
|
178
|
+
13. Receive / fallback
|
|
179
|
+
14. External transactions
|
|
180
|
+
15. External views
|
|
181
|
+
16. Public views
|
|
182
|
+
17. Public transactions
|
|
183
|
+
18. Internal transactions
|
|
184
|
+
19. Internal helpers
|
|
185
|
+
20. Internal views
|
|
186
|
+
21. Private helpers
|
|
187
|
+
|
|
188
|
+
Use these additional section labels where they better match the contents of the block:
|
|
189
|
+
- `internal functions` is accepted as equivalent to `internal helpers`
|
|
190
|
+
- `events` and `structs` are acceptable in specialized contracts that define them explicitly
|
|
148
191
|
|
|
149
192
|
Functions are alphabetized within each section.
|
|
150
193
|
|
|
@@ -197,7 +240,7 @@ interface IJBExample is IJBBase {
|
|
|
197
240
|
| Public/external function | `camelCase` | `cashOutTokensOf` |
|
|
198
241
|
| Internal/private function | `_camelCase` | `_processFee` |
|
|
199
242
|
| Internal storage | `_camelCase` | `_accountingContextForTokenOf` |
|
|
200
|
-
| Function parameter | `camelCase` | `projectId`, `cashOutCount` |
|
|
243
|
+
| Function parameter | `camelCase` (no underscores) | `projectId`, `cashOutCount` |
|
|
201
244
|
|
|
202
245
|
## NatSpec
|
|
203
246
|
|
|
@@ -565,7 +608,3 @@ CI checks formatting via `forge fmt --check`.
|
|
|
565
608
|
### Contract Size Checks
|
|
566
609
|
|
|
567
610
|
CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
|
|
568
|
-
|
|
569
|
-
## Repo-Specific Deviations
|
|
570
|
-
|
|
571
|
-
None. This repo follows the standard configuration exactly.
|