@bananapus/core-v6 0.0.16 → 0.0.18
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 +1 -1
- package/ARCHITECTURE.md +2 -1
- package/AUDIT_INSTRUCTIONS.md +342 -0
- package/CHANGE_LOG.md +400 -0
- package/README.md +4 -4
- package/RISKS.md +171 -50
- package/SKILLS.md +9 -6
- package/USER_JOURNEYS.md +622 -0
- package/package.json +2 -2
- package/script/DeployPeriphery.s.sol +7 -1
- package/src/JBController.sol +5 -0
- package/src/JBDeadline.sol +3 -0
- package/src/JBDirectory.sol +2 -1
- package/src/JBMultiTerminal.sol +50 -9
- package/src/JBPermissions.sol +2 -0
- package/src/JBPrices.sol +8 -2
- package/src/JBRulesets.sol +3 -0
- package/src/JBSplits.sol +9 -5
- package/src/JBTerminalStore.sol +54 -47
- package/src/JBTokens.sol +3 -0
- package/src/interfaces/IJBTerminalStore.sol +3 -0
- package/src/libraries/JBFees.sol +2 -0
- package/src/libraries/JBMetadataResolver.sol +17 -4
- package/src/structs/JBBeforeCashOutRecordedContext.sol +4 -0
- package/test/TestAuditResponseDesignProofs.sol +434 -0
- package/test/TestDataHookFuzzing.sol +520 -0
- package/test/TestFeeFreeCashOutBypass.sol +617 -0
- package/test/TestL2SequencerPriceFeed.sol +292 -0
- package/test/TestMetadataOffsetOverflow.sol +179 -0
- package/test/TestMultiTerminalSurplus.sol +348 -0
- package/test/TestPermit2DataHook.t.sol +360 -0
- package/test/TestRulesetQueueing.sol +1 -2
- package/test/TestRulesetWeightCaching.sol +122 -124
- package/test/WeirdTokenTests.t.sol +37 -0
- package/test/regression/HoldFeesCashOutReserved.t.sol +415 -0
- package/test/regression/WeightCacheBoundary.t.sol +291 -0
- package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +2 -2
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +18 -17
- package/test/units/static/JBMultiTerminal/TestPay.sol +6 -4
- package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +206 -18
- package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +280 -0
- package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +55 -12
- package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +72 -0
- package/docs/book.css +0 -13
- package/docs/book.toml +0 -12
- package/docs/solidity.min.js +0 -74
- package/docs/src/README.md +0 -703
- package/docs/src/SUMMARY.md +0 -94
- package/docs/src/src/JBChainlinkV3PriceFeed.sol/contract.JBChainlinkV3PriceFeed.md +0 -83
- package/docs/src/src/JBChainlinkV3SequencerPriceFeed.sol/contract.JBChainlinkV3SequencerPriceFeed.md +0 -88
- package/docs/src/src/JBController.sol/contract.JBController.md +0 -1121
- package/docs/src/src/JBDeadline.sol/contract.JBDeadline.md +0 -84
- package/docs/src/src/JBDirectory.sol/contract.JBDirectory.md +0 -294
- package/docs/src/src/JBERC20.sol/contract.JBERC20.md +0 -190
- package/docs/src/src/JBFeelessAddresses.sol/contract.JBFeelessAddresses.md +0 -80
- package/docs/src/src/JBFundAccessLimits.sol/contract.JBFundAccessLimits.md +0 -253
- package/docs/src/src/JBMultiTerminal.sol/contract.JBMultiTerminal.md +0 -1472
- package/docs/src/src/JBPermissions.sol/contract.JBPermissions.md +0 -199
- package/docs/src/src/JBPrices.sol/contract.JBPrices.md +0 -154
- package/docs/src/src/JBProjects.sol/contract.JBProjects.md +0 -131
- package/docs/src/src/JBRulesets.sol/contract.JBRulesets.md +0 -677
- package/docs/src/src/JBSplits.sol/contract.JBSplits.md +0 -237
- package/docs/src/src/JBTerminalStore.sol/contract.JBTerminalStore.md +0 -591
- package/docs/src/src/JBTokens.sol/contract.JBTokens.md +0 -353
- package/docs/src/src/README.md +0 -25
- package/docs/src/src/abstract/JBControlled.sol/abstract.JBControlled.md +0 -64
- package/docs/src/src/abstract/JBPermissioned.sol/abstract.JBPermissioned.md +0 -84
- package/docs/src/src/abstract/README.md +0 -5
- package/docs/src/src/enums/JBApprovalStatus.sol/enum.JBApprovalStatus.md +0 -17
- package/docs/src/src/enums/README.md +0 -4
- package/docs/src/src/interfaces/IJBCashOutHook.sol/interface.IJBCashOutHook.md +0 -29
- package/docs/src/src/interfaces/IJBCashOutTerminal.sol/interface.IJBCashOutTerminal.md +0 -57
- package/docs/src/src/interfaces/IJBControlled.sol/interface.IJBControlled.md +0 -12
- package/docs/src/src/interfaces/IJBController.sol/interface.IJBController.md +0 -334
- package/docs/src/src/interfaces/IJBDirectory.sol/interface.IJBDirectory.md +0 -108
- package/docs/src/src/interfaces/IJBDirectoryAccessControl.sol/interface.IJBDirectoryAccessControl.md +0 -19
- package/docs/src/src/interfaces/IJBFeeTerminal.sol/interface.IJBFeeTerminal.md +0 -91
- package/docs/src/src/interfaces/IJBFeelessAddresses.sol/interface.IJBFeelessAddresses.md +0 -26
- package/docs/src/src/interfaces/IJBFundAccessLimits.sol/interface.IJBFundAccessLimits.md +0 -88
- package/docs/src/src/interfaces/IJBMigratable.sol/interface.IJBMigratable.md +0 -29
- package/docs/src/src/interfaces/IJBMultiTerminal.sol/interface.IJBMultiTerminal.md +0 -50
- package/docs/src/src/interfaces/IJBPayHook.sol/interface.IJBPayHook.md +0 -28
- package/docs/src/src/interfaces/IJBPayoutTerminal.sol/interface.IJBPayoutTerminal.md +0 -105
- package/docs/src/src/interfaces/IJBPermissioned.sol/interface.IJBPermissioned.md +0 -12
- package/docs/src/src/interfaces/IJBPermissions.sol/interface.IJBPermissions.md +0 -74
- package/docs/src/src/interfaces/IJBPermitTerminal.sol/interface.IJBPermitTerminal.md +0 -15
- package/docs/src/src/interfaces/IJBPriceFeed.sol/interface.IJBPriceFeed.md +0 -12
- package/docs/src/src/interfaces/IJBPrices.sol/interface.IJBPrices.md +0 -74
- package/docs/src/src/interfaces/IJBProjectUriRegistry.sol/interface.IJBProjectUriRegistry.md +0 -19
- package/docs/src/src/interfaces/IJBProjects.sol/interface.IJBProjects.md +0 -49
- package/docs/src/src/interfaces/IJBRulesetApprovalHook.sol/interface.IJBRulesetApprovalHook.md +0 -35
- package/docs/src/src/interfaces/IJBRulesetDataHook.sol/interface.IJBRulesetDataHook.md +0 -97
- package/docs/src/src/interfaces/IJBRulesets.sol/interface.IJBRulesets.md +0 -165
- package/docs/src/src/interfaces/IJBSplitHook.sol/interface.IJBSplitHook.md +0 -31
- package/docs/src/src/interfaces/IJBSplits.sol/interface.IJBSplits.md +0 -35
- package/docs/src/src/interfaces/IJBTerminal.sol/interface.IJBTerminal.md +0 -141
- package/docs/src/src/interfaces/IJBTerminalStore.sol/interface.IJBTerminalStore.md +0 -198
- package/docs/src/src/interfaces/IJBToken.sol/interface.IJBToken.md +0 -54
- package/docs/src/src/interfaces/IJBTokenUriResolver.sol/interface.IJBTokenUriResolver.md +0 -12
- package/docs/src/src/interfaces/IJBTokens.sol/interface.IJBTokens.md +0 -151
- package/docs/src/src/interfaces/README.md +0 -33
- package/docs/src/src/libraries/JBCashOuts.sol/library.JBCashOuts.md +0 -40
- package/docs/src/src/libraries/JBConstants.sol/library.JBConstants.md +0 -52
- package/docs/src/src/libraries/JBCurrencyIds.sol/library.JBCurrencyIds.md +0 -19
- package/docs/src/src/libraries/JBFees.sol/library.JBFees.md +0 -52
- package/docs/src/src/libraries/JBFixedPointNumber.sol/library.JBFixedPointNumber.md +0 -12
- package/docs/src/src/libraries/JBMetadataResolver.sol/library.JBMetadataResolver.md +0 -242
- package/docs/src/src/libraries/JBRulesetMetadataResolver.sol/library.JBRulesetMetadataResolver.md +0 -180
- package/docs/src/src/libraries/JBSplitGroupIds.sol/library.JBSplitGroupIds.md +0 -14
- package/docs/src/src/libraries/JBSurplus.sol/library.JBSurplus.md +0 -44
- package/docs/src/src/libraries/README.md +0 -12
- package/docs/src/src/periphery/JBDeadline1Day.sol/contract.JBDeadline1Day.md +0 -15
- package/docs/src/src/periphery/JBDeadline3Days.sol/contract.JBDeadline3Days.md +0 -15
- package/docs/src/src/periphery/JBDeadline3Hours.sol/contract.JBDeadline3Hours.md +0 -15
- package/docs/src/src/periphery/JBDeadline7Days.sol/contract.JBDeadline7Days.md +0 -15
- package/docs/src/src/periphery/JBMatchingPriceFeed.sol/contract.JBMatchingPriceFeed.md +0 -22
- package/docs/src/src/periphery/README.md +0 -8
- package/docs/src/src/structs/JBAccountingContext.sol/struct.JBAccountingContext.md +0 -20
- package/docs/src/src/structs/JBAfterCashOutRecordedContext.sol/struct.JBAfterCashOutRecordedContext.md +0 -43
- package/docs/src/src/structs/JBAfterPayRecordedContext.sol/struct.JBAfterPayRecordedContext.md +0 -42
- package/docs/src/src/structs/JBBeforeCashOutRecordedContext.sol/struct.JBBeforeCashOutRecordedContext.md +0 -45
- package/docs/src/src/structs/JBBeforePayRecordedContext.sol/struct.JBBeforePayRecordedContext.md +0 -41
- package/docs/src/src/structs/JBCashOutHookSpecification.sol/struct.JBCashOutHookSpecification.md +0 -22
- package/docs/src/src/structs/JBCurrencyAmount.sol/struct.JBCurrencyAmount.md +0 -17
- package/docs/src/src/structs/JBFee.sol/struct.JBFee.md +0 -20
- package/docs/src/src/structs/JBFundAccessLimitGroup.sol/struct.JBFundAccessLimitGroup.md +0 -39
- package/docs/src/src/structs/JBPayHookSpecification.sol/struct.JBPayHookSpecification.md +0 -22
- package/docs/src/src/structs/JBPermissionsData.sol/struct.JBPermissionsData.md +0 -21
- package/docs/src/src/structs/JBRuleset.sol/struct.JBRuleset.md +0 -55
- package/docs/src/src/structs/JBRulesetConfig.sol/struct.JBRulesetConfig.md +0 -51
- package/docs/src/src/structs/JBRulesetMetadata.sol/struct.JBRulesetMetadata.md +0 -79
- package/docs/src/src/structs/JBRulesetWeightCache.sol/struct.JBRulesetWeightCache.md +0 -16
- package/docs/src/src/structs/JBRulesetWithMetadata.sol/struct.JBRulesetWithMetadata.md +0 -16
- package/docs/src/src/structs/JBSingleAllowance.sol/struct.JBSingleAllowance.md +0 -26
- package/docs/src/src/structs/JBSplit.sol/struct.JBSplit.md +0 -49
- package/docs/src/src/structs/JBSplitGroup.sol/struct.JBSplitGroup.md +0 -17
- package/docs/src/src/structs/JBSplitHookContext.sol/struct.JBSplitHookContext.md +0 -29
- package/docs/src/src/structs/JBTerminalConfig.sol/struct.JBTerminalConfig.md +0 -16
- package/docs/src/src/structs/JBTokenAmount.sol/struct.JBTokenAmount.md +0 -23
- package/docs/src/src/structs/README.md +0 -25
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
|
|
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.
|