@bananapus/core-v6 0.0.30 → 0.0.32

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 (49) 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 +45 -17
  16. package/src/JBDirectory.sol +26 -13
  17. package/src/JBFundAccessLimits.sol +28 -7
  18. package/src/JBMultiTerminal.sol +180 -86
  19. package/src/JBPermissions.sol +17 -17
  20. package/src/JBRulesets.sol +82 -23
  21. package/src/JBSplits.sol +31 -12
  22. package/src/JBTerminalStore.sol +137 -53
  23. package/src/JBTokens.sol +5 -2
  24. package/src/abstract/JBControlled.sol +10 -3
  25. package/src/abstract/JBPermissioned.sol +1 -1
  26. package/src/interfaces/IJBRulesetDataHook.sol +5 -4
  27. package/src/libraries/JBCashOuts.sol +1 -1
  28. package/src/libraries/JBConstants.sol +1 -1
  29. package/src/libraries/JBCurrencyIds.sol +1 -1
  30. package/src/libraries/JBFees.sol +1 -1
  31. package/src/libraries/JBFixedPointNumber.sol +1 -1
  32. package/src/libraries/JBMetadataResolver.sol +5 -2
  33. package/src/libraries/JBPayoutSplitGroupLib.sol +7 -2
  34. package/src/libraries/JBRulesetMetadataResolver.sol +1 -1
  35. package/src/libraries/JBSplitGroupIds.sol +1 -1
  36. package/src/libraries/JBSurplus.sol +5 -2
  37. package/src/structs/JBSplit.sol +4 -1
  38. package/test/TestForwardedTokenConsumption.sol +419 -0
  39. package/test/audit/CrossTerminalSurplusSpoof.t.sol +140 -0
  40. package/test/audit/CycledSurplusAllowanceReset.t.sol +184 -0
  41. package/test/units/static/JBController/TestPreviewMintOf.sol +5 -4
  42. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +15 -12
  43. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +6 -0
  44. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +3 -0
  45. package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +3 -0
  46. package/test/units/static/JBMultiTerminal/TestPay.sol +7 -15
  47. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +1 -1
  48. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +1 -1
  49. package/CHANGE_LOG.md +0 -479
