@bananapus/core-v6 0.0.30 → 0.0.31

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.
Files changed (44) hide show
  1. package/ADMINISTRATION.md +43 -13
  2. package/ARCHITECTURE.md +62 -137
  3. package/AUDIT_INSTRUCTIONS.md +149 -428
  4. package/CHANGELOG.md +73 -0
  5. package/README.md +90 -201
  6. package/RISKS.md +27 -12
  7. package/SKILLS.md +31 -441
  8. package/STYLE_GUIDE.md +52 -19
  9. package/USER_JOURNEYS.md +76 -627
  10. package/package.json +1 -2
  11. package/references/entrypoints.md +160 -0
  12. package/references/types-errors-events.md +297 -0
  13. package/script/Deploy.s.sol +7 -2
  14. package/script/DeployPeriphery.s.sol +51 -4
  15. package/src/JBController.sol +11 -3
  16. package/src/JBDirectory.sol +1 -0
  17. package/src/JBMultiTerminal.sol +126 -72
  18. package/src/JBRulesets.sol +2 -1
  19. package/src/JBTerminalStore.sol +22 -11
  20. package/src/abstract/JBControlled.sol +7 -1
  21. package/src/abstract/JBPermissioned.sol +1 -1
  22. package/src/interfaces/IJBRulesetDataHook.sol +5 -4
  23. package/src/libraries/JBCashOuts.sol +1 -1
  24. package/src/libraries/JBConstants.sol +1 -1
  25. package/src/libraries/JBCurrencyIds.sol +1 -1
  26. package/src/libraries/JBFees.sol +1 -1
  27. package/src/libraries/JBFixedPointNumber.sol +1 -1
  28. package/src/libraries/JBMetadataResolver.sol +1 -1
  29. package/src/libraries/JBPayoutSplitGroupLib.sol +3 -1
  30. package/src/libraries/JBRulesetMetadataResolver.sol +1 -1
  31. package/src/libraries/JBSplitGroupIds.sol +1 -1
  32. package/src/libraries/JBSurplus.sol +1 -1
  33. package/src/structs/JBSplit.sol +4 -1
  34. package/test/TestForwardedTokenConsumption.sol +418 -0
  35. package/test/audit/CycledSurplusAllowanceReset.t.sol +184 -0
  36. package/test/units/static/JBController/TestPreviewMintOf.sol +5 -3
  37. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +15 -11
  38. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +6 -0
  39. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +3 -0
  40. package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +3 -0
  41. package/test/units/static/JBMultiTerminal/TestPay.sol +7 -15
  42. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +1 -1
  43. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +1 -1
  44. package/CHANGE_LOG.md +0 -479
package/USER_JOURNEYS.md CHANGED
@@ -1,668 +1,117 @@
1
- # nana-core-v6 -- User Journeys
1
+ # User Journeys
2
2
 
3
- All user paths through the Juicebox V6 core protocol. For each journey: entry point, key parameters, state changes, events, and edge cases.
3
+ ## Who This Repo Serves
4
4
 
5
- ---
5
+ - founders launching and evolving Juicebox projects
6
+ - supporters paying projects in the asset a terminal accepts
7
+ - token holders cashing out against project surplus
8
+ - operators managing permissions, splits, fund access limits, and rulesets
9
+ - integrators wiring hooks, terminals, price feeds, and migrations into the canonical protocol surface
6
10
 
7
- ## 1. Launch Project
11
+ ## Journey 1: Launch A Project With The Right Initial Shape
8
12
 
9
- **Entry point**: `JBController.launchProjectFor(address owner, string projectUri, JBRulesetConfig[] rulesetConfigurations, JBTerminalConfig[] terminalConfigurations, string memo)`
13
+ **Starting state:** you know who should own the project, which terminals it should use, and what the first ruleset should allow.
10
14
 
11
- **Who can call**: Anyone. The project ERC-721 is minted to the specified `owner`.
15
+ **Success:** the project NFT exists, the initial ruleset is active, accepted terminals are installed, and downstream hooks or splits can begin working immediately.
12
16
 
13
- **Parameters**:
14
- - `owner` -- Address that receives the project ERC-721 NFT
15
- - `projectUri` -- Metadata URI (typically IPFS hash)
16
- - `rulesetConfigurations` -- Array of `JBRulesetConfig` structs defining the project's economic rules
17
- - `terminalConfigurations` -- Array of `JBTerminalConfig` structs specifying which terminals accept which tokens
18
- - `memo` -- Arbitrary string emitted in the event
17
+ **Flow**
18
+ 1. Call `JBController.launchProjectFor(...)` with the owner, URI, ruleset config, terminal configs, and any split or hook metadata.
19
+ 2. `JBProjects` mints the project NFT, `JBDirectory` records controller and terminal routing, and `JBRulesets` stores the first ruleset.
20
+ 3. If the project wants ERC-20 tokens, reserved-rate behavior, or hook-driven behavior, that configuration is committed at launch instead of being inferred later.
21
+ 4. The project can now accept payments and queue future rulesets without changing project identity.
19
22
 
20
- **State changes**:
21
- 1. `JBProjects.createFor(owner)` -- Mints ERC-721, increments project count, returns `projectId`
22
- 2. `uriOf[projectId] = projectUri` -- Stores metadata URI (if non-empty)
23
- 3. `JBDirectory.setControllerOf(projectId, controller)` -- Sets this controller as the project's controller
24
- 4. For each terminal config: `terminal.addAccountingContextsFor(projectId, contexts)` -- Registers accepted tokens
25
- 5. `JBDirectory.setTerminalsOf(projectId, terminals)` -- Registers terminals
26
- 6. For each ruleset config:
27
- - `JBRulesets.queueFor(...)` -- Creates ruleset with packed intrinsic/user properties and metadata
28
- - `JBSplits.setSplitGroupsOf(...)` -- Stores split groups for the ruleset
29
- - `JBFundAccessLimits.setFundAccessLimitsFor(...)` -- Stores payout limits and surplus allowances
23
+ **Failure cases that matter:** mismatched accounting contexts, wrong terminals for the target asset, invalid hook metadata, and launching with permissions or ownership assumptions that cannot be repaired cleanly later.
30
24
 
