@bananapus/core-v6 0.0.29 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +43 -13
- package/ARCHITECTURE.md +62 -137
- package/AUDIT_INSTRUCTIONS.md +149 -428
- package/CHANGELOG.md +73 -0
- package/README.md +90 -201
- package/RISKS.md +27 -12
- package/SKILLS.md +31 -441
- package/STYLE_GUIDE.md +52 -19
- package/USER_JOURNEYS.md +76 -627
- package/package.json +2 -3
- package/references/entrypoints.md +160 -0
- package/references/types-errors-events.md +297 -0
- package/script/Deploy.s.sol +7 -2
- package/script/DeployPeriphery.s.sol +51 -4
- package/src/JBController.sol +11 -3
- package/src/JBDirectory.sol +1 -0
- package/src/JBMultiTerminal.sol +126 -72
- package/src/JBRulesets.sol +2 -1
- package/src/JBTerminalStore.sol +22 -11
- package/src/abstract/JBControlled.sol +7 -1
- package/src/abstract/JBPermissioned.sol +1 -1
- package/src/interfaces/IJBRulesetDataHook.sol +5 -4
- package/src/libraries/JBCashOuts.sol +1 -1
- package/src/libraries/JBConstants.sol +1 -1
- package/src/libraries/JBCurrencyIds.sol +1 -1
- package/src/libraries/JBFees.sol +1 -1
- package/src/libraries/JBFixedPointNumber.sol +1 -1
- package/src/libraries/JBMetadataResolver.sol +1 -1
- package/src/libraries/JBPayoutSplitGroupLib.sol +3 -1
- package/src/libraries/JBRulesetMetadataResolver.sol +1 -1
- package/src/libraries/JBSplitGroupIds.sol +1 -1
- package/src/libraries/JBSurplus.sol +1 -1
- package/src/structs/JBSplit.sol +4 -1
- package/test/TestForwardedTokenConsumption.sol +418 -0
- package/test/audit/CycledSurplusAllowanceReset.t.sol +184 -0
- package/test/units/static/JBController/TestPreviewMintOf.sol +5 -3
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +15 -11
- package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +6 -0
- package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +3 -0
- package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +3 -0
- package/test/units/static/JBMultiTerminal/TestPay.sol +7 -15
- package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +1 -1
- package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +1 -1
- package/CHANGE_LOG.md +0 -479
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -1,429 +1,150 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
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.
|