@@ -1,429 +1,150 @@
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, ~8,100 lines in main contracts. All contracts use Solidity 0.8.28.
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, and previews (`previewPayFor`, `previewCashOutFrom`). Multi-token. Permit2 integration. | Store, Controller, Splits, Directory, Prices |
30
- | **JBController** | ~1,253 | Orchestrator. Project lifecycle, ruleset queuing, token minting/burning, reserved token distribution, mint preview (`previewMintOf`). ERC-2771 meta-tx. | Rulesets, Tokens, Splits, FundAccessLimits, Directory, Prices |
31
- | **JBTerminalStore** | ~1,267 | 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** | ~344 | Routes projects to terminals and controllers. Migration lifecycle (before/after). | Projects, Permissions |
34
- | **JBTokens** | ~415 | Dual token system: credits (internal) + ERC-20. Credits burned first on burn. 18-decimal requirement. | JBERC20 (clone) |
35
- | **JBSplits** | ~333 | Packed split storage per project/ruleset/group. Locked splits enforcement. Fallback to ruleset 0. | -- |
36
- | **JBFundAccessLimits** | ~318 | Payout limits and surplus allowances per terminal/token/currency. Strictly increasing currency order. | -- |
37
- | **JBPrices** | ~233 | 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** | ~126 | ERC-721 project ownership. Auto-incrementing IDs. | -- |
40
- | **JBERC20** | ~144 | Cloneable ERC20Votes+Permit. Owned by JBTokens. Deployed via `Clones.clone()`. | -- |
41
- | **JBFeelessAddresses** | ~50 | Fee-exempt address registry. Owner-only. | -- |
42
- | **JBDeadline** | ~76 | Approval hook. Rejects rulesets queued within DURATION seconds of start. Ships as 3h, 1d, 3d, 7d variants. | -- |
43
- | **JBChainlinkV3PriceFeed** | ~74 | Chainlink v3 feed with staleness threshold. Rejects negative/zero/incomplete. | Chainlink AggregatorV3 |
44
- | **JBChainlinkV3SequencerPriceFeed** | ~75 | 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. After any outflow (payouts, `useAllowanceOf`, non-zero-tax or feeless cashouts), the counter is capped at the remaining balance -- non-fee-free funds leave first. During zero-tax cashout, the 2.5% fee applies only up to this surplus amount (then depletes it), preventing a round-trip fee bypass. Cleared on terminal migration (the migration fee settles this liability).
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
- -> JBPayoutSplitGroupLib.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** -- `JBPayoutSplitGroupLib._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
- 15. **Preview functions call data hooks** -- `previewPayFor`, `previewCashOutFrom`, and their store-level counterparts invoke data hooks during simulation. A reverting data hook will cause the preview to revert. Preview functions are `view` but still make external calls to hooks.
266
- 16. **Store preview functions take an explicit terminal parameter** -- `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` take an explicit `terminal` address for balance/surplus lookups. Callers must pass a registered terminal to get correct results. The terminal-level `JBMultiTerminal.previewPayFor` / `previewCashOutFrom` handle this automatically by passing `address(this)`.
267
-
268
- ## Priority Areas to Audit
269
-
270
- Ordered by blast radius:
271
-
272
- | Priority | Target | Why |
273
- |----------|--------|-----|
274
- | 1 | **JBMultiTerminal + JBTerminalStore** | All funds flow through here. No reentrancy guard. CEI ordering is the only defense. |
275
- | 2 | **JBCashOuts bonding curve math** | Determines how much holders can extract. Edge cases with 0 supply, 0 count, MAX tax rate. `mulDiv` rounding direction. |
276
- | 3 | **Data hook integration** | Data hooks have absolute control. Verify all constraints on hook return values are enforced. |
277
- | 4 | **JBRulesets weight decay + transition logic** | Complex linked-list traversal with approval hook fallback. Weight cache threshold. Timing at ruleset boundaries. |
278
- | 5 | **Fee arithmetic (JBFees)** | Forward/backward consistency. Held fee lifecycle. Fee return calculations in `_returnHeldFees`. |
279
- | 6 | **Cross-terminal surplus** (`JBSurplus`) | Aggregation across terminals with price conversion. Verify surplus cannot be inflated across terminals. |
280
- | 7 | **Permission system** | ROOT escalation, wildcard project scope, ERC-2771 spoofing. |
281
- | 8 | **Permit2 metadata parsing** | Malformed metadata, amount mismatch between permit and payment. |
282
-
283
- ## How to Run Tests
284
-
285
- ```bash
286
- cd nana-core-v6
287
- npm install
288
- forge build
289
- forge test
290
-
291
- # Run with high verbosity for debugging
292
- forge test -vvvv --match-test testExploitName
293
-
294
- # Write a PoC
295
- forge test --match-path test/audit/ExploitPoC.t.sol -vvv
296
-
297
- # Run invariant tests
298
- forge test --match-contract Invariant
299
-
300
- # Gas analysis
301
- forge test --gas-report
302
- ```
303
-
304
- The existing test suite has 185 test files including:
305
- - **Integration tests**: Full flow tests for pay, cash out, payouts
306
- - **Formal property tests**: 7 bonding curve properties + 6 fee properties
307
- - **Invariant tests**: TerminalStore (5), Phase3Deep (8), Rulesets (4), Tokens (4)
308
- - **Economic simulation**: 3 projects, 10 actors, 15 operations, 6 invariants
309
- - **Flash loan attack tests**: 12 attack vectors in `FlashLoanAttacks.t.sol`
310
-
311
- Review the invariant tests to understand what is already proven -- then try to break those invariants with configurations the tests do not cover.
312
-
313
- ## Invariants to Verify
314
-
315
- These MUST hold. If you can break any of them, it is a finding:
316
-
317
- 1. **Balance conservation**: `terminal.balance(token) >= sum(store.balanceOf(projectId, terminal, token))` for all projects
318
- 2. **Inflow >= Outflow**: Total funds received by a project >= total funds distributed
319
- 3. **Fee monotonicity**: Project #1's balance only increases over time
320
- 4. **Token supply consistency**: `JBTokens.totalSupplyOf(projectId) == creditSupply + erc20.totalSupply()`
321
- 5. **Ruleset existence**: After `launchProjectFor()`, `currentOf(projectId)` always returns a valid ruleset
322
- 6. **No flash-loan profit**: Pay + cash out in same block should never yield more than was paid (minus fees)
323
- 7. **Payout limits**: A project cannot extract more than its configured payout limit per ruleset cycle
324
- 8. **Surplus allowance**: A project cannot withdraw more than its configured surplus allowance per ruleset
325
-
326
- ## How to Report Findings
327
-
328
- For each finding:
329
-
330
- 1. **Title** -- one line, starts with severity (CRITICAL/HIGH/MEDIUM/LOW)
331
- 2. **Affected contract(s)** -- exact file path and line numbers
332
- 3. **Description** -- what is wrong, in plain language
333
- 4. **Trigger sequence** -- step-by-step, minimal steps to reproduce
334
- 5. **Impact** -- what an attacker gains, what a user loses (with numbers if possible)
335
- 6. **Proof** -- code trace showing the exact execution path, or a Foundry test
336
- 7. **Fix** -- minimal code change that resolves the issue
337
-
338
- **Severity guide:**
339
- - **CRITICAL**: Direct fund loss, permanent DoS, or system insolvency. Exploitable with no preconditions.
340
- - **HIGH**: Conditional fund loss, privilege escalation, or broken core invariant. Requires specific but realistic setup.
341
- - **MEDIUM**: Value leakage, griefing with cost to attacker, incorrect accounting, degraded functionality.
342
- - **LOW**: Informational, cosmetic inconsistency, edge-case-only with no material impact.
343
-
344
- ## Attack Vectors
345
-
346
- These are the attack patterns most likely to yield findings in core. Ordered by estimated likelihood of undiscovered bugs.
347
-
348
- ### 1. Hook Composition Attacks
349
-
350
- Hooks execute after state is partially committed. Data hooks control weight and fund allocation absolutely. Pay/cashout hooks receive diverted funds and can re-enter the protocol.
351
-
352
- **Specific sequences to test:**
353
- - Data hook returns `totalSupply = surplus` during `beforeCashOutRecordedWith` → `reclaimAmount = cashOutCount`, completely bypassing the bonding curve. Verify all constraints on hook return values.
354
- - Data hook sets `weight = type(uint256).max` during pay → can this overflow in `mulDiv(amount.value, weight, weightRatio)`?
355
- - Pay hook calls `cashOutTokensOf` on the same project during `afterPayRecordedWith`. Tokens are minted and store is updated. Is the cashout profitable given the inflated balance?
356
- - Split hook calls `pay()` on the same project during `sendPayoutsOf`. Payout limit is consumed but new payment adds to balance and mints tokens. Does this create a value loop?
357
- - Cash out hook calls `pay()` on same project. Tokens are burned before hooks execute, but new tokens are minted. Can this inflate supply?
358
-
359
- ### 2. Bonding Curve Economic Attacks
1
+ # Audit Instructions
2
+
3
+ This is the core Juicebox V6 protocol. Most ecosystem invariants reduce to this repo eventually.
4
+
5
+ ## Objective
6
+
7
+ Find issues that:
8
+ - break terminal solvency or internal accounting
9
+ - let projects extract more than payout or surplus-allowance limits
10
+ - miscompute payment minting, reserved tokens, or cash-out reclaim amounts
11
+ - corrupt ruleset transitions, approvals, or decay behavior
12
+ - bypass the permission model, migrations, or fee lifecycle
13
+
14
+ ## Scope
15
+
16
+ In scope:
17
+ - all Solidity under `src/`
18
+ - deployment scripts in `script/`
19
+ - price-feed setup and periphery contracts under `src/periphery/`
20
+
21
+ Especially critical contracts:
22
+ - `JBMultiTerminal`
23
+ - `JBTerminalStore`
24
+ - `JBController`
25
+ - `JBRulesets`
26
+ - `JBTokens`
27
+ - `JBPermissions`
28
+ - `JBPrices`
29
+ - `JBSplits`
30
+ - `JBFundAccessLimits`
31
+
32
+ ## Start Here
33
+
34
+ For the fastest serious review, read in this order:
35
+ - `JBTerminalStore`
36
+ - `JBMultiTerminal`
37
+ - `JBController`
38
+ - `JBRulesets`
39
+ - `JBPermissions`
40
+ - `JBPrices`
41
+
42
+ That order mirrors how most high-severity issues emerge:
43
+ - accounting is computed
44
+ - funds are moved
45
+ - tokens are minted or burned
46
+ - permissions and price context decide whether the move was legitimate
47
+
48
+ ## System Model
49
+
50
+ Core roles:
51
+ - `JBMultiTerminal`: holds funds and executes pay, payout, cash-out, allowance, and fee-processing flows
52
+ - `JBTerminalStore`: accounting and surplus logic
53
+ - `JBController`: project lifecycle, token mint/burn, and permissions-sensitive operations
54
+ - `JBRulesets`: current and queued economic parameters
55
+ - `JBTokens`: ERC-20 and credit accounting
56
+ - `JBPermissions`: access-control backbone
57
+
58
+ The rest of the ecosystem plugs into these extension points:
59
+ - data hooks
60
+ - pay hooks
61
+ - cash-out hooks
62
+ - split hooks
63
+ - approval hooks
64
+
65
+ Core ordering to keep in mind:
66
+ - store records accounting before terminal fulfillment finishes
67
+ - controller mint and burn operations happen around terminal flows, not as a separate settlement layer
68
+ - hooks can turn what looks like a simple pay or cash-out into a multi-contract composition
69
+
70
+ ## Critical Invariants
71
+
72
+ 1. Terminal solvency
73
+ Internal balances and held-fee obligations must reconcile with actual terminal token balances.
74
+
75
+ 2. No over-withdrawal
76
+ Payouts and allowance usage must never exceed configured per-cycle limits.
77
+
78
+ 3. Cash-out correctness
79
+ Surplus, total supply, tax rate, fee treatment, and hook overrides must combine into the intended reclaim amount.
80
+
81
+ 4. Ruleset integrity
82
+ The active ruleset and any fallback or cycling behavior must reflect exact timing and approval-hook semantics.
83
+
84
+ 5. Token accounting consistency
85
+ Credits, ERC-20 total supply, reserved token balance, and burn/mint paths must remain internally coherent.
86
+
87
+ 6. Privilege containment
88
+ Permissions, wildcard grants, controller migration, and terminal routing must not allow unauthorized project control or fund movement.
89
+
90
+ 7. Held-fee correctness
91
+ When fee payment is deferred, later replenishment or migration behavior must not accidentally forgive, duplicate, or cross-charge the obligation.
92
+
93
+ 8. Preview coherence
94
+ `previewPayFor` and `previewCashOutFrom` should not become meaningfully inconsistent with execution in ways downstream repos can exploit.
95
+
96
+ ## Threat Model
97
+
98
+ Prioritize:
99
+ - hook-driven reentrancy or state-ordering issues
100
+ - price-feed failure and cross-currency conversions
101
+ - fee-processing failure paths
102
+ - migration and feeless-address edge cases
103
+ - ruleset-boundary timing attacks
104
+ - wildcard or root permission escalation
105
+
106
+ The highest-yield attacker mindsets here are:
107
+ - a malicious hook that receives control after balances move but before the full user flow is conceptually finished
108
+ - a project owner exploiting migration, limits, or feeless settings rather than breaking access control directly
109
+ - a cross-currency user extracting value from rounding or stale price conversions
110
+
111
+ ## Hotspots
112
+
113
+ - `pay`, `cashOutTokensOf`, `sendPayoutsOf`, and `useAllowanceOf`
114
+ - `preview*` paths when downstream repos treat them as execution truth
115
+ - held-fee lifecycle and `_processFee`
116
+ - surplus aggregation across terminals
117
+ - controller migration and terminal migration
118
+ - `setPermissionsFor` and any wildcard semantics
119
+
120
+ ## Sequences Worth Replaying
121
+
122
+ 1. `pay` with a data hook that returns altered weight and hook specs, then re-enter through a pay hook.
123
+ 2. `cashOutTokensOf` when `useTotalSurplusForCashOuts` or cross-terminal surplus logic matters.
124
+ 3. `sendPayoutsOf` into splits that route to another project, hook, or failing beneficiary.
125
+ 4. held-fee accumulation -> migration or balance depletion -> `processHeldFeesOf`.
126
+ 5. permission grants involving operators, wildcard project IDs, or later controller changes.
127
+
128
+ ## Finding Bar
129
+
130
+ The best findings in this repo usually prove one of these:
131
+ - the store records more value than the terminal can safely honor
132
+ - a limit is consumed too late, after externally controlled code can already profit
133
+ - a migration or held-fee edge path changes who ultimately bears an obligation
134
+ - permissions that look project-scoped become ecosystem-relevant through wildcard or routing semantics
135
+
136
+ ## Build And Verification
137
+
138
+ Standard workflow:
139
+ - `npm install`
140
+ - `forge build`
141
+ - `forge test`
142
+
143
+ The current test suite already targets:
144
+ - flash-loan and economic exploits
145
+ - permissions invariants
146
+ - ruleset transitions
147
+ - fee and migration edge cases
148
+ - multi-token and cross-currency surplus behavior
360
149
 
361
- The cash out formula: `reclaimAmount = (surplus * count / supply) * [(MAX - tax) + tax * (count / supply)] / MAX`
362
-
363
- **Sequences:**
364
- - Pay → immediate cash out in same block: should never profit after fees. Test with different hook configurations.
365
- - Pay with data hook that inflates weight → cash out before reserved tokens are distributed. Pending reserved tokens inflate `totalSupply` (H-4 finding).
366
- - Cash out with `cashOutCount >= totalSupply` returns entire surplus (C-5 finding, by design). Can you engineer this condition without being the last holder? (e.g., front-running a burn)
367
- - Cross-terminal surplus aggregation: `useTotalSurplusForCashOuts` aggregates surplus across all terminals via `JBSurplus`. Can you manipulate surplus in one terminal to inflate cashout value in another?
368
-
369
- ### 3. Reentrancy Through CEI Ordering
370
-
371
- No contract uses `ReentrancyGuard`. The protocol relies on checks-effects-interactions ordering.
372
-
373
- **Critical surfaces (in order of risk):**
374
- 1. **Pay hook → cash out**: After `recordPaymentFrom` + `mintTokensOf`, the hook executes. It could call `cashOutTokensOf`. Store balance and tokens are updated. Is the resulting cashout computed against post-payment state correctly?
375
- 2. **Split hook → pay**: During `sendPayoutsOf`, splits receive funds. A hook could call `pay()`. Payout limit is consumed, but new payment updates state.
376
- 3. **Fee processing → re-entry**: `_processFee` calls `terminal.pay()` on project #1's terminal. If project #1 has a pay hook that calls back into the originating terminal, the fee is already deducted.
377
- 4. **processHeldFeesOf**: Re-reads storage index each iteration, updates index BEFORE external call. Verify this ordering prevents re-entrancy exploitation.
378
-
379
- ### 4. Ruleset Transition Timing
380
-
381
- Rulesets transition at exact block timestamps. Transaction ordering at boundaries matters.
382
-
383
- **What to test:**
384
- - Payment at last second of ruleset vs. first second of next: both should execute with correct weights.
385
- - Approval hook rejection at boundary: fallback to `basedOnId` chain simulates cycling from last approved ruleset. Is this always equivalent?
386
- - `duration = 0` rulesets immediately replaced when new one queued. Can you pay and queue in the same tx to get old weight but new parameters?
387
- - Weight decay across 20,000+ cycles without cache: `WeightCacheRequired` revert = DoS. Can an attacker force a project into this state?
388
-
389
- ## Anti-Patterns to Hunt
390
-
391
- | Pattern | Where to Look | Why It's Dangerous |
392
- |---------|--------------|-------------------|
393
- | `try-catch` swallowing errors | JBMultiTerminal (hooks, fees, splits) | Failed external calls silently change control flow. Fee try-catch enables temporary fee avoidance. |
394
- | `mulDiv` rounding direction | JBCashOuts, JBFees, JBTerminalStore | Rounding in attacker's favor compounds over many transactions. Verify rounding favors the protocol. |
395
- | Currency type confusion | JBTerminalStore, JBFundAccessLimits | Abstract (1=ETH, 2=USD) vs concrete (`uint32(address)`) currencies. `groupId` (`uint256`) vs `currency` (`uint32`) truncation. |
396
- | Named returns without explicit `return` | JBTerminalStore, JBRulesets | Auto-return of named variables. Easy to miss intermediate assignments that change the return value. |
397
- | Bit-packed metadata shifts | JBRulesetMetadataResolver | Off-by-one in shift amounts or mask widths corrupts adjacent fields. 256-bit layout with 17 fields. |
398
- | State before validation | `recordPayoutFor` | Balance deducted and limit incremented BEFORE validation. Safe due to atomic revert, but matters for reentrancy analysis. |
399
- | Lazy evaluation of pending state | `pendingReservedTokenBalanceOf` | Reserved tokens accumulate but aren't minted until `sendReservedTokensToSplitsOf`. `totalSupply` includes pending. |
400
- | External call in loop | Payout splits, `processHeldFeesOf` | Gas griefing via reverting external calls. Each caught by try-catch but still costs gas. |
401
-
402
- ## Previous Audit Findings
403
-
404
- | ID | Severity | Status | Description |
405
- |----|----------|--------|-------------|
406
- | C-5 | Critical | Known/Accepted | `cashOut(0)` with `totalSupply == 0` returns entire surplus. By design — last holder gets everything. Documented in `FlashLoanAttacks.t.sol`. |
407
- | H-4 | High | Known/Accepted | Pending reserved tokens inflate `totalSupply` in bonding curve calculation, reducing cashout value by 50%+ in extreme cases. Distributing reserved tokens via `sendReservedTokensToSplitsOf` before cashing out mitigates. |
408
- | FV-1 | Low | Known/Accepted | Bonding curve subadditivity violation from `mulDiv` rounding. Measured at <0.01%, economically insignificant. Proven bounded in formal property tests. |
409
-
410
- ## Coverage Gaps
411
-
412
- The 185 test files cover most flows, but these areas have limited or no coverage:
413
-
414
- - **Multi-hook composition**: No end-to-end tests for data hook + pay hook + cashout hook interacting in a single flow with reentrancy.
415
- - **Extreme weight decay**: Weight decay beyond 20,000 cycles tested for revert, but not for precision loss at exactly the cache threshold boundary.
416
- - **Cross-terminal surplus manipulation**: No tests where surplus is manipulated in terminal A to inflate cashout value in terminal B via `useTotalSurplusForCashOuts`.
417
- - **Approval hook edge cases**: Limited testing of approval hook state changes between `queueRulesetsOf` and ruleset start time.
418
- - **Concurrent held fee processing**: `processHeldFeesOf` tested sequentially but not under reentrancy from fee payment hooks.
419
- - **ERC-2771 meta-tx spoofing**: No tests verifying that a malicious forwarder cannot spoof `_msgSender()` to bypass permissions.
420
-
421
- ## Compiler and Version Info
422
-
423
- - **Solidity**: 0.8.28
424
- - **EVM target**: Cancun (uses transient storage opcodes)
425
- - **Optimizer**: 200 runs (no via-IR)
426
- - **Dependencies**: OpenZeppelin 5.x, Solady, forge-std
427
- - **Build**: `forge build` (Foundry)
428
-
429
- Go break it.
150
+ The highest-value findings in this repo are the ones that make downstream hooks or deployers unsafe even when those repos are otherwise correct.