@bananapus/core-v6 0.0.16 → 0.0.17

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 (43) hide show
  1. package/ADMINISTRATION.md +1 -1
  2. package/ARCHITECTURE.md +2 -1
  3. package/AUDIT_INSTRUCTIONS.md +342 -0
  4. package/CHANGE_LOG.md +375 -0
  5. package/README.md +4 -4
  6. package/RISKS.md +171 -50
  7. package/SKILLS.md +9 -6
  8. package/USER_JOURNEYS.md +622 -0
  9. package/package.json +2 -2
  10. package/script/DeployPeriphery.s.sol +7 -1
  11. package/src/JBController.sol +5 -0
  12. package/src/JBDeadline.sol +3 -0
  13. package/src/JBDirectory.sol +2 -1
  14. package/src/JBMultiTerminal.sol +50 -9
  15. package/src/JBPermissions.sol +2 -0
  16. package/src/JBPrices.sol +8 -2
  17. package/src/JBRulesets.sol +3 -0
  18. package/src/JBSplits.sol +9 -5
  19. package/src/JBTerminalStore.sol +54 -47
  20. package/src/JBTokens.sol +3 -0
  21. package/src/interfaces/IJBTerminalStore.sol +3 -0
  22. package/src/libraries/JBFees.sol +2 -0
  23. package/src/libraries/JBMetadataResolver.sol +17 -4
  24. package/src/structs/JBBeforeCashOutRecordedContext.sol +4 -0
  25. package/test/TestAuditResponseDesignProofs.sol +434 -0
  26. package/test/TestDataHookFuzzing.sol +520 -0
  27. package/test/TestFeeFreeCashOutBypass.sol +617 -0
  28. package/test/TestL2SequencerPriceFeed.sol +292 -0
  29. package/test/TestMetadataOffsetOverflow.sol +179 -0
  30. package/test/TestMultiTerminalSurplus.sol +348 -0
  31. package/test/TestPermit2DataHook.t.sol +360 -0
  32. package/test/TestRulesetQueueing.sol +1 -2
  33. package/test/TestRulesetWeightCaching.sol +122 -124
  34. package/test/WeirdTokenTests.t.sol +37 -0
  35. package/test/regression/HoldFeesCashOutReserved.t.sol +415 -0
  36. package/test/regression/WeightCacheBoundary.t.sol +291 -0
  37. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +2 -2
  38. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +18 -17
  39. package/test/units/static/JBMultiTerminal/TestPay.sol +6 -4
  40. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +206 -18
  41. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +280 -0
  42. package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +55 -12
  43. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +72 -0
