@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
package/ADMINISTRATION.md CHANGED
@@ -148,7 +148,7 @@ Admin privileges and their scope in nana-core-v6.
148
148
 
149
149
  | Function | Required Role | Permission ID | Scope | What It Does |
150
150
  |----------|--------------|---------------|-------|-------------|
151
- | `setSplitGroupsOf` | Project's controller, OR the address whose first 160 bits match the group ID (for self-namespaced splits) | N/A (onlyControllerOf or msg.sender namespace) | Per project | Sets split groups for a project/ruleset. Must preserve any currently locked splits. Percentage total per group must not exceed 100%. |
151
+ | `setSplitGroupsOf` | Project's controller, OR the address whose lower 160 bits match the group ID AND the upper 96 bits are non-zero (for self-namespaced splits) | N/A (onlyControllerOf or msg.sender namespace) | Per project | Sets split groups for a project/ruleset. Must preserve any currently locked splits. Percentage total per group must not exceed 100%. GroupIds with zero upper 96 bits (e.g., `uint256(uint160(tokenAddress))`) are protocol-reserved for terminal payout groups and always require controller authorization. |
152
152
 
153
153
  ### JBFundAccessLimits
154
154
 
package/ARCHITECTURE.md CHANGED
@@ -68,7 +68,8 @@ Holder -> JBMultiTerminal.cashOutTokensOf()
68
68
  -> JBController.burnTokensOf()
69
69
  -> Transfer reclaimed tokens to beneficiary
70
70
  -> [Optional] Cash out hooks execute