31
- **Events**: `LaunchProject(rulesetId, projectId, projectUri, memo, caller)`
25
+ ## Journey 2: Accept A Payment And Issue The Right Token Exposure
32
26
 
33
- **Edge cases**:
34
- - Empty `rulesetConfigurations` array is valid -- project launches with no rulesets (cannot receive payments until rulesets are launched via `launchRulesetsFor`)
35
- - Multiple rulesets can be queued in a single launch -- they form a linked list
36
- - If `block.timestamp` collision occurs for rulesetId, the ID is incremented by 1
27
+ **Starting state:** the project has an active ruleset and a terminal that accepts the payer's asset.
37
28
 
38
- ---
29
+ **Success:** treasury balances increase, hooks run in the right order, and the beneficiary receives credits or ERC-20 tokens consistent with the ruleset.
39
30
 
40
- ## 2. Pay a Project
31
+ **Flow**
32
+ 1. A payer calls `pay(...)` on `JBMultiTerminal`.
33
+ 2. The terminal validates the accounting context, records funds, and asks `JBTerminalStore` to derive issuance from the active ruleset.
34
+ 3. `JBController` and `JBTokens` decide whether the beneficiary gets project token credits, ERC-20s, or no issuance because weight is zero.
35
+ 4. Any configured pay hooks or data hooks run around the accounting path.
41
36
 
42
- **Entry point**: `JBMultiTerminal.pay(uint256 projectId, address token, uint256 amount, address beneficiary, uint256 minReturnedTokens, string memo, bytes metadata)`
37
+ **Edge conditions that change user experience:** paused payments, custom hook side effects, fee-on-transfer tokens, unsupported price feeds, zero-weight rulesets, and permit-based flows.
43
38
 
44
- **Who can call**: Anyone.
39
+ ## Journey 3: Turn Credits Into ERC-20 Tokens Once A Project Wants A Transferable Token
45
40
 
46
- **Parameters**:
47
- - `projectId` -- The project to pay
48
- - `token` -- Token address (`JBConstants.NATIVE_TOKEN` for ETH)
49
- - `amount` -- Amount of tokens (ignored for native token; uses `msg.value`)
50
- - `beneficiary` -- Address to receive minted project tokens
51
- - `minReturnedTokens` -- Slippage protection; reverts if fewer tokens minted
52
- - `memo` -- Arbitrary string
53
- - `metadata` -- Bytes; may contain Permit2 data (keyed by `"permit2"` ID) and/or hook-specific data
41
+ **Starting state:** users already have project token credits and the project now wants an ERC-20 representation.
54
42
 
55
- **State changes**:
56
- 1. Tokens transferred to terminal (or `msg.value` accepted)
57
- 2. `JBTerminalStore.balanceOf[terminal][projectId][token]` incremented (minus any hook-diverted amounts)
58
- 3. `JBTokens` mints project tokens to beneficiary (credits or ERC-20)
59
- 4. `JBController.pendingReservedTokenBalanceOf[projectId]` incremented by reserved portion
60
- 5. Pay hooks execute (if data hook returns specifications)
43
+ **Success:** the project deploys or installs its ERC-20 token and holders can claim credits into transferable balances.
61
44
 
62
- **Events**: `Pay(rulesetId, rulesetCycleNumber, projectId, payer, beneficiary, amount, newlyIssuedTokenCount, memo, metadata, caller)`
45
+ **Flow**
46
+ 1. Deploy or set the project's ERC-20 token through `JBController`.
47
+ 2. Holders or operators call `claimTokensFor(...)` to convert credits into ERC-20 balances for a beneficiary.
48
+ 3. Future issuance can continue using the same project identity while users now interact with a standard token surface.
63
49
 
64
- **Edge cases**:
65
- - `amount = 0` is valid -- records a zero payment, mints 0 tokens
66
- - `beneficiary = address(0)` -- tokens minted to zero address (effectively burned on mint)
67
- - If `pausePay` is set in ruleset metadata, `recordPaymentFrom` reverts
68
- - Token count = `mulDiv(amount.value, weight, weightRatio)` -- if weight is 0, no tokens minted
69
- - Data hook can return empty weight (0) to suppress minting while still recording payment
70
- - Fee-on-transfer tokens: actual amount received is `_balanceOf(token) - balanceBefore` (measured via balance diff)
50
+ ## Journey 4: Distribute Treasury Funds Through Governed Paths
71
51
 
72
- **Preview**: Call `JBMultiTerminal.previewPayFor(projectId, token, amount, beneficiary, metadata)` to simulate the full payment on-chain -- including data hook effects and the reserved/beneficiary token split. Returns `(ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications)`. This is a `view` function that does not modify state. For lower-level access without the mint split, the terminal delegates to `JBTerminalStore.previewPayFrom(terminal, payer, amount, projectId, beneficiary, metadata)` which returns the raw `(ruleset, tokenCount, hookSpecifications)`.
52
+ **Starting state:** the project has terminal balances and the owner wants payouts or allowance-based withdrawals.
73
53
 
74
- ---
54
+ **Success:** treasury value leaves only through configured limits, recipients, and fee logic instead of arbitrary admin withdrawals.
75
55
 
76
- ## 3. Cash Out Tokens
56
+ **Flow**
57
+ 1. Authorized actors call payout or allowance surfaces on the terminal.
58
+ 2. `JBFundAccessLimits` bounds how much may leave for the current ruleset cycle.
59
+ 3. `JBSplits` fans value out to beneficiaries, projects, hooks, or fee recipients as configured.
60
+ 4. `JBTerminalStore` updates balances and fee accounting so later previews and cash outs remain consistent.
77
61
 
78
- **Entry point**: `JBMultiTerminal.cashOutTokensOf(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, uint256 minTokensReclaimed, address payable beneficiary, bytes metadata)`
62
+ **Failure cases that matter:** stale split expectations, exceeding access limits, downstream hook failures, and assuming allowance withdrawals behave like payouts when fee treatment differs.
79
63
 
80
- **Who can call**: The token holder, or an address with the holder's `CASH_OUT_TOKENS` permission.
64
+ ## Journey 5: Let Holders Cash Out Against Surplus
81
65
 