@@ -0,0 +1,622 @@
1
+ # nana-core-v6 -- User Journeys
2
+
3
+ All user paths through the Juicebox V6 core protocol. For each journey: entry point, key parameters, state changes, events, and edge cases.
4
+
5
+ ---
6
+
7
+ ## 1. Launch Project
8
+
9
+ **Entry point**: `JBController.launchProjectFor(address owner, string projectUri, JBRulesetConfig[] rulesetConfigurations, JBTerminalConfig[] terminalConfigurations, string memo)`
10
+
11
+ **Who can call**: Anyone. The project ERC-721 is minted to the specified `owner`.
12
+
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
19
+
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
30
+
31
+ **Events**: `LaunchProject(rulesetId, projectId, projectUri, memo, caller)`
32
+
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
37
+
38
+ ---
39
+
40
+ ## 2. Pay a Project
41
+
42
+ **Entry point**: `JBMultiTerminal.pay(uint256 projectId, address token, uint256 amount, address beneficiary, uint256 minReturnedTokens, string memo, bytes metadata)`
43
+
44
+ **Who can call**: Anyone.
45
+
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
54
+
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)
61
+
62
+ **Events**: `Pay(rulesetId, rulesetCycleNumber, projectId, payer, beneficiary, amount, newlyIssuedTokenCount, memo, metadata, caller)`
63
+
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)
71
+
72
+ ---
73
+
74
+ ## 3. Cash Out Tokens
75
+
76
+ **Entry point**: `JBMultiTerminal.cashOutTokensOf(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, uint256 minTokensReclaimed, address payable beneficiary, bytes metadata)`
77
+
78
+ **Who can call**: The token holder, or an address with the holder's `CASH_OUT_TOKENS` permission.
79
+
80
+ **Parameters**:
81
+ - `holder` -- Address whose tokens are being cashed out
82
+ - `projectId` -- The project to cash out from
83
+ - `cashOutCount` -- Number of project tokens to burn (18 decimals)
84
+ - `tokenToReclaim` -- Terminal token to receive back
85
+ - `minTokensReclaimed` -- Slippage protection
86
+ - `beneficiary` -- Address to receive reclaimed tokens
87
+ - `metadata` -- Hook-specific data
88
+
89
+ **State changes**:
90
+ 1. `JBTerminalStore.balanceOf[terminal][projectId][token]` decremented by `reclaimAmount + hookSpec amounts`
91
+ 2. Project tokens burned via `JBController.burnTokensOf()` (credits first, then ERC-20)
92
+ 3. Reclaimed tokens transferred to beneficiary
93
+ 4. Cash out hooks execute (if data hook returns specifications)
94
+ 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.
95
+
96
+ **Events**: `CashOutTokens(rulesetId, rulesetCycleNumber, projectId, holder, beneficiary, cashOutCount, cashOutTaxRate, reclaimAmount, metadata, caller)`
97
+
98
+ **Edge cases**:
99
+ - `cashOutCount = 0` with `totalSupply = 0` -- returns entire surplus (C-5 known bug)
100
+ - `cashOutTaxRate = MAX (10,000)` -- returns 0 (all surplus locked)
101
+ - `cashOutTaxRate = 0` -- proportional (1:1 against supply) with no discount
102
+ - `cashOutCount >= totalSupply` -- returns entire surplus regardless of tax rate
103
+ - Data hook can override `cashOutTaxRate`, `cashOutCount`, `totalSupply` to arbitrary values
104
+ - 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.
105
+ - Pending reserved tokens inflate `totalSupply`, reducing individual cash out value (H-4)
106
+
107
+ ---
108
+
109
+ ## 4. Send Payouts to Splits
110
+
111
+ **Entry point**: `JBMultiTerminal.sendPayoutsOf(uint256 projectId, address token, uint256 amount, uint256 currency, uint256 minTokensPaidOut)`
112
+
113
+ **Who can call**: Anyone (unless `ownerMustSendPayouts` is set, then requires `SEND_PAYOUTS` permission from owner).
114
+
115
+ **Parameters**:
116
+ - `projectId` -- The project distributing payouts
117
+ - `token` -- Token being distributed
118
+ - `amount` -- Amount to distribute (in terms of `currency`)
119
+ - `currency` -- Currency denomination of the amount; must match a configured payout limit's currency
120
+ - `minTokensPaidOut` -- Slippage protection on the actual token amount paid out
121
+
122
+ **State changes**:
123
+ 1. `JBTerminalStore.balanceOf` decremented by `amountPaidOut` (currency-converted)
124
+ 2. `JBTerminalStore.usedPayoutLimitOf` incremented
125
+ 3. For each split: funds transferred (split hooks, project terminals, or addresses)
126
+ 4. Failed splits: amount returned to project balance via `recordAddedBalanceFor`
127
+ 5. Leftover (if splits < 100%): sent to project owner
128
+ 6. Fee taken on all non-feeless payouts
129
+
130
+ **Events**: `SendPayouts(rulesetId, rulesetCycleNumber, projectId, projectOwner, amount, amountPaidOut, fee, netLeftoverPayoutAmount, caller)`, `SendPayoutToSplit(...)` per split
131
+
132
+ **Edge cases**:
133
+ - `amount > payout limit` -- reverts with `InadequateControllerPayoutLimit`. Does NOT auto-cap.
134
+ - Empty `fundAccessLimitGroups` for the terminal/token = zero payout limit = always reverts
135
+ - Currency conversion uses `JBPrices` if `currency != accountingContext.currency`
136
+ - Payout limit resets each ruleset cycle (`cycleNumber`)
137
+ - `ownerMustSendPayouts` flag gates who can trigger payouts
138
+ - Individual split failures are caught by try-catch; the payout continues to remaining splits
139
+ - Split percentage uses `mulDiv(amount, split.percent, leftoverPercentage)` -- each split gets its proportion of the remaining amount, not of the original total
140
+
141
+ ---
142
+
143
+ ## 5. Use Surplus Allowance
144
+
145
+ **Entry point**: `JBMultiTerminal.useAllowanceOf(uint256 projectId, address token, uint256 amount, uint256 currency, uint256 minTokensPaidOut, address payable beneficiary, address payable feeBeneficiary, string memo)`
146
+
147
+ **Who can call**: Project owner or address with `USE_ALLOWANCE` permission.
148
+
149
+ **Parameters**:
150
+ - `projectId`, `token`, `amount`, `currency` -- What to withdraw and in what denomination
151
+ - `minTokensPaidOut` -- Slippage protection on net amount after fees
152
+ - `beneficiary` -- Receives the withdrawn surplus
153
+ - `feeBeneficiary` -- Receives project #1 tokens minted from the fee payment
154
+ - `memo` -- Arbitrary string
155
+
156
+ **State changes**:
157
+ 1. `JBTerminalStore.balanceOf` decremented by `usedAmount`
158
+ 2. `JBTerminalStore.usedSurplusAllowanceOf` incremented
159
+ 3. Fee taken (unless owner or beneficiary is feeless)
160
+ 4. Net amount transferred to beneficiary
161
+
162
+ **Events**: `UseAllowance(rulesetId, rulesetCycleNumber, projectId, beneficiary, feeBeneficiary, amount, amountPaidOut, netAmountPaidOut, memo, caller)`
163
+
164
+ **Edge cases**:
165
+ - `usedAmount > surplus` -- reverts (`InadequateTerminalStoreBalance`)
166
+ - Surplus allowance resets each ruleset (keyed by `rulesetId`, not `cycleNumber`)
167
+ - Amount validated against surplus BEFORE checking allowance limit
168
+ - If both owner and beneficiary are feeless, no fee is taken
169
+
170
+ ---
171
+
172
+ ## 6. Mint Tokens (Owner)
173
+
174
+ **Entry point**: `JBController.mintTokensOf(uint256 projectId, uint256 tokenCount, address beneficiary, string memo, bool useReservedPercent)`
175
+
176
+ **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.
177
+
178
+ **Parameters**:
179
+ - `projectId` -- Target project
180
+ - `tokenCount` -- Total tokens to mint (including reserved portion)
181
+ - `beneficiary` -- Receives the non-reserved tokens
182
+ - `memo` -- Arbitrary string
183
+ - `useReservedPercent` -- If true, applies the ruleset's `reservedPercent`; if false, all tokens go to beneficiary
184
+
185
+ **State changes**:
186
+ 1. `JBTokens.mintFor(beneficiary, beneficiaryTokenCount)` -- Mints non-reserved portion
187
+ 2. `pendingReservedTokenBalanceOf[projectId]` incremented by reserved portion
188
+
189
+ **Events**: `MintTokens(beneficiary, projectId, tokenCount, beneficiaryTokenCount, memo, reservedPercent, caller)`
190
+
191
+ **Edge cases**:
192
+ - `tokenCount = 0` -- reverts (`ZeroTokensToMint`)
193
+ - If `allowOwnerMinting` is false in ruleset, only terminals and data hooks can mint
194
+ - If `reservedPercent = 10,000` (100%), all tokens go to pending reserved balance, `beneficiaryTokenCount = 0`
195
+ - Terminal calls this with `useReservedPercent = true` during payments
196
+
197
+ ---
198
+
199
+ ## 7. Burn Tokens
200
+
201
+ **Entry point**: `JBController.burnTokensOf(address holder, uint256 projectId, uint256 tokenCount, string memo)`
202
+
203
+ **Who can call**: The token holder, an address with the holder's `BURN_TOKENS` permission, or a project terminal.
204
+
205
+ **Parameters**:
206
+ - `holder` -- Address whose tokens to burn
207
+ - `projectId` -- Project whose tokens are being burned
208
+ - `tokenCount` -- Number of tokens to burn
209
+ - `memo` -- Arbitrary string
210
+
211
+ **State changes**:
212
+ 1. Credits burned first (up to credit balance)
213
+ 2. Remaining amount burned from ERC-20 balance (if any)
214
+ 3. `JBTokens` reduces credit and/or ERC-20 supply
215
+
216
+ **Events**: `BurnTokens(holder, projectId, tokenCount, memo, caller)`
217
+
218
+ **Edge cases**:
219
+ - `tokenCount = 0` -- reverts (`ZeroTokensToBurn`)
220
+ - Credits are always burned first. If holder has 100 credits and 50 ERC-20, burning 120 burns all 100 credits + 20 ERC-20.
221
+ - Terminal calls this during cash outs
222
+
223
+ ---
224
+
225
+ ## 8. Queue New Ruleset
226
+
227
+ **Entry point**: `JBController.queueRulesetsOf(uint256 projectId, JBRulesetConfig[] rulesetConfigurations, string memo)`
228
+
229
+ **Who can call**: Project owner, address with `QUEUE_RULESETS` permission, or the `OMNICHAIN_RULESET_OPERATOR`.
230
+
231
+ **Parameters**:
232
+ - `projectId` -- Target project
233
+ - `rulesetConfigurations` -- Array of ruleset configs to queue
234
+ - `memo` -- Arbitrary string
235
+
236
+ **State changes**:
237
+ 1. For each config:
238
+ - `JBRulesets.queueFor(...)` -- Creates new ruleset in linked list
239
+ - `JBSplits.setSplitGroupsOf(...)` -- Sets splits for the new ruleset
240
+ - `JBFundAccessLimits.setFundAccessLimitsFor(...)` -- Sets limits for the new ruleset
241
+ 2. `latestRulesetIdOf[projectId]` updated
242
+
243
+ **Events**: `QueueRulesets(rulesetId, projectId, memo, caller)`, `RulesetQueued(rulesetId, projectId, ...)`
244
+
245
+ **Edge cases**:
246
+ - Empty array reverts (`RulesetsArrayEmpty`)
247
+ - `reservedPercent > 10,000` reverts
248
+ - `cashOutTaxRate > 10,000` reverts
249
+ - `weight > type(uint112).max` reverts
250
+ - `duration > type(uint32).max` reverts
251
+ - `mustStartAtOrAfter + duration > type(uint48).max` reverts
252
+ - If `mustStartAtOrAfter = 0`, it defaults to `block.timestamp`
253
+ - If `rulesetId` collides with current timestamp, it is incremented by 1
254
+ - Approval hook address is validated: must have code, must support `IJBRulesetApprovalHook` interface
255
+ - Queued rulesets take effect after the current ruleset expires (or immediately for `duration = 0`)
256
+
257
+ ---
258
+
259
+ ## 9. Set Splits
260
+
261
+ **Entry point**: `JBController.setSplitGroupsOf(uint256 projectId, uint256 rulesetId, JBSplitGroup[] splitGroups)`
262
+
263
+ **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).
264
+
265
+ **Parameters**:
266
+ - `projectId` -- Target project
267
+ - `rulesetId` -- The ruleset ID the splits apply to. Use `0` for default/fallback splits.
268
+ - `splitGroups` -- Array of `JBSplitGroup` structs, each containing a `groupId` and `JBSplit[]`
269
+
270
+ **State changes**:
271
+ 1. `JBSplits` stores the new split groups for the project/ruleset/group combination
272
+ 2. Locked splits from existing configuration must be preserved (validated by `JBSplits`)
273
+
274
+ **Events**: Emitted by `JBSplits` (not `JBController`)
275
+
276
+ **Edge cases**:
277
+ - Locked splits (`lockedUntil > block.timestamp`) cannot be removed or modified
278
+ - If no splits set for a rulesetId, `splitsOf()` falls back to `rulesetId = 0` (default splits)
279
+ - If no default splits either, all payouts/reserved tokens go to project owner
280
+ - Split `percent` values are out of `SPLITS_TOTAL_PERCENT` (1,000,000,000)
281
+ - Payout splits use `groupId = uint256(uint160(token))`, reserved token splits use `groupId = 1` (`JBSplitGroupIds.RESERVED_TOKENS`)
282
+
283
+ ---
284
+
285
+ ## 10. Migrate Terminal
286
+
287
+ **Entry point**: `JBMultiTerminal.migrateBalanceOf(uint256 projectId, address token, IJBTerminal to)`
288
+
289
+ **Who can call**: Project owner or address with `MIGRATE_TERMINAL` permission.
290
+
291
+ **Parameters**:
292
+ - `projectId` -- Project being migrated
293
+ - `token` -- Token balance to migrate
294
+ - `to` -- Destination terminal
295
+
296
+ **State changes**:
297
+ 1. `JBTerminalStore.balanceOf[oldTerminal][projectId][token]` set to 0
298
+ 2. Funds transferred to destination terminal via `to.addToBalanceOf()`
299
+ 3. Destination terminal records the added balance
300
+
301
+ **Events**: `MigrateTerminal(projectId, token, to, amount, caller)`
302
+
303
+ **Edge cases**:
304
+ - Requires `allowTerminalMigration` in current ruleset
305
+ - Destination terminal must have accounting context for the token (validated via `accountingContextForTokenOf`)
306
+ - **Held fees are NOT transferred** -- they remain in the old terminal. Held fees belong to the fee beneficiary (project #1), not the migrating project.
307
+ - If balance is 0, no transfer occurs
308
+ - This only migrates one token's balance. Must be called once per token.
309
+
310
+ ---
311
+
312
+ ## 11. Migrate Controller
313
+
314
+ **Entry point**: `JBDirectory.setControllerOf(uint256 projectId, IERC165 controller)`
315
+
316
+ **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.
317
+
318
+ **Flow**:
319
+ 1. `JBDirectory.setControllerOf(projectId, newController)` is called
320
+ 2. Directory calls `newController.beforeReceiveMigrationFrom(oldController, projectId)`:
321
+ - Copies metadata URI from old controller
322
+ - Distributes pending reserved tokens from old controller
323
+ 3. Directory calls `oldController.migrate(projectId, newController)`:
324
+ - Reverts if pending reserved tokens > 0 (must distribute first)
325
+ 4. Directory updates `controllerOf[projectId] = newController`
326
+ 5. Directory calls `newController.afterReceiveMigrationFrom(oldController, projectId)`
327
+
328
+ **Events**: `Migrate(projectId, to, caller)` from old controller; `SetController(projectId, controller, caller)` from directory
329
+
330
+ **Edge cases**:
331
+ - `pendingReservedTokenBalanceOf[projectId] != 0` causes revert in `migrate()` -- reserved tokens must be distributed first
332
+ - `beforeReceiveMigrationFrom` automatically distributes pending reserved tokens from the old controller
333
+ - The old controller's `migrate()` runs while the directory still points to it (prevents reentrancy window)
334
+ - First controller can be set by addresses in `isAllowedToSetFirstController` without owner permission
335
+
336
+ ---
337
+
338
+ ## 12. Deploy ERC-20 for Project Tokens
339
+
340
+ **Entry point**: `JBController.deployERC20For(uint256 projectId, string name, string symbol, bytes32 salt)`
341
+
342
+ **Who can call**: Project owner or address with `DEPLOY_ERC20` permission.
343
+
344
+ **Parameters**:
345
+ - `projectId` -- Target project
346
+ - `name` -- ERC-20 token name
347
+ - `symbol` -- ERC-20 token symbol
348
+ - `salt` -- For deterministic deployment (CREATE2). Pass `bytes32(0)` for non-deterministic.
349
+
350
+ **State changes**:
351
+ 1. `JBTokens.deployERC20For()` clones `JBERC20` implementation via `Clones.clone()` (or `Clones.cloneDeterministic()` if salt provided)
352
+ 2. Clone's `initialize(name, symbol, owner=JBTokens)` is called
353
+ 3. `JBTokens.tokenOf[projectId]` set to the new token
354
+
355
+ **Events**: `DeployERC20(projectId, deployer, salt, saltHash, caller)`
356
+
357
+ **Edge cases**:
358
+ - Can only be called once per project. If a token is already set, `JBTokens` reverts.
359
+ - `salt` is hashed with `_msgSender()` to prevent front-running deterministic deployments
360
+ - The clone's constructor sets invalid name/symbol; real values come from `initialize()`
361
+
362
+ ---
363
+
364
+ ## 13. Claim Credits as ERC-20
365
+
366
+ **Entry point**: `JBController.claimTokensFor(address holder, uint256 projectId, uint256 tokenCount, address beneficiary)`
367
+
368
+ **Who can call**: The credit holder or address with `CLAIM_TOKENS` permission.
369
+
370
+ **Parameters**:
371
+ - `holder` -- Address whose credits to convert
372
+ - `projectId` -- Target project
373
+ - `tokenCount` -- Number of credits to convert to ERC-20
374
+ - `beneficiary` -- Address to receive the ERC-20 tokens
375
+
376
+ **State changes**:
377
+ 1. `JBTokens.creditBalanceOf[holder][projectId]` decreased by `tokenCount`
378
+ 2. ERC-20 tokens minted to `beneficiary` for `tokenCount`
379
+
380
+ **Edge cases**:
381
+ - Requires an ERC-20 token to be deployed for the project (reverts otherwise)
382
+ - Credits and ERC-20 tokens are fungible -- this is a one-way conversion from internal credits to on-chain ERC-20
383
+ - Does not require any ruleset flag (always allowed if ERC-20 exists)
384
+
385
+ ---
386
+
387
+ ## 14. Process Held Fees
388
+
389
+ **Entry point**: `JBMultiTerminal.processHeldFeesOf(uint256 projectId, address token, uint256 count)`
390
+
391
+ **Who can call**: Anyone.
392
+
393
+ **Parameters**:
394
+ - `projectId` -- Project whose held fees to process
395
+ - `token` -- Token the fees are denominated in
396
+ - `count` -- Maximum number of held fees to process
397
+
398
+ **State changes**:
399
+ 1. For each processable fee (unlocked, i.e., `unlockTimestamp <= block.timestamp`):
400
+ - Fee entry deleted from `_heldFeesOf` array
401
+ - `_nextHeldFeeIndexOf` incremented
402
+ - Fee amount sent to project #1's terminal via `_processFee` (try-catch)
403
+ - On failure: fee amount returned to project's balance
404
+ 2. If all fees processed: array and index are reset to 0
405
+
406
+ **Events**: `ProcessFee(projectId, token, amount, wasHeld, beneficiary, caller)` per fee, or `FeeReverted(...)` on failure
407
+
408
+ **Edge cases**:
409
+ - Fees unlock after 28 days from when they were held
410
+ - Processing stops at the first locked fee (fees are sequential)
411
+ - Re-reads storage index each iteration (reentrancy-safe)
412
+ - If fee terminal for project #1 does not exist for the token, `_processFee` reverts (caught by try-catch, returns to project balance)
413
+
414
+ ---
415
+
416
+ ## 15. Add Price Feeds
417
+
418
+ **Entry point**: `JBController.addPriceFeed(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency, IJBPriceFeed feed)`
419
+
420
+ **Who can call**: Project owner or address with `ADD_PRICE_FEED` permission.
421
+
422
+ **Parameters**:
423
+ - `projectId` -- Project the feed applies to (0 for protocol-wide default, owner-only)
424
+ - `pricingCurrency` -- Currency the feed's output is in
425
+ - `unitCurrency` -- Currency being priced
426
+ - `feed` -- The price feed contract address
427
+
428
+ **State changes**:
429
+ 1. `JBPrices` stores the feed for the `(projectId, pricingCurrency, unitCurrency)` triple
430
+ 2. Feed is **immutable** once set -- cannot be replaced or removed
431
+
432
+ **Events**: Emitted by `JBPrices`
433
+
434
+ **Edge cases**:
435
+ - Requires `allowAddPriceFeed` in current ruleset
436
+ - Protocol-wide defaults (`projectId = 0`) can only be set by the `JBPrices` owner
437
+ - `JBPrices` auto-calculates inverse: if A->B exists, B->A is derived. If both explicit and inverse exist, explicit takes priority.
438
+ - Lookup order: project-specific -> project-specific inverse -> default (projectId=0) -> default inverse
439
+
440
+ ---
441
+
442
+ ## 16. Set Permissions
443
+
444
+ **Entry point**: `JBPermissions.setPermissionsFor(address account, JBPermissionsData permissionsData)`
445
+
446
+ **Who can call**: The `account` itself, or a ROOT operator for the project (with restrictions).
447
+
448
+ **Parameters**:
449
+ - `account` -- The account granting permissions
450
+ - `permissionsData.operator` -- Address receiving the permissions
451
+ - `permissionsData.projectId` -- Project scope (0 = wildcard, all projects)
452
+ - `permissionsData.permissionIds` -- Array of `uint8` permission IDs to grant
453
+
454
+ **State changes**:
455
+ 1. `permissionsOf[operator][account][projectId]` set to packed `uint256` bitmap
456
+
457
+ **Events**: `OperatorPermissionsSet(operator, account, projectId, permissionIds, packed, caller)`
458
+
459
+ **Edge cases**:
460
+ - Permission ID 0 cannot be set (reserved, always `NoZeroPermission` revert)
461
+ - ROOT (ID 1) cannot be set for wildcard `projectId = 0` (`CantSetRootPermissionForWildcardProject`)
462
+ - ROOT operators can set non-ROOT permissions for others on their scoped project
463
+ - ROOT operators CANNOT grant ROOT to other addresses
464
+ - ROOT operators CANNOT set permissions for wildcard `projectId = 0`
465
+ - Setting permissions replaces the entire bitmap (not additive) -- passing an empty array clears all permissions
466
+
467
+ ---
468
+
469
+ ## 17. Transfer Project Ownership
470
+
471
+ **Entry point**: `JBProjects.transferFrom(address from, address to, uint256 tokenId)` (standard ERC-721)
472
+
473
+ **Who can call**: The current owner, an approved address, or an operator approved for all.
474
+
475
+ **Parameters**:
476
+ - `from` -- Current owner
477
+ - `to` -- New owner
478
+ - `tokenId` -- The project ID (same as the ERC-721 token ID)
479
+
480
+ **State changes**:
481
+ 1. ERC-721 ownership transferred
482
+ 2. All `PROJECTS.ownerOf(projectId)` calls now return the new owner
483
+ 3. All permission checks that reference the owner now apply to the new owner
484
+
485
+ **Edge cases**:
486
+ - This is a standard ERC-721 transfer. All ERC-721 rules apply (approval, operator, etc.)
487
+ - **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.
488
+ - The new owner immediately gets full control: queue rulesets, set terminals, set splits, etc.
489
+ - Transferring to `address(0)` is prevented by OpenZeppelin's ERC-721 implementation
490
+
491
+ ---
492
+
493
+ ## 18. Add to Balance (Without Minting)
494
+
495
+ **Entry point**: `JBMultiTerminal.addToBalanceOf(uint256 projectId, address token, uint256 amount, bool shouldReturnHeldFees, string memo, bytes metadata)`
496
+
497
+ **Who can call**: Anyone.
498
+
499
+ **Parameters**:
500
+ - `projectId` -- Project to add funds to
501
+ - `token` -- Token to add
502
+ - `amount` -- Amount to add (uses `msg.value` for native token)
503
+ - `shouldReturnHeldFees` -- If true, uses the added amount to return held fees (reducing the fee burden)
504
+ - `memo`, `metadata` -- Arbitrary data
505
+
506
+ **State changes**:
507
+ 1. Tokens transferred to terminal
508
+ 2. If `shouldReturnHeldFees`: iterates held fees, returns fees proportional to amount added
509
+ 3. `JBTerminalStore.balanceOf[terminal][projectId][token]` incremented by `amount + returnedFees`
510
+
511
+ **Events**: `AddToBalance(projectId, amount, returnedFees, memo, metadata, caller)`
512
+
513
+ **Edge cases**:
514
+ - Does NOT mint tokens -- purely adds to balance. This increases surplus, which increases cash out value for existing holders.
515
+ - `shouldReturnHeldFees = true` partially or fully returns held fees. The returned fee amount is added to the project balance on top of the deposited amount.
516
+ - Used by terminal migration (`migrateBalanceOf`) and split payouts (when `preferAddToBalance = true`)
517
+
518
+ ---
519
+
520
+ ## 19. Transfer Credits
521
+
522
+ **Entry point**: `JBController.transferCreditsFrom(address holder, uint256 projectId, address recipient, uint256 creditCount)`
523
+
524
+ **Who can call**: The credit holder or address with `TRANSFER_CREDITS` permission.
525
+
526
+ **Parameters**:
527
+ - `holder` -- Address transferring credits
528
+ - `projectId` -- Project whose credits are being transferred
529
+ - `recipient` -- Address to receive credits
530
+ - `creditCount` -- Number of credits to transfer
531
+
532
+ **State changes**:
533
+ 1. `JBTokens.creditBalanceOf[holder][projectId]` decreased
534
+ 2. `JBTokens.creditBalanceOf[recipient][projectId]` increased
535
+
536
+ **Events**: Emitted by `JBTokens`
537
+
538
+ **Edge cases**:
539
+ - Reverts if `pauseCreditTransfers` is set in current ruleset
540
+ - Credits are internal -- they are NOT ERC-20 tokens. Use `claimTokensFor` to convert credits to ERC-20.
541
+ - This only transfers credits, not ERC-20 tokens. ERC-20 tokens are transferred via standard ERC-20 `transfer`/`transferFrom`.
542
+
543
+ ---
544
+
545
+ ## 20. Set Project URI
546
+
547
+ **Entry point**: `JBController.setUriOf(uint256 projectId, string uri)`
548
+
549
+ **Who can call**: Project owner or address with `SET_PROJECT_URI` permission.
550
+
551
+ **State changes**: `uriOf[projectId] = uri`
552
+
553
+ **Events**: `SetUri(projectId, uri, caller)`
554
+
555
+ ---
556
+
557
+ ## 21. Set Custom Token
558
+
559
+ **Entry point**: `JBController.setTokenFor(uint256 projectId, IJBToken token)`
560
+
561
+ **Who can call**: Project owner or address with `SET_TOKEN` permission.
562
+
563
+ **State changes**: `JBTokens.tokenOf[projectId] = token`
564
+
565
+ **Edge cases**:
566
+ - Requires `allowSetCustomToken` in current or upcoming ruleset
567
+ - Can only be called if no token is currently set for the project
568
+ - The token must conform to `IJBToken` interface (18 decimals required)
569
+
570
+ ---
571
+
572
+ ## 22. Set Token Metadata
573
+
574
+ **Entry point**: `JBController.setTokenMetadataOf(uint256 projectId, string name, string symbol)`
575
+
576
+ **Who can call**: Project owner or address with `SET_TOKEN_METADATA` permission.
577
+
578
+ **State changes**: Updates the ERC-20 token's name and symbol via `JBERC20.setNameAndSymbol()`
579
+
580
+ ---
581
+
582
+ ## 23. Update Ruleset Weight Cache
583
+
584
+ **Entry point**: `JBRulesets.updateRulesetWeightCache(uint256 projectId, uint256 rulesetId)`
585
+
586
+ **Who can call**: Anyone.
587
+
588
+ **Parameters**:
589
+ - `projectId` -- Target project
590
+ - `rulesetId` -- The specific ruleset to update cache for
591
+
592
+ **State changes**:
593
+ 1. `_weightCacheOf[projectId][rulesetId].weight` updated to current decayed weight
594
+ 2. `_weightCacheOf[projectId][rulesetId].weightCutMultiple` updated
595
+
596
+ **Events**: `WeightCacheUpdated(projectId, weight, weightCutMultiple, caller)`
597
+
598
+ **Edge cases**:
599
+ - Required for projects with >20,000 cycles (otherwise `currentOf()` reverts with `WeightCacheRequired`)
600
+ - Advances the cache by at most `_WEIGHT_CUT_MULTIPLE_CACHE_LOOKUP_THRESHOLD` (20,000) cycles per call
601
+ - Multiple calls needed to fully catch up for very large cycle gaps
602
+ - No-op if `duration == 0` or `weightCutPercent == 0`
603
+
604
+ ---
605
+
606
+ ## 24. Add Accounting Contexts
607
+
608
+ **Entry point**: `JBMultiTerminal.addAccountingContextsFor(uint256 projectId, JBAccountingContext[] accountingContexts)`
609
+
610
+ **Who can call**: Project owner, address with `ADD_ACCOUNTING_CONTEXTS` permission, or the project's controller.
611
+
612
+ **State changes**:
613
+ 1. For each context: validates token decimals, stores `_accountingContextForTokenOf[projectId][token]`
614
+ 2. Appends to `_accountingContextsOf[projectId]` array
615
+
616
+ **Events**: `SetAccountingContext(projectId, context, caller)` per token
617
+
618
+ **Edge cases**:
619
+ - Requires `allowAddAccountingContext` in current ruleset (if ruleset exists)
620
+ - Token cannot be added twice (`AccountingContextAlreadySet`)
621
+ - Currency must be non-zero (`ZeroAccountingContextCurrency`)
622
+ - For non-native tokens: decimals are validated against the token's `decimals()` function. Tokens that revert on `decimals()` bypass validation (caller responsible).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
27
27
  },