71
- -> Take fees (2.5% to project #1)
71
+ -> Take fees (2.5% to project #1) if cashOutTaxRate > 0
72
+ OR if cashOutTaxRate == 0 and project has unconsumed fee-free surplus (_feeFreeSurplusOf)
72
73
  ```
73
74
 
74
75
  ### Payout Flow
@@ -0,0 +1,342 @@
1
+ # nana-core-v6 -- Audit Instructions
2
+
3
+ You are auditing the core Juicebox V6 protocol -- a modular system for programmable treasuries with configurable rulesets, bonding-curve cash outs, split-based payouts, and a compositional hook system. Your goal is to find bugs that lose funds, break invariants, or enable unauthorized access.
4
+
5
+ Read [RISKS.md](./RISKS.md) for known risks, trust model, and reentrancy analysis. Then come back here.
6
+
7
+ ## Architecture Overview
8
+
9
+ 16 contracts, ~6,300 lines in main contracts. All contracts use Solidity 0.8.26.
10
+
11
+ ```
12
+ JBProjects (ERC-721)
13
+ |
14
+ JBDirectory
15
+ / | \
16
+ JBController JBMultiTerminal JBPermissions
17
+ / | \ |
18
+ JBRulesets | JBTokens JBTerminalStore
19
+ | |
20
+ JBSplits JBPrices
21
+ |
22
+ JBFundAccessLimits
23
+ ```
24
+
25
+ ### Contract Roles
26
+
27
+ | Contract | Lines | Role | Calls |
28
+ |----------|-------|------|-------|
29
+ | **JBMultiTerminal** | ~2024 | Payment terminal. Handles pay, cash out, payouts, surplus allowance, fees. Multi-token. Permit2 integration. | Store, Controller, Splits, Directory, Prices |
30
+ | **JBController** | ~1186 | Orchestrator. Project lifecycle, ruleset queuing, token minting/burning, reserved token distribution. ERC-2771 meta-tx. | Rulesets, Tokens, Splits, FundAccessLimits, Directory, Prices |
31
+ | **JBTerminalStore** | ~800 | Bookkeeping. Balances, payout limit tracking, surplus calculation, bonding curve reclaim math. Data hook integration point. | Rulesets, Prices, Directory |
32
+ | **JBRulesets** | ~1093 | Ruleset lifecycle. Linked-list via `basedOnId`. Weight decay with cache (20k iteration threshold). Approval hooks. Bit-packed storage. | Directory (via JBControlled) |
33
+ | **JBDirectory** | ~300 | Routes projects to terminals and controllers. Migration lifecycle (before/after). | Projects, Permissions |
34
+ | **JBTokens** | ~300 | Dual token system: credits (internal) + ERC-20. Credits burned first on burn. 18-decimal requirement. | JBERC20 (clone) |
35
+ | **JBSplits** | ~300 | Packed split storage per project/ruleset/group. Locked splits enforcement. Fallback to ruleset 0. | -- |
36
+ | **JBFundAccessLimits** | ~200 | Payout limits and surplus allowances per terminal/token/currency. Strictly increasing currency order. | -- |
37
+ | **JBPrices** | ~200 | Price feed registry. Project-specific + default fallback. Immutable once set. Inverse auto-calculation. | Chainlink feeds |
38
+ | **JBPermissions** | ~260 | 256-bit packed permission bitmap. ROOT (1) grants all. Wildcard projectId=0. ERC-2771. | -- |
39
+ | **JBProjects** | ~100 | ERC-721 project ownership. Auto-incrementing IDs. | -- |
40
+ | **JBERC20** | ~200 | Cloneable ERC20Votes+Permit. Owned by JBTokens. Deployed via `Clones.clone()`. | -- |
41
+ | **JBFeelessAddresses** | ~50 | Fee-exempt address registry. Owner-only. | -- |
42
+ | **JBDeadline** | ~100 | Approval hook. Rejects rulesets queued within DURATION seconds of start. Ships as 3h, 1d, 3d, 7d variants. | -- |
43
+ | **JBChainlinkV3PriceFeed** | ~80 | Chainlink v3 feed with staleness threshold. Rejects negative/zero/incomplete. | Chainlink AggregatorV3 |
44
+ | **JBChainlinkV3SequencerPriceFeed** | ~120 | L2 sequencer-aware Chainlink feed. Grace period after restart. | Chainlink AggregatorV3 + Sequencer feed |
45
+
46
+ ## Key Flows
47
+
48
+ ### Payment Flow (`pay`)
49
+
50
+ ```
51
+ User -> JBMultiTerminal.pay()
52
+ -> _acceptFundsFor() // Transfer tokens in (or accept msg.value)
53
+ -> [Optional] Permit2 decode from metadata
54
+ -> JBTerminalStore.recordPaymentFrom()
55
+ -> RULESETS.currentOf() // Get current ruleset
56
+ -> [If useDataHookForPay] dataHook.beforePayRecordedWith() -> returns (weight, hookSpecs)
57
+ -> Calculate tokenCount = mulDiv(amount.value, weight, weightRatio)
58
+ -> Increment balanceOf[terminal][projectId][token]
59
+ -> Deduct hook specification amounts from balance
60
+ -> JBController.mintTokensOf()
61
+ -> Calculate reserved vs beneficiary tokens
62
+ -> TOKENS.mintFor(beneficiary)
63
+ -> Increment pendingReservedTokenBalanceOf
64
+ -> [If hookSpecs] _fulfillPayHookSpecificationsFor()
65
+ -> For each spec: transfer funds, call hook.afterPayRecordedWith()
66
+ ```
67
+
68
+ **State at hook execution time**: Store balance updated (post-hook-deductions). Tokens minted. Pending reserved tokens accumulated. Hooks see the fully settled state.
69
+
70
+ ### Cash Out Flow (`cashOutTokensOf`)
71
+
72
+ ```
73
+ Holder -> JBMultiTerminal.cashOutTokensOf()
74
+ -> JBTerminalStore.recordCashOutFor()
75
+ -> RULESETS.currentOf()
76
+ -> Calculate surplus (local or total, depending on useTotalSurplusForCashOuts)
77
+ -> Get totalSupply (including pending reserved tokens)
78
+ -> [If useDataHookForCashOut] dataHook.beforeCashOutRecordedWith()
79
+ -> Returns (cashOutTaxRate, cashOutCount, totalSupply, hookSpecs) // ALL overrideable
80
+ -> JBCashOuts.cashOutFrom(surplus, cashOutCount, totalSupply, cashOutTaxRate)
81
+ -> Deduct reclaimAmount + hookSpec amounts from balance
82
+ -> JBController.burnTokensOf()
83
+ -> Transfer reclaimAmount to beneficiary // BEFORE hooks execute
84
+ -> [If hookSpecs] _fulfillCashOutHookSpecificationsFor()
85
+ -> For each spec: transfer funds, call hook.afterCashOutRecordedWith()
86
+ -> _takeFeeFrom() on total amount eligible for fees
87
+ -> Fee charged if cashOutTaxRate > 0, OR if cashOutTaxRate == 0 and _feeFreeSurplusOf[projectId][token] > 0 (up to that amount)
88
+ -> Fee skipped if beneficiary is feeless
89
+ ```
90
+
91
+ **Critical note**: The beneficiary receives the reclaim amount BEFORE cash out hooks execute. Fees are taken AFTER hooks. `_feeFreeSurplusOf[projectId][token]` accumulates the value of fee-free intra-terminal payouts. During zero-tax cashout, the 2.5% fee applies only up to this surplus amount (then depletes it), preventing a round-trip fee bypass (payout via same terminal → zero-tax cashout) while scoping fees precisely to the fee-free inflow.
92
+
93
+ ### Payout Flow (`sendPayoutsOf`)
94
+
95
+ ```
96
+ Anyone -> JBMultiTerminal.sendPayoutsOf()
97
+ -> [If ownerMustSendPayouts] Require SEND_PAYOUTS permission
98
+ -> JBTerminalStore.recordPayoutFor()
99
+ -> Deduct amount from balance
100
+ -> Increment usedPayoutLimitOf
101
+ -> Validate against FUND_ACCESS_LIMITS.payoutLimitOf()
102
+ -> _sendPayoutsToSplitGroupOf()
103
+ -> Get splits from JBSplits.splitsOf()
104
+ -> For each split (try-catch per split):
105
+ -> Split to hook: deduct fee, call hook.processSplitWith() with funds
106
+ -> Split to project: deduct fee, call terminal.pay() or terminal.addToBalanceOf()
107
+ -> Split to address: deduct fee, transfer directly
108
+ -> On failure: return amount to project balance, emit PayoutReverted
109
+ -> Send leftover to project owner (try-catch)
110
+ -> _takeFeeFrom() on total fee-eligible amount
111
+ ```
112
+
113
+ **Key detail**: Payout limit is consumed even if splits fail. This is by design -- the project authorized the distribution. Failed splits return funds to the project balance.
114
+
115
+ ### Surplus Allowance Flow (`useAllowanceOf`)
116
+
117
+ ```
118
+ Owner -> JBMultiTerminal.useAllowanceOf()
119
+ -> Require USE_ALLOWANCE permission
120
+ -> JBTerminalStore.recordUsedAllowanceOf()
121
+ -> Deduct from balance
122
+ -> Increment usedSurplusAllowanceOf
123
+ -> Validate against FUND_ACCESS_LIMITS.surplusAllowanceOf()
124
+ -> Validate amount <= current surplus
125
+ -> _takeFeeFrom() (fee subtracted from amount)
126
+ -> Transfer net amount to beneficiary
127
+ ```
128
+
129
+ ### Reserved Token Distribution (`sendReservedTokensToSplitsOf`)
130
+
131
+ ```
132
+ Anyone -> JBController.sendReservedTokensToSplitsOf()
133
+ -> Read pendingReservedTokenBalanceOf (revert if 0)
134
+ -> Zero out pendingReservedTokenBalanceOf // BEFORE minting
135
+ -> TOKENS.mintFor(controller, tokenCount) // Mint to controller
136
+ -> For each reserved token split:
137
+ -> Split to hook: transfer tokens, call hook.processSplitWith()
138
+ -> Split to project with terminal: call terminal.pay() with tokens (try-catch)
139
+ -> Split to address: transfer tokens directly
140
+ -> Split to 0xdead: burn tokens
141
+ -> Send leftover tokens to project owner
142
+ ```
143
+
144
+ ## Storage Layout and State Management
145
+
146
+ ### JBTerminalStore -- The Source of Truth
147
+
148
+ All financial state lives in `JBTerminalStore`:
149
+
150
+ ```solidity
151
+ // Real balances -- the money
152
+ mapping(terminal => mapping(projectId => mapping(token => uint256))) public balanceOf;
153
+
154
+ // Consumption tracking -- limits enforcement
155
+ mapping(terminal => mapping(projectId => mapping(token => mapping(cycleNumber => mapping(currency => uint256)))))
156
+ public usedPayoutLimitOf;
157
+
158
+ mapping(terminal => mapping(projectId => mapping(token => mapping(rulesetId => mapping(currency => uint256)))))
159
+ public usedSurplusAllowanceOf;
160
+ ```
161
+
162
+ Key observations:
163
+ - `balanceOf` is keyed by terminal address -- anyone can call `recordAddedBalanceFor`, but only registered terminals meaningfully interact
164
+ - `usedPayoutLimitOf` resets each cycle (keyed by `cycleNumber`). Payout limits refresh when the ruleset cycles.
165
+ - `usedSurplusAllowanceOf` resets each ruleset (keyed by `rulesetId`). Surplus allowances refresh when a new ruleset takes effect.
166
+
167
+ ### JBRulesets -- Bit-Packed State
168
+
169
+ Rulesets are stored across three packed storage slots per ruleset:
170
+
171
+ | Slot | Name | Contents |
172
+ |------|------|----------|
173
+ | 1 | `_packedIntrinsicPropertiesOf` | `weight` (112 bits), `basedOnId` (48), `start` (48), `cycleNumber` (48) |
174
+ | 2 | `_packedUserPropertiesOf` | `approvalHook` (160), `duration` (32), `weightCutPercent` (32) |
175
+ | 3 | `_metadataOf` | 256-bit packed metadata (see below) |
176
+
177
+ ### Metadata Bit Layout (`JBRulesetMetadataResolver`)
178
+
179
+ ```
180
+ Bits 0-3: version (4 bits) -- currently 0
181
+ Bits 4-19: reservedPercent (16 bits, max 10,000)
182
+ Bits 20-35: cashOutTaxRate (16 bits, max 10,000)
183
+ Bits 36-67: baseCurrency (32 bits)
184
+ Bits 68-81: 14 boolean flags (1 bit each):
185
+ pausePay, pauseCreditTransfers, allowOwnerMinting,
186
+ allowSetCustomToken, allowTerminalMigration, allowSetTerminals,
187
+ allowSetController, allowAddAccountingContext, allowAddPriceFeed,
188
+ ownerMustSendPayouts, holdFees, useTotalSurplusForCashOuts,
189
+ useDataHookForPay, useDataHookForCashOut
190
+ Bits 82-241: dataHook address (160 bits)
191
+ Bits 242-255: metadata (14 bits, project-defined)
192
+ ```
193
+
194
+ ## Hook Interfaces
195
+
196
+ Five extension points, ordered by power:
197
+
198
+ | Hook | Interface | Called By | When | Power Level |
199
+ |------|-----------|-----------|------|-------------|
200
+ | **Data Hook (pay)** | `IJBRulesetDataHook.beforePayRecordedWith` | JBTerminalStore | During `recordPaymentFrom` | ABSOLUTE -- controls weight and fund allocation |
201
+ | **Data Hook (cashout)** | `IJBRulesetDataHook.beforeCashOutRecordedWith` | JBTerminalStore | During `recordCashOutFor` | ABSOLUTE -- controls tax rate, count, supply, fund allocation |
202
+ | **Pay Hook** | `IJBPayHook.afterPayRecordedWith` | JBMultiTerminal | After payment recorded + tokens minted | MEDIUM -- receives diverted funds, executes arbitrary logic |
203
+ | **Cash Out Hook** | `IJBCashOutHook.afterCashOutRecordedWith` | JBMultiTerminal | After cash out recorded + tokens burned + beneficiary paid | MEDIUM -- receives diverted funds |
204
+ | **Split Hook** | `IJBSplitHook.processSplitWith` | JBMultiTerminal (payouts) / JBController (reserved tokens) | During payout distribution or reserved token distribution | MEDIUM -- receives split funds |
205
+ | **Approval Hook** | `IJBRulesetApprovalHook.approvalStatusOf` | JBRulesets | During `currentOf()` / `upcomingOf()` | LOW -- can approve/reject/delay rulesets |
206
+
207
+ **Data hook security note**: `beforeCashOutRecordedWith` returns FOUR overrideable values: `cashOutTaxRate`, `cashOutCount`, `totalSupply`, and `hookSpecifications`. A malicious data hook can set `totalSupply = surplus` causing `reclaimAmount = cashOutCount`, completely bypassing the bonding curve.
208
+
209
+ ## Library Dependencies
210
+
211
+ | Library | Used By | Purpose | Audit Focus |
212
+ |---------|---------|---------|-------------|
213
+ | `JBCashOuts` | JBTerminalStore | Bonding curve: `base * [(MAX-tax) + tax*(count/supply)] / MAX`. Binary search for inverse (`minCashOutCountFor`). | Rounding direction in `mulDiv`. Edge cases: count=0, supply=0, taxRate=MAX. |
214
+ | `JBFees` | JBMultiTerminal | Forward: `amount * FEE / MAX_FEE`. Backward: `amount * MAX_FEE / (MAX_FEE - FEE) - amount`. | Consistency between forward and backward. Rounding bounds. |
215
+ | `JBRulesetMetadataResolver` | JBController, JBMultiTerminal, JBTerminalStore | Packs/unpacks 256-bit metadata. Shift/mask operations for each field. | Bit overlap, off-by-one in shifts, mask correctness. |
216
+ | `JBMetadataResolver` | JBMultiTerminal | Variable-length `{id:data}` key-value metadata encoding with lookup table. Used for Permit2 data. | Malformed metadata handling, ID collision. |
217
+ | `JBFixedPointNumber` | JBTerminalStore (via JBSurplus) | Decimal adjustment: `value * 10^targetDecimals / 10^sourceDecimals` with fidelity cap. | Overflow in adjustment, precision loss. |
218
+ | `JBSurplus` | JBTerminalStore | Aggregates surplus across terminals. Calls each terminal's store for balance and payout limits. | Cross-terminal surplus consistency. |
219
+
220
+ ## Key Constants
221
+
222
+ | Constant | Value | Context |
223
+ |----------|-------|---------|
224
+ | `FEE` | 25 | Fee percentage (out of MAX_FEE = 1000) = 2.5% |
225
+ | `MAX_FEE` | 1,000 | 100% fee cap |
226
+ | `MAX_RESERVED_PERCENT` | 10,000 | Basis points (100%) |
227
+ | `MAX_CASH_OUT_TAX_RATE` | 10,000 | Basis points (100%). Rate of 10,000 = nothing reclaimable. Rate of 0 = proportional (1:1). |
228
+ | `MAX_WEIGHT_CUT_PERCENT` | 1,000,000,000 | 9-decimal precision (100%) |
229
+ | `SPLITS_TOTAL_PERCENT` | 1,000,000,000 | 9-decimal precision (100%) |
230
+ | `NATIVE_TOKEN` | `0x...EEEe` | Sentinel address for native ETH (or native token on any chain) |
231
+ | `_FEE_BENEFICIARY_PROJECT_ID` | 1 | Project #1 receives all protocol fees |
232
+ | `_FEE_HOLDING_SECONDS` | 2,419,200 | 28 days |
233
+ | `_WEIGHT_CUT_MULTIPLE_CACHE_LOOKUP_THRESHOLD` | 20,000 | Max weight decay iterations per call |
234
+
235
+ ### Special Values
236
+
237
+ | Value | Context | Meaning |
238
+ |-------|---------|---------|
239
+ | `weight = 0` | Ruleset | No token issuance for payments |
240
+ | `weight = 1` | Ruleset config | Inherit decayed weight from previous ruleset (sentinel) |
241
+ | `duration = 0` | Ruleset | Never expires; immediately replaced when new ruleset queued |
242
+ | `projectId = 0` | Permissions | Wildcard: permission applies to ALL projects. Cannot combine with ROOT. |
243
+ | `rulesetId = 0` | Splits | Fallback split group when no splits set for specific ruleset |
244
+ | `projectId = 0` | Prices | Protocol-wide default price feed (owner-only) |
245
+
246
+ ## Gotchas for Auditors
247
+
248
+ These are the patterns that will trip you up if you are not aware of them:
249
+
250
+ 1. **`controllerOf()` returns `IERC165`, not `address`** -- must cast: `IJBController(address(directory.controllerOf(projectId)))`
251
+ 2. **`primaryTerminalOf()` returns `IJBTerminal`, not `address`** -- must cast
252
+ 3. **`terminalsOf()` returns `IJBTerminal[]`, not `address[]`**
253
+ 4. **`pricePerUnitOf()` is on `IJBPrices`, not `IJBController`**
254
+ 5. **`baseCurrency` (1=ETH, 2=USD) != `JBAccountingContext.currency` (uint32(uint160(token)))** -- two different currency systems. `JBPrices` mediates between them.
255
+ 6. **`groupId` (uint256) != `currency` (uint32)** -- both derived from token address but different bit widths. `groupId = uint256(uint160(token))`, `currency = uint32(uint160(token))`.
256
+ 6b. **`setSplitGroupsOf` self-auth requires non-zero upper 96 bits.** The self-auth path (where `msg.sender` matches the lower 160 bits of the `groupId`) additionally requires `groupId >> 160 != 0`. Bare-address groupIds (upper 96 bits = 0) are protocol-reserved for terminal payout groups and always require controller auth. This prevents accepted token contracts from hijacking payout splits.
257
+ 7. **Empty `fundAccessLimitGroups` = zero payouts, NOT unlimited** -- `sendPayoutsOf` reverts on any amount. Use `uint224.max` for unlimited.
258
+ 8. **`sendPayoutsOf()` reverts when `amount > payout limit`** -- does NOT auto-cap to limit.
259
+ 9. **Cash out tax rate semantics are inverted from what you might expect**: 0% = proportional (1:1) redemption. 100% = nothing reclaimable (all surplus locked).
260
+ 10. **`recordPayoutFor` deducts balance and increments used limit BEFORE validation** -- safe because the entire transaction reverts atomically, but the ordering matters for reentrancy analysis.
261
+ 11. **Try-catch on external calls** -- `_sendPayoutToSplit`, `_processFee`, `executePayReservedTokenToTerminal` all use try-catch. Failed calls return funds to project balance and emit events. This is NOT a bug -- it prevents single-point-of-failure DoS.
262
+ 12. **Credits are burned before ERC-20 tokens** in `JBTokens.burnFrom()`.
263
+ 13. **`JBERC20` is cloned via `Clones.clone()`** -- constructor sets invalid name/symbol; real values set in `initialize()`.
264
+ 14. **Named returns auto-return** -- several functions use named return variables without explicit `return` statements.
265
+
266
+ ## Priority Areas to Audit
267
+
268
+ Ordered by blast radius:
269
+
270
+ | Priority | Target | Why |
271
+ |----------|--------|-----|
272
+ | 1 | **JBMultiTerminal + JBTerminalStore** | All funds flow through here. No reentrancy guard. CEI ordering is the only defense. |
273
+ | 2 | **JBCashOuts bonding curve math** | Determines how much holders can extract. Edge cases with 0 supply, 0 count, MAX tax rate. `mulDiv` rounding direction. |
274
+ | 3 | **Data hook integration** | Data hooks have absolute control. Verify all constraints on hook return values are enforced. |
275
+ | 4 | **JBRulesets weight decay + transition logic** | Complex linked-list traversal with approval hook fallback. Weight cache threshold. Timing at ruleset boundaries. |
276
+ | 5 | **Fee arithmetic (JBFees)** | Forward/backward consistency. Held fee lifecycle. Fee return calculations in `_returnHeldFees`. |
277
+ | 6 | **Cross-terminal surplus** (`JBSurplus`) | Aggregation across terminals with price conversion. Verify surplus cannot be inflated across terminals. |
278
+ | 7 | **Permission system** | ROOT escalation, wildcard project scope, ERC-2771 spoofing. |
279
+ | 8 | **Permit2 metadata parsing** | Malformed metadata, amount mismatch between permit and payment. |
280
+
281
+ ## How to Run Tests
282
+
283
+ ```bash
284
+ cd nana-core-v6
285
+ npm install
286
+ forge build
287
+ forge test
288
+
289
+ # Run with high verbosity for debugging
290
+ forge test -vvvv --match-test testExploitName
291
+
292
+ # Write a PoC
293
+ forge test --match-path test/audit/ExploitPoC.t.sol -vvv
294
+
295
+ # Run invariant tests
296
+ forge test --match-contract Invariant
297
+
298
+ # Gas analysis
299
+ forge test --gas-report
300
+ ```
301
+
302
+ The existing test suite has 165 test files including:
303
+ - **Integration tests**: Full flow tests for pay, cash out, payouts
304
+ - **Formal property tests**: 7 bonding curve properties + 6 fee properties
305
+ - **Invariant tests**: TerminalStore (5), Phase3Deep (8), Rulesets (4), Tokens (4)
306
+ - **Economic simulation**: 3 projects, 10 actors, 15 operations, 6 invariants
307
+ - **Flash loan attack tests**: 12 attack vectors in `FlashLoanAttacks.t.sol`
308
+
309
+ Review the invariant tests to understand what is already proven -- then try to break those invariants with configurations the tests do not cover.
310
+
311
+ ## Invariants to Verify
312
+
313
+ These MUST hold. If you can break any of them, it is a finding:
314
+
315
+ 1. **Balance conservation**: `terminal.balance(token) >= sum(store.balanceOf(projectId, terminal, token))` for all projects
316
+ 2. **Inflow >= Outflow**: Total funds received by a project >= total funds distributed
317
+ 3. **Fee monotonicity**: Project #1's balance only increases over time
318
+ 4. **Token supply consistency**: `JBTokens.totalSupplyOf(projectId) == creditSupply + erc20.totalSupply()`
319
+ 5. **Ruleset existence**: After `launchProjectFor()`, `currentOf(projectId)` always returns a valid ruleset
320
+ 6. **No flash-loan profit**: Pay + cash out in same block should never yield more than was paid (minus fees)
321
+ 7. **Payout limits**: A project cannot extract more than its configured payout limit per ruleset cycle
322
+ 8. **Surplus allowance**: A project cannot withdraw more than its configured surplus allowance per ruleset
323
+
324
+ ## How to Report Findings
325
+
326
+ For each finding:
327
+
328
+ 1. **Title** -- one line, starts with severity (CRITICAL/HIGH/MEDIUM/LOW)
329
+ 2. **Affected contract(s)** -- exact file path and line numbers
330
+ 3. **Description** -- what is wrong, in plain language
331
+ 4. **Trigger sequence** -- step-by-step, minimal steps to reproduce
332
+ 5. **Impact** -- what an attacker gains, what a user loses (with numbers if possible)
333
+ 6. **Proof** -- code trace showing the exact execution path, or a Foundry test
334
+ 7. **Fix** -- minimal code change that resolves the issue
335
+
336
+ **Severity guide:**
337
+ - **CRITICAL**: Direct fund loss, permanent DoS, or system insolvency. Exploitable with no preconditions.
338
+ - **HIGH**: Conditional fund loss, privilege escalation, or broken core invariant. Requires specific but realistic setup.
339
+ - **MEDIUM**: Value leakage, griefing with cost to attacker, incorrect accounting, degraded functionality.
340
+ - **LOW**: Informational, cosmetic inconsistency, edge-case-only with no material impact.
341
+
342
+ Go break it.