82
- **Parameters**:
83
- - `holder` -- Address whose tokens are being cashed out
84
- - `projectId` -- The project to cash out from
85
- - `cashOutCount` -- Number of project tokens to burn (18 decimals)
86
- - `tokenToReclaim` -- Terminal token to receive back
87
- - `minTokensReclaimed` -- Slippage protection
88
- - `beneficiary` -- Address to receive reclaimed tokens
89
- - `metadata` -- Hook-specific data
66
+ **Starting state:** a holder owns project token exposure and the project has reclaimable surplus in some terminal.
90
67
 
91
- **State changes**:
92
- 1. `STORE.recordCashOutFor()` -- computes the reclaim amount via bonding curve (using current `totalSupply` which still includes the tokens being cashed out), then decrements `JBTerminalStore.balanceOf[terminal][projectId][token]` by `reclaimAmount + hookSpec amounts`
93
- 2. Project tokens burned via `JBController.burnTokensOf()` (credits first, then ERC-20) -- happens AFTER the reclaim calculation, so the bonding curve sees the pre-burn supply
94
- 3. Reclaimed tokens transferred to beneficiary
95
- 4. Cash out hooks execute (if data hook returns specifications)
96
- 5. Fee taken (2.5%) on total amount eligible for fees, unless beneficiary is feeless. When `cashOutTaxRate == 0`, the fee applies only up to the project's unconsumed fee-free surplus (`_feeFreeSurplusOf`) from intra-terminal payouts -- once depleted, cashouts are fee-free.
68
+ **Success:** the holder burns the intended amount of token exposure and receives the correct reclaim amount under the current ruleset.
97
69
 
98
- **Events**: `CashOutTokens(rulesetId, rulesetCycleNumber, projectId, holder, beneficiary, cashOutCount, cashOutTaxRate, reclaimAmount, metadata, caller)`
70
+ **Flow**
71
+ 1. The holder calls `cashOutTokensOf(...)` on the relevant terminal.
72
+ 2. `JBTerminalStore` calculates reclaim value using surplus, outstanding token supply, cash-out tax rate, and any pending reserved token effects.
73
+ 3. Cash-out hooks can modify behavior or side effects, but the core accounting remains anchored in the terminal store.
74
+ 4. Tokens burn and value exits the treasury through the terminal that actually held the asset.
99
75
 
100
- **Preview**: Call `JBMultiTerminal.previewCashOutFrom(holder, projectId, cashOutCount, tokenToReclaim, beneficiary, metadata)` to simulate the full cash out on-chain -- including data hook effects on tax rate, supply, and hook specifications. Returns `(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications)`. This is a `view` function that does not modify state. The terminal delegates to `JBTerminalStore.previewCashOutFrom(terminal, holder, projectId, cashOutCount, tokenToReclaim, beneficiaryIsFeeless, metadata)` for the bonding curve computation. For a simpler estimate without data hook effects, use `currentTotalReclaimableSurplusOf(projectId, cashOutCount, decimals, currency)` or the 6-param `currentReclaimableSurplusOf` overload.
76
+ **Edge conditions that matter:** fee-free addresses, custom cash-out hooks, preview-versus-execution drift under volatile routing, and multi-terminal surplus that users may misread as single-pool liquidity.
101
77
 
102
- **Edge cases**:
103
- - `cashOutCount = 0` with `totalSupply = 0` -- returns entire surplus (C-5 known bug)
104
- - `cashOutTaxRate = MAX (10,000)` -- returns 0 (all surplus locked)
105
- - `cashOutTaxRate = 0` -- proportional (1:1 against supply) with no discount
106
- - `cashOutCount >= totalSupply` -- returns entire surplus regardless of tax rate
107
- - Data hook can override `cashOutTaxRate`, `cashOutCount`, `totalSupply` to arbitrary values
108
- - Fee is NOT taken when `cashOutTaxRate == 0` UNLESS the project has unconsumed fee-free surplus from intra-terminal payouts (`_feeFreeSurplusOf`). The fee applies only up to the surplus amount and depletes it — preventing round-trip fee bypass while scoping fees precisely to the fee-free inflow.
109
- - Pending reserved tokens inflate `totalSupply`, reducing individual cash out value (H-4)
78
+ ## Journey 6: Queue New Rulesets Without Migrating The Project
110
79
 
111
- ---
80
+ **Starting state:** the project is live and future economics need to change.
112
81
 
113
- ## 4. Send Payouts to Splits
82
+ **Success:** the next ruleset activates on schedule while the project keeps the same identity, treasury, and downstream integrations.
114
83
 
115
- **Entry point**: `JBMultiTerminal.sendPayoutsOf(uint256 projectId, address token, uint256 amount, uint256 currency, uint256 minTokensPaidOut)`
84
+ **Flow**
85
+ 1. The owner or an operator with the right permission queues one or more new rulesets through `JBController`.
86
+ 2. `JBRulesets` stores the proposed future configuration and any approval hook requirements.
87
+ 3. When the active duration elapses, the next approved ruleset becomes live and future pays, payouts, and cash outs follow the new terms.
88
+ 4. Existing balances and token history remain intact because only future behavior changed.
116
89
 
117
- **Who can call**: Anyone (unless `ownerMustSendPayouts` is set, then requires `SEND_PAYOUTS` permission from owner).
90
+ **Failure cases that matter:** forgetting approval hooks, queueing incompatible metadata for installed hooks, and assuming a ruleset change can retroactively repair prior accounting.
118
91
 
119
- **Parameters**:
120
- - `projectId` -- The project distributing payouts
121
- - `token` -- Token being distributed
122
- - `amount` -- Amount to distribute (in terms of `currency`)
123
- - `currency` -- Currency denomination of the amount; must match a configured payout limit's currency
124
- - `minTokensPaidOut` -- Slippage protection on the actual token amount paid out
92
+ ## Journey 7: Migrate A Project To New Terminal Or Controller Surfaces Deliberately
125
93
 
126
- **State changes**:
127
- 1. `JBTerminalStore.balanceOf` decremented by `amountPaidOut` (currency-converted)
128
- 2. `JBTerminalStore.usedPayoutLimitOf` incremented
129
- 3. For each split: funds transferred (split hooks, project terminals, or addresses)
130
- 4. Failed splits: amount returned to project balance via `recordAddedBalanceFor`
131
- 5. Leftover (if splits < 100%): sent to project owner
132
- 6. Fee taken on all non-feeless payouts
94
+ **Starting state:** the project needs to move to a new terminal or controller path without pretending historical balances and routing do not exist.
133
95
 