28
28
  "dependencies": {
29
- "@bananapus/permission-ids-v6": "^0.0.9",
29
+ "@bananapus/permission-ids-v6": "^0.0.10",
30
30
  "@chainlink/contracts": "^1.3.0",
31
31
  "@openzeppelin/contracts": "^5.6.1",
32
32
  "@prb/math": "^4.1.1",
@@ -254,6 +254,9 @@ contract DeployPeriphery is Script, Sphinx {
254
254
  });
255
255
  } else if (block.chainid == 84_532) {
256
256
  usdc = address(0x036CbD53842c5426634e7929541eC2318f3dCF7e);
257
+ // TODO: Verify this feed address — 0xd30e2101... is the Arbitrum Sepolia ETH/USD feed and is likely
258
+ // incorrect for Base Sepolia USDC/USD. Replace with the correct Chainlink Base Sepolia USDC/USD address
259
+ // before deploying. Testnet-only; does not affect mainnet deployments.
257
260
  usdcFeed = new JBChainlinkV3PriceFeed({
258
261
  feed: AggregatorV3Interface(address(0xd30e2101a97dcbAeBCBC04F14C3f624E67A35165)),
259
262
  threshold: 86_400 seconds
@@ -281,14 +284,17 @@ contract DeployPeriphery is Script, Sphinx {
281
284
 
282
285
  core.prices
283
286
  .addPriceFeedFor({
284
- // forge-lint: disable-next-line(unsafe-typecast)
285
287
  projectId: 0,
286
288
  pricingCurrency: JBCurrencyIds.USD,
289
+ // forge-lint: disable-next-line(unsafe-typecast)
287
290
  unitCurrency: uint32(uint160(usdc)),
288
291
  feed: usdcFeed
289
292
  });
290
293
  }
291
294
 
295
+ /// @dev This helper predicts addresses using the Arachnid CREATE2 deployer, but actual deployments go through
296
+ /// Sphinx (which uses a different deployer). As a result, this guard will never detect Sphinx-deployed contracts
297
+ /// and is only effective for contracts deployed via the Arachnid deployer directly.
292
298
  function _isDeployed(bytes32 salt, bytes memory creationCode, bytes memory arguments) internal view returns (bool) {
293
299
  address _deployedTo = vm.computeCreate2Address({
294
300
  salt: salt,
@@ -95,6 +95,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
95
95
  IJBTokens public immutable override TOKENS;
96
96
 
97
97
  /// @notice The address of the contract that manages omnichain ruleset ops.
98
+ /// @dev This is a deterministic CREATE2 deployment; bytecode verification at runtime would add
99
+ /// gas
100
+ /// cost for marginal security benefit since the address is verified at deploy time.
101
+ /// @dev Trust assumption is bounded: the operator can only queue rulesets that the omnichain
102
+ /// deployer's logic permits. It cannot drain funds or bypass access controls.
98
103
  address public immutable override OMNICHAIN_RULESET_OPERATOR;
99
104
 
100
105
  //*********************************************************************//