134
- **Events**: `SendPayouts(rulesetId, rulesetCycleNumber, projectId, projectOwner, amount, amountPaidOut, fee, netLeftoverPayoutAmount, caller)`, `SendPayoutToSplit(...)` per split
96
+ **Success:** migration uses the protocol's explicit surfaces so balances, permissions, and future routing stay coherent.
135
97
 
136
- **Edge cases**:
137
- - `amount > payout limit` -- reverts with `InadequateControllerPayoutLimit`. Does NOT auto-cap.
138
- - Empty `fundAccessLimitGroups` for the terminal/token = zero payout limit = always reverts
139
- - Currency conversion uses `JBPrices` if `currency != accountingContext.currency`
140
- - Payout limit resets each ruleset cycle (`cycleNumber`)
141
- - `ownerMustSendPayouts` flag gates who can trigger payouts
142
- - Individual split failures are caught by try-catch; the payout continues to remaining splits
143
- - Split percentage uses `mulDiv(amount, split.percent, leftoverPercentage)` -- each split gets its proportion of the remaining amount, not of the original total
98
+ **Flow**
99
+ 1. Confirm the active ruleset permits migration and the destination surface is the intended successor.
100
+ 2. Use `JBController.migrate(...)` and the terminal-store migration paths instead of manually repointing addresses.
101
+ 3. Recheck directory routing and accepted accounting contexts after migration completes.
144
102
 
145
- ---
103
+ ## Journey 8: Hand Off Authority Without Handing Out Root Access
146
104
 
147
- ## 5. Use Surplus Allowance
105
+ **Starting state:** the project owner wants operators, delegates, or automation to manage only specific surfaces.
148
106
 
149
- **Entry point**: `JBMultiTerminal.useAllowanceOf(uint256 projectId, address token, uint256 amount, uint256 currency, uint256 minTokensPaidOut, address payable beneficiary, address payable feeBeneficiary, string memo)`
107
+ **Success:** permissions are narrow, auditable, and scoped to the actions the operator actually needs.
150
108
 
151
- **Who can call**: Project owner or address with `USE_ALLOWANCE` permission.
109
+ **Flow**
110
+ 1. The owner configures operator permissions in `JBPermissions`.
111
+ 2. Downstream calls check those packed permission bits instead of assuming project ownership.
112
+ 3. Integrations such as ownable wrappers, hook deployers, and router registries can now respect project-scoped delegation without custom ACL logic.
152
113
 
153
- **Parameters**:
154
- - `projectId`, `token`, `amount`, `currency` -- What to withdraw and in what denomination
155
- - `minTokensPaidOut` -- Slippage protection on net amount after fees
156
- - `beneficiary` -- Receives the withdrawn surplus
157
- - `feeBeneficiary` -- Receives project #1 tokens minted from the fee payment
158
- - `memo` -- Arbitrary string
114
+ ## Hand-Offs
159
115
 
160
- **State changes**:
161
- 1. `JBTerminalStore.balanceOf` decremented by `usedAmount`
162
- 2. `JBTerminalStore.usedSurplusAllowanceOf` incremented
163
- 3. Fee taken (unless owner or beneficiary is feeless)
164
- 4. Net amount transferred to beneficiary
165
-
166
- **Events**: `UseAllowance(rulesetId, rulesetCycleNumber, projectId, beneficiary, feeBeneficiary, amount, amountPaidOut, netAmountPaidOut, memo, caller)`
167
-
168
- **Edge cases**:
169
- - `usedAmount > surplus` -- reverts (`InadequateTerminalStoreBalance`)
170
- - Surplus allowance resets each ruleset (keyed by `rulesetId`, not `cycleNumber`)
171
- - Amount validated against surplus BEFORE checking allowance limit
172
- - If either the owner or the beneficiary is feeless, no fee is taken
173
-
174
- ---
175
-
176
- ## 6. Mint Tokens (Owner)
177
-
178
- **Entry point**: `JBController.mintTokensOf(uint256 projectId, uint256 tokenCount, address beneficiary, string memo, bool useReservedPercent)`
179
-
180
- **Who can call**: Project owner, address with `MINT_TOKENS` permission, a project terminal, the data hook, or an address with `hasMintPermissionFor` from the data hook.
181
-
182
- **Parameters**:
183
- - `projectId` -- Target project
184
- - `tokenCount` -- Total tokens to mint (including reserved portion)
185
- - `beneficiary` -- Receives the non-reserved tokens
186
- - `memo` -- Arbitrary string
187
- - `useReservedPercent` -- If true, applies the ruleset's `reservedPercent`; if false, all tokens go to beneficiary
188
-
189
- **State changes**:
190
- 1. `JBTokens.mintFor(beneficiary, beneficiaryTokenCount)` -- Mints non-reserved portion
191
- 2. `pendingReservedTokenBalanceOf[projectId]` incremented by reserved portion
192
-
193
- **Events**: `MintTokens(beneficiary, projectId, tokenCount, beneficiaryTokenCount, memo, reservedPercent, caller)`
194
-
195
- **Preview**: Call `JBController.previewMintOf(projectId, tokenCount, useReservedPercent)` to see how a mint would split between the beneficiary and reserved token pools under the current ruleset. Returns `(beneficiaryTokenCount, reservedTokenCount)`. This is a `view` function that does not modify state.
196
-
197
- **Edge cases**:
198
- - `tokenCount = 0` -- reverts (`ZeroTokensToMint`)
199
- - If `allowOwnerMinting` is false in ruleset, only terminals and data hooks can mint
200
- - If `reservedPercent = 10,000` (100%), all tokens go to pending reserved balance, `beneficiaryTokenCount = 0`
201
- - Terminal calls this with `useReservedPercent = true` during payments
202
-
203
- ---
204
-
205
- ## 7. Burn Tokens
206
-
207
- **Entry point**: `JBController.burnTokensOf(address holder, uint256 projectId, uint256 tokenCount, string memo)`
208
-
209
- **Who can call**: The token holder, an address with the holder's `BURN_TOKENS` permission, or a project terminal.
210
-
211
- **Parameters**:
212
- - `holder` -- Address whose tokens to burn
213
- - `projectId` -- Project whose tokens are being burned
214
- - `tokenCount` -- Number of tokens to burn
215
- - `memo` -- Arbitrary string
216
-
217
- **State changes**:
218
- 1. Credits burned first (up to credit balance)
219
- 2. Remaining amount burned from ERC-20 balance (if any)
220
- 3. `JBTokens` reduces credit and/or ERC-20 supply
221
-
222
- **Events**: `BurnTokens(holder, projectId, tokenCount, memo, caller)`
223
-
224
- **Edge cases**:
225
- - `tokenCount = 0` -- reverts (`ZeroTokensToBurn`)
226
- - Credits are always burned first. If holder has 100 credits and 50 ERC-20, burning 120 burns all 100 credits + 20 ERC-20.
227
- - Terminal calls this during cash outs
228
-
229
- ---
230
-
231
- ## 8. Queue New Ruleset
232
-
233
- **Entry point**: `JBController.queueRulesetsOf(uint256 projectId, JBRulesetConfig[] rulesetConfigurations, string memo)`
234
-
235
- **Who can call**: Project owner, address with `QUEUE_RULESETS` permission, or the `OMNICHAIN_RULESET_OPERATOR`.
236
-
237
- **Parameters**:
238
- - `projectId` -- Target project
239
- - `rulesetConfigurations` -- Array of ruleset configs to queue
240
- - `memo` -- Arbitrary string
241
-
242
- **State changes**:
243
- 1. For each config:
244
- - `JBRulesets.queueFor(...)` -- Creates new ruleset in linked list
245
- - `JBSplits.setSplitGroupsOf(...)` -- Sets splits for the new ruleset
246
- - `JBFundAccessLimits.setFundAccessLimitsFor(...)` -- Sets limits for the new ruleset
247
- 2. `latestRulesetIdOf[projectId]` updated
248
-
249
- **Events**: `QueueRulesets(rulesetId, projectId, memo, caller)`, `RulesetQueued(rulesetId, projectId, ...)`
250
-
251
- **Edge cases**:
252
- - Empty array reverts (`RulesetsArrayEmpty`)
253
- - `reservedPercent > 10,000` reverts
254
- - `cashOutTaxRate > 10,000` reverts
255
- - `weight > type(uint112).max` reverts
256
- - `duration > type(uint32).max` reverts
257
- - `mustStartAtOrAfter + duration > type(uint48).max` reverts
258
- - If `mustStartAtOrAfter = 0`, it defaults to `block.timestamp`
259
- - If `rulesetId` collides with current timestamp, it is incremented by 1
260
- - Approval hook address is validated: must have code, must support `IJBRulesetApprovalHook` interface
261
- - Queued rulesets take effect after the current ruleset expires (or immediately for `duration = 0`)
262
-
263
- ---
264
-
265
- ## 9. Set Splits
266
-
267
- **Entry point**: `JBController.setSplitGroupsOf(uint256 projectId, uint256 rulesetId, JBSplitGroup[] splitGroups)`
268
-
269
- **Who can call**: Project owner or address with `SET_SPLIT_GROUPS` permission. Alternatively, a contract whose address matches the lower 160 bits of a `groupId` can set that group directly -- but only if the upper 96 bits of the `groupId` are non-zero (bare-address groupIds are protocol-reserved for terminal payout groups).
270
-
271
- **Parameters**:
272
- - `projectId` -- Target project
273
- - `rulesetId` -- The ruleset ID the splits apply to. Use `0` for default/fallback splits.
274
- - `splitGroups` -- Array of `JBSplitGroup` structs, each containing a `groupId` and `JBSplit[]`
275
-
276
- **State changes**:
277
- 1. `JBSplits` stores the new split groups for the project/ruleset/group combination
278
- 2. Locked splits from existing configuration must be preserved (validated by `JBSplits`)
279
-
280
- **Events**: `SetSplit(projectId, rulesetId, groupId, split, caller)` per split (emitted by `JBSplits`, not `JBController`)
281
-
282
- **Edge cases**:
283
- - Locked splits (`lockedUntil > block.timestamp`) cannot be removed or modified
284
- - If no splits set for a rulesetId, `splitsOf()` falls back to `rulesetId = 0` (default splits)
285
- - If no default splits either, all payouts/reserved tokens go to project owner
286
- - Split `percent` values are out of `SPLITS_TOTAL_PERCENT` (1,000,000,000)
287
- - Payout splits use `groupId = uint256(uint160(token))`, reserved token splits use `groupId = 1` (`JBSplitGroupIds.RESERVED_TOKENS`)
288
-
289
- ---
290
-
291
- ## 10. Migrate Terminal
292
-
293
- **Entry point**: `JBMultiTerminal.migrateBalanceOf(uint256 projectId, address token, IJBTerminal to)`
294
-
295
- **Who can call**: Project owner or address with `MIGRATE_TERMINAL` permission.
296
-
297
- **Parameters**:
298
- - `projectId` -- Project being migrated
299
- - `token` -- Token balance to migrate
300
- - `to` -- Destination terminal
301
-
302
- **State changes**:
303
- 1. `_feeFreeSurplusOf[projectId][token]` cleared
304
- 2. `JBTerminalStore.balanceOf[oldTerminal][projectId][token]` set to 0
305
- 3. If destination is non-feeless: 2.5% protocol fee deducted from balance via `_takeFeeFrom`
306
- 4. Remaining funds transferred to destination terminal via `to.addToBalanceOf()`
307
- 5. Destination terminal records the added balance
308
-
309
- **Events**: `MigrateTerminal(projectId, token, to, amount, caller)`
310
-
311
- **Edge cases**:
312
- - Requires `allowTerminalMigration` in current ruleset
313
- - Destination terminal must have accounting context for the token (validated via `accountingContextForTokenOf`)
314
- - **Standard 2.5% protocol fee** is charged when migrating to a non-feeless terminal, consistent with all other fund egress. This also settles any `_feeFreeSurplusOf` liability.
315
- - **Held fees are NOT transferred** -- they remain in the old terminal. Held fees belong to the fee beneficiary (project #1), not the migrating project.
316
- - If balance is 0, no transfer occurs
317
- - This only migrates one token's balance. Must be called once per token.
318
-
319
- ---
320
-
321
- ## 11. Migrate Controller
322
-
323
- **Entry point**: `JBDirectory.setControllerOf(uint256 projectId, IERC165 controller)`
324
-
325
- **Who can call**: Project owner, address with `SET_CONTROLLER` permission, or an address in `isAllowedToSetFirstController` (for first controller only). The current controller's ruleset must have `allowSetController` enabled.
326
-
327
- **Parameters**:
328
- - `projectId` -- The project being migrated
329
- - `controller` -- The new controller (must support `IERC165`)
330
-
331
- **Flow**:
332
- 1. `JBDirectory.setControllerOf(projectId, newController)` is called
333
- 2. If old controller exists AND new controller supports `IJBMigratable`: directory calls `newController.beforeReceiveMigrationFrom(oldController, projectId)`:
334
- - Copies metadata URI from old controller (if it supports `IJBProjectUriRegistry`)
335
- - Distributes pending reserved tokens from old controller (if it supports `IJBController` and has pending tokens)
336
- 3. If old controller exists AND old controller supports `IJBMigratable`: directory calls `oldController.migrate(projectId, newController)`:
337
- - Reverts if pending reserved tokens > 0 (must distribute first)
338
- 4. Directory updates `controllerOf[projectId] = newController`
339
- 5. If old controller exists AND new controller supports `IJBMigratable`: directory calls `newController.afterReceiveMigrationFrom(oldController, projectId)` (currently a no-op; verifies caller is the directory)
340
-
341
- **Events**: `Migrate(projectId, to, caller)` from old controller; `SetController(projectId, controller, caller)` from directory
342
-
343
- **Edge cases**:
344
- - `pendingReservedTokenBalanceOf[projectId] != 0` causes revert in `migrate()` -- reserved tokens must be distributed first
345
- - `beforeReceiveMigrationFrom` automatically distributes pending reserved tokens from the old controller
346
- - The old controller's `migrate()` runs while the directory still points to it (prevents reentrancy window)
347
- - First controller can be set by addresses in `isAllowedToSetFirstController` without owner permission
348
-
349
- ---
350
-
351
- ## 12. Deploy ERC-20 for Project Tokens
352
-
353
- **Entry point**: `JBController.deployERC20For(uint256 projectId, string name, string symbol, bytes32 salt)`
354
-
355
- **Who can call**: Project owner or address with `DEPLOY_ERC20` permission.
356
-
357
- **Parameters**:
358
- - `projectId` -- Target project
359
- - `name` -- ERC-20 token name
360
- - `symbol` -- ERC-20 token symbol
361
- - `salt` -- For deterministic deployment (CREATE2). Pass `bytes32(0)` for non-deterministic.
362
-
363
- **State changes**:
364
- 1. `JBTokens.deployERC20For()` clones `JBERC20` implementation via `Clones.clone()` (or `Clones.cloneDeterministic()` if salt provided)
365
- 2. Clone's `initialize(name, symbol, owner=JBTokens)` is called
366
- 3. `JBTokens.tokenOf[projectId]` set to the new token
367
-
368
- **Events**: `DeployERC20(projectId, deployer, salt, saltHash, caller)` from `JBController`, `DeployERC20(projectId, token, name, symbol, salt, caller)` from `JBTokens`
369
-
370
- **Edge cases**:
371
- - Can only be called once per project. If a token is already set, `JBTokens` reverts.
372
- - `salt` is hashed with `_msgSender()` to prevent front-running deterministic deployments
373
- - The clone's constructor sets invalid name/symbol; real values come from `initialize()`
374
-
375
- ---
376
-
377
- ## 13. Claim Credits as ERC-20
378
-
379
- **Entry point**: `JBController.claimTokensFor(address holder, uint256 projectId, uint256 tokenCount, address beneficiary)`
380
-
381
- **Who can call**: The credit holder or address with `CLAIM_TOKENS` permission.
382
-
383
- **Parameters**:
384
- - `holder` -- Address whose credits to convert
385
- - `projectId` -- Target project
386
- - `tokenCount` -- Number of credits to convert to ERC-20
387
- - `beneficiary` -- Address to receive the ERC-20 tokens
388
-
389
- **State changes**:
390
- 1. `JBTokens.creditBalanceOf[holder][projectId]` decreased by `tokenCount`
391
- 2. ERC-20 tokens minted to `beneficiary` for `tokenCount`
392
-
393
- **Events**: `ClaimTokens(holder, projectId, creditBalance, count, beneficiary, caller)` (emitted by `JBTokens`)
394
-
395
- **Edge cases**:
396
- - Requires an ERC-20 token to be deployed for the project (reverts otherwise)
397
- - Credits and ERC-20 tokens are fungible -- this is a one-way conversion from internal credits to on-chain ERC-20
398
- - Does not require any ruleset flag (always allowed if ERC-20 exists)
399
-
400
- ---
401
-
402
- ## 14. Process Held Fees
403
-
404
- **Entry point**: `JBMultiTerminal.processHeldFeesOf(uint256 projectId, address token, uint256 count)`
405
-
406
- **Who can call**: Anyone.
407
-
408
- **Parameters**:
409
- - `projectId` -- Project whose held fees to process
410
- - `token` -- Token the fees are denominated in
411
- - `count` -- Maximum number of held fees to process
412
-
413
- **State changes**:
414
- 1. For each processable fee (unlocked, i.e., `unlockTimestamp <= block.timestamp`):
415
- - Fee entry deleted from `_heldFeesOf` array
416
- - `_nextHeldFeeIndexOf` incremented
417
- - Fee amount sent to project #1's terminal via `_processFee` (try-catch)
418
- - On failure: fee amount returned to project's balance
419
- 2. If all fees processed: array and index are reset to 0
420
-
421
- **Events**: `ProcessFee(projectId, token, amount, wasHeld, beneficiary, caller)` per fee, or `FeeReverted(...)` on failure
422
-
423
- **Edge cases**:
424
- - Fees unlock after 28 days from when they were held
425
- - Processing stops at the first locked fee (fees are sequential)
426
- - Re-reads storage index each iteration (reentrancy-safe)
427
- - If fee terminal for project #1 does not exist for the token, `_processFee` reverts (caught by try-catch, returns to project balance)
428
-
429
- ---
430
-
431
- ## 15. Add Price Feeds
432
-
433
- **Entry point**: `JBController.addPriceFeedFor(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency, IJBPriceFeed feed)`
434
-
435
- **Who can call**: Project owner or address with `ADD_PRICE_FEED` permission.
436
-
437
- **Parameters**:
438
- - `projectId` -- Project the feed applies to (0 for protocol-wide default, owner-only)
439
- - `pricingCurrency` -- Currency the feed's output is in
440
- - `unitCurrency` -- Currency being priced
441
- - `feed` -- The price feed contract address
442
-
443
- **State changes**:
444
- 1. `JBPrices` stores the feed for the `(projectId, pricingCurrency, unitCurrency)` triple
445
- 2. Feed is **immutable** once set -- cannot be replaced or removed
446
-
447
- **Events**: `AddPriceFeed(projectId, pricingCurrency, unitCurrency, feed, caller)` (emitted by `JBPrices`)
448
-
449
- **Edge cases**:
450
- - Requires `allowAddPriceFeed` in current ruleset
451
- - Protocol-wide defaults (`projectId = 0`) can only be set by the `JBPrices` owner
452
- - `JBPrices` auto-calculates inverse: if A->B exists, B->A is derived. If both explicit and inverse exist, explicit takes priority.
453
- - Lookup order: project-specific -> project-specific inverse -> default (projectId=0) -> default inverse
454
-
455
- ---
456
-
457
- ## 16. Set Permissions
458
-
459
- **Entry point**: `JBPermissions.setPermissionsFor(address account, JBPermissionsData permissionsData)`
460
-
461
- **Who can call**: The `account` itself, or a ROOT operator for the project (with restrictions).
462
-
463
- **Parameters**:
464
- - `account` -- The account granting permissions
465
- - `permissionsData.operator` -- Address receiving the permissions
466
- - `permissionsData.projectId` -- Project scope (0 = wildcard, all projects)
467
- - `permissionsData.permissionIds` -- Array of `uint8` permission IDs to grant
468
-
469
- **State changes**:
470
- 1. `permissionsOf[operator][account][projectId]` set to packed `uint256` bitmap
471
-
472
- **Events**: `OperatorPermissionsSet(operator, account, projectId, permissionIds, packed, caller)`
473
-
474
- **Edge cases**:
475
- - Permission ID 0 cannot be set (reserved, always `NoZeroPermission` revert)
476
- - ROOT (ID 1) cannot be set for wildcard `projectId = 0` (`CantSetRootPermissionForWildcardProject`)
477
- - ROOT operators can set non-ROOT permissions for others on their scoped project
478
- - ROOT operators CANNOT grant ROOT to other addresses
479
- - ROOT operators CANNOT set permissions for wildcard `projectId = 0`
480
- - Setting permissions replaces the entire bitmap (not additive) -- passing an empty array clears all permissions
481
-
482
- ---
483
-
484
- ## 17. Transfer Project Ownership
485
-
486
- **Entry point**: `JBProjects.transferFrom(address from, address to, uint256 tokenId)` (standard ERC-721)
487
-
488
- **Who can call**: The current owner, an approved address, or an operator approved for all.
489
-
490
- **Parameters**:
491
- - `from` -- Current owner
492
- - `to` -- New owner
493
- - `tokenId` -- The project ID (same as the ERC-721 token ID)
494
-
495
- **State changes**:
496
- 1. ERC-721 ownership transferred
497
- 2. All `PROJECTS.ownerOf(projectId)` calls now return the new owner
498
- 3. All permission checks that reference the owner now apply to the new owner
499
-
500
- **Events**: `Transfer(from, to, tokenId)` (standard ERC-721 event)
501
-
502
- **Edge cases**:
503
- - This is a standard ERC-721 transfer. All ERC-721 rules apply (approval, operator, etc.)
504
- - **Permissions are NOT transferred**. Existing operators retain their permissions scoped to the account that granted them (the old owner). The new owner must grant their own permissions.
505
- - The new owner immediately gets full control: queue rulesets, set terminals, set splits, etc.
506
- - Transferring to `address(0)` is prevented by OpenZeppelin's ERC-721 implementation
507
-
508
- ---
509
-
510
- ## 18. Add to Balance (Without Minting)
511
-
512
- **Entry point**: `JBMultiTerminal.addToBalanceOf(uint256 projectId, address token, uint256 amount, bool shouldReturnHeldFees, string memo, bytes metadata)`
513
-
514
- **Who can call**: Anyone.
515
-
516
- **Parameters**:
517
- - `projectId` -- Project to add funds to
518
- - `token` -- Token to add
519
- - `amount` -- Amount to add (uses `msg.value` for native token)
520
- - `shouldReturnHeldFees` -- If true, uses the added amount to return held fees (reducing the fee burden)
521
- - `memo`, `metadata` -- Arbitrary data
522
-
523
- **State changes**:
524
- 1. Tokens transferred to terminal
525
- 2. If `shouldReturnHeldFees`: iterates held fees, returns fees proportional to amount added
526
- 3. `JBTerminalStore.balanceOf[terminal][projectId][token]` incremented by `amount + returnedFees`
527
-
528
- **Events**: `AddToBalance(projectId, amount, returnedFees, memo, metadata, caller)`
529
-
530
- **Edge cases**:
531
- - Does NOT mint tokens -- purely adds to balance. This increases surplus, which increases cash out value for existing holders.
532
- - `shouldReturnHeldFees = true` partially or fully returns held fees. The returned fee amount is added to the project balance on top of the deposited amount.
533
- - Used by terminal migration (`migrateBalanceOf`) and split payouts (when `preferAddToBalance = true`)
534
-
535
- ---
536
-
537
- ## 19. Transfer Credits
538
-
539
- **Entry point**: `JBController.transferCreditsFrom(address holder, uint256 projectId, address recipient, uint256 creditCount)`
540
-
541
- **Who can call**: The credit holder or address with `TRANSFER_CREDITS` permission.
542
-
543
- **Parameters**:
544
- - `holder` -- Address transferring credits
545
- - `projectId` -- Project whose credits are being transferred
546
- - `recipient` -- Address to receive credits
547
- - `creditCount` -- Number of credits to transfer
548
-
549
- **State changes**:
550
- 1. `JBTokens.creditBalanceOf[holder][projectId]` decreased
551
- 2. `JBTokens.creditBalanceOf[recipient][projectId]` increased
552
-
553
- **Events**: `TransferCredits(holder, projectId, recipient, count, caller)` (emitted by `JBTokens`)
554
-
555
- **Edge cases**:
556
- - Reverts if `pauseCreditTransfers` is set in current ruleset
557
- - Credits are internal -- they are NOT ERC-20 tokens. Use `claimTokensFor` to convert credits to ERC-20.
558
- - This only transfers credits, not ERC-20 tokens. ERC-20 tokens are transferred via standard ERC-20 `transfer`/`transferFrom`.
559
-
560
- ---
561
-
562
- ## 20. Set Project URI
563
-
564
- **Entry point**: `JBController.setUriOf(uint256 projectId, string uri)`
565
-
566
- **Who can call**: Project owner or address with `SET_PROJECT_URI` permission.
567
-
568
- **Parameters**:
569
- - `projectId` -- Target project
570
- - `uri` -- Metadata URI (typically an IPFS hash)
571
-
572
- **State changes**: `uriOf[projectId] = uri`
573
-
574
- **Events**: `SetUri(projectId, uri, caller)`
575
-
576
- **Edge cases**:
577
- - Empty string is valid -- clears the metadata URI
578
- - No ruleset flag required (always allowed with permission)
579
-
580
- ---
581
-
582
- ## 21. Set Custom Token
583
-
584
- **Entry point**: `JBController.setTokenFor(uint256 projectId, IJBToken token)`
585
-
586
- **Who can call**: Project owner or address with `SET_TOKEN` permission.
587
-
588
- **Parameters**:
589
- - `projectId` -- Target project
590
- - `token` -- The custom token contract (must implement `IJBToken`)
591
-
592
- **State changes**: `JBTokens.tokenOf[projectId] = token`
593
-
594
- **Events**: `SetToken(projectId, token, caller)` (emitted by `JBTokens`)
595
-
596
- **Edge cases**:
597
- - Requires `allowSetCustomToken` in current or upcoming ruleset
598
- - Can only be called if no token is currently set for the project
599
- - The token must conform to `IJBToken` interface (18 decimals required)
600
-
601
- ---
602
-
603
- ## 22. Set Token Metadata
604
-
605
- **Entry point**: `JBController.setTokenMetadataOf(uint256 projectId, string name, string symbol)`
606
-
607
- **Who can call**: Project owner or address with `SET_TOKEN_METADATA` permission.
608
-
609
- **Parameters**:
610
- - `projectId` -- Target project
611
- - `name` -- New ERC-20 token name
612
- - `symbol` -- New ERC-20 token symbol
613
-
614
- **State changes**: Updates the ERC-20 token's name and symbol via `JBERC20.setMetadata()`
615
-
616
- **Events**: `SetTokenMetadata(projectId, name, symbol, caller)` (emitted by `JBTokens`)
617
-
618
- **Edge cases**:
619
- - Requires an ERC-20 token to be deployed for the project (reverts if no token set)
620
- - Only works with `JBERC20` clones (custom tokens that implement `IJBToken` may not support `setMetadata`)
621
-
622
- ---
623
-
624
- ## 23. Update Ruleset Weight Cache
625
-
626
- **Entry point**: `JBRulesets.updateRulesetWeightCache(uint256 projectId, uint256 rulesetId)`
627
-
628
- **Who can call**: Anyone.
629
-
630
- **Parameters**:
631
- - `projectId` -- Target project
632
- - `rulesetId` -- The specific ruleset to update cache for
633
-
634
- **State changes**:
635
- 1. `_weightCacheOf[projectId][rulesetId].weight` updated to current decayed weight
636
- 2. `_weightCacheOf[projectId][rulesetId].weightCutMultiple` updated
637
-
638
- **Events**: `WeightCacheUpdated(projectId, weight, weightCutMultiple, caller)`
639
-
640
- **Edge cases**:
641
- - Required for projects with >20,000 cycles (otherwise `currentOf()` reverts with `WeightCacheRequired`)
642
- - Advances the cache by at most `_WEIGHT_CUT_MULTIPLE_CACHE_LOOKUP_THRESHOLD` (20,000) cycles per call
643
- - Multiple calls needed to fully catch up for very large cycle gaps
644
- - No-op if `duration == 0` or `weightCutPercent == 0`
645
-
646
- ---
647
-
648
- ## 24. Add Accounting Contexts
649
-
650
- **Entry point**: `JBMultiTerminal.addAccountingContextsFor(uint256 projectId, JBAccountingContext[] accountingContexts)`
651
-
652
- **Who can call**: Project owner, address with `ADD_ACCOUNTING_CONTEXTS` permission, or the project's controller.
653
-
654
- **Parameters**:
655
- - `projectId` -- Target project
656
- - `accountingContexts` -- Array of `JBAccountingContext` structs, each specifying a token address, decimals, and currency
657
-
658
- **State changes**:
659
- 1. For each context: validates token decimals, stores `_accountingContextForTokenOf[projectId][token]`
660
- 2. Appends to `_accountingContextsOf[projectId]` array
661
-
662
- **Events**: `SetAccountingContext(projectId, context, caller)` per token
663
-
664
- **Edge cases**:
665
- - Requires `allowAddAccountingContext` in current ruleset (if ruleset exists)
666
- - Token cannot be added twice (`AccountingContextAlreadySet`)
667
- - Currency must be non-zero (`ZeroAccountingContextCurrency`)
668
- - For non-native tokens: decimals are validated against the token's `decimals()` function. Tokens that revert on `decimals()` bypass validation (caller responsible).
116
+ - Use [nana-permission-ids-v6](../nana-permission-ids-v6/USER_JOURNEYS.md) for the shared permission vocabulary that downstream repos import.
117
+ - Use [nana-721-hook-v6](../nana-721-hook-v6/USER_JOURNEYS.md), [nana-router-terminal-v6](../nana-router-terminal-v6/USER_JOURNEYS.md), and [nana-buyback-hook-v6](../nana-buyback-hook-v6/USER_JOURNEYS.md) for opinionated layers on top of the core terminal and ruleset surfaces.