@bananapus/core-v6 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/ADMINISTRATION.md +5 -1
  2. package/ARCHITECTURE.md +2 -1
  3. package/AUDIT_INSTRUCTIONS.md +342 -0
  4. package/CHANGE_LOG.md +375 -0
  5. package/README.md +6 -6
  6. package/RISKS.md +171 -50
  7. package/SKILLS.md +11 -6
  8. package/STYLE_GUIDE.md +16 -2
  9. package/USER_JOURNEYS.md +622 -0
  10. package/package.json +2 -2
  11. package/script/Deploy.s.sol +22 -13
  12. package/script/DeployPeriphery.s.sol +76 -52
  13. package/script/helpers/CoreDeploymentLib.sol +83 -35
  14. package/src/JBChainlinkV3PriceFeed.sol +1 -0
  15. package/src/JBController.sol +23 -3
  16. package/src/JBDeadline.sol +3 -0
  17. package/src/JBDirectory.sol +2 -1
  18. package/src/JBERC20.sol +12 -3
  19. package/src/JBFundAccessLimits.sol +12 -2
  20. package/src/JBMultiTerminal.sol +53 -10
  21. package/src/JBPermissions.sol +3 -0
  22. package/src/JBPrices.sol +8 -2
  23. package/src/JBProjects.sol +1 -1
  24. package/src/JBRulesets.sol +14 -0
  25. package/src/JBSplits.sol +14 -5
  26. package/src/JBTerminalStore.sol +57 -47
  27. package/src/JBTokens.sol +43 -4
  28. package/src/interfaces/IJBController.sol +6 -0
  29. package/src/interfaces/IJBPermitTerminal.sol +1 -0
  30. package/src/interfaces/IJBTerminalStore.sol +3 -0
  31. package/src/interfaces/IJBToken.sol +5 -0
  32. package/src/interfaces/IJBTokens.sol +13 -0
  33. package/src/libraries/JBFees.sol +2 -0
  34. package/src/libraries/JBMetadataResolver.sol +24 -7
  35. package/src/libraries/JBRulesetMetadataResolver.sol +21 -21
  36. package/src/structs/JBAccountingContext.sol +1 -0
  37. package/src/structs/JBAfterCashOutRecordedContext.sol +1 -0
  38. package/src/structs/JBAfterPayRecordedContext.sol +1 -0
  39. package/src/structs/JBBeforeCashOutRecordedContext.sol +5 -0
  40. package/src/structs/JBBeforePayRecordedContext.sol +1 -0
  41. package/src/structs/JBCashOutHookSpecification.sol +1 -0
  42. package/src/structs/JBCurrencyAmount.sol +1 -0
  43. package/src/structs/JBFee.sol +1 -0
  44. package/src/structs/JBFundAccessLimitGroup.sol +1 -0
  45. package/src/structs/JBPayHookSpecification.sol +1 -0
  46. package/src/structs/JBPermissionsData.sol +1 -0
  47. package/src/structs/JBRuleset.sol +1 -0
  48. package/src/structs/JBRulesetConfig.sol +1 -0
  49. package/src/structs/JBRulesetMetadata.sol +1 -0
  50. package/src/structs/JBRulesetWeightCache.sol +1 -0
  51. package/src/structs/JBRulesetWithMetadata.sol +1 -0
  52. package/src/structs/JBSingleAllowance.sol +1 -0
  53. package/src/structs/JBSplit.sol +1 -0
  54. package/src/structs/JBSplitGroup.sol +1 -0
  55. package/src/structs/JBSplitHookContext.sol +1 -0
  56. package/src/structs/JBTerminalConfig.sol +1 -0
  57. package/src/structs/JBTokenAmount.sol +1 -0
  58. package/test/ComprehensiveInvariant.t.sol +15 -2
  59. package/test/CoreExploitTests.t.sol +34 -1
  60. package/test/EconomicSimulation.t.sol +10 -2
  61. package/test/EntryPointPermutations.t.sol +17 -3
  62. package/test/FlashLoanAttacks.t.sol +12 -1
  63. package/test/PermissionEscalation.t.sol +53 -10
  64. package/test/RulesetTransitions.t.sol +15 -1
  65. package/test/SplitLoopTests.t.sol +25 -2
  66. package/test/TestAccessToFunds.sol +17 -2
  67. package/test/TestAuditResponseDesignProofs.sol +434 -0
  68. package/test/TestCashOut.sol +15 -1
  69. package/test/TestCashOutCountFor.sol +1 -1
  70. package/test/TestCashOutHooks.sol +47 -25
  71. package/test/TestCashOutTimingEdge.sol +13 -1
  72. package/test/TestDataHookFuzzing.sol +520 -0
  73. package/test/TestDurationUnderflow.sol +13 -1
  74. package/test/TestFeeFreeCashOutBypass.sol +617 -0
  75. package/test/TestFeeProcessingFailure.sol +16 -1
  76. package/test/TestFees.sol +14 -1
  77. package/test/TestInterfaceSupport.sol +20 -1
  78. package/test/TestJBERC20Inheritance.sol +11 -1
  79. package/test/TestL2SequencerPriceFeed.sol +292 -0
  80. package/test/TestLaunchProject.sol +13 -1
  81. package/test/TestMetaTx.sol +15 -1
  82. package/test/TestMetadataOffsetOverflow.sol +179 -0
  83. package/test/TestMetadataParserLib.sol +37 -4
  84. package/test/TestMigrationHeldFees.sol +16 -1
  85. package/test/TestMintTokensOf.sol +14 -1
  86. package/test/TestMultiTerminalSurplus.sol +348 -0
  87. package/test/TestMultiTokenSurplus.sol +14 -1
  88. package/test/TestMultipleAccessLimits.sol +23 -1
  89. package/test/TestPayBurnRedeemFlow.sol +16 -1
  90. package/test/TestPayHooks.sol +33 -14
  91. package/test/TestPermissions.sol +20 -1
  92. package/test/TestPermissionsEdge.sol +5 -1
  93. package/test/TestPermit2DataHook.t.sol +360 -0
  94. package/test/TestPermit2Terminal.sol +36 -3
  95. package/test/TestRulesetQueueing.sol +23 -1
  96. package/test/TestRulesetQueuingStress.sol +20 -1
  97. package/test/TestRulesetWeightCaching.sol +127 -125
  98. package/test/TestSplits.sol +23 -1
  99. package/test/TestTerminalMigration.sol +11 -1
  100. package/test/TestTokenFlow.sol +18 -1
  101. package/test/TestWeightCacheStaleAfterRejection.sol +15 -1
  102. package/test/WeirdTokenTests.t.sol +54 -1
  103. package/test/fork/TestChainlinkPriceFeedFork.sol +6 -1
  104. package/test/formal/BondingCurveProperties.t.sol +8 -1
  105. package/test/formal/FeeProperties.t.sol +7 -1
  106. package/test/helpers/JBTest.sol +1 -1
  107. package/test/helpers/TestBaseWorkflow.sol +84 -1
  108. package/test/invariants/Phase3DeepInvariant.t.sol +13 -2
  109. package/test/invariants/RulesetsInvariant.t.sol +12 -2
  110. package/test/invariants/TerminalStoreInvariant.t.sol +11 -2
  111. package/test/invariants/TokensInvariant.t.sol +13 -2
  112. package/test/invariants/handlers/ComprehensiveHandler.sol +19 -1
  113. package/test/invariants/handlers/EconomicHandler.sol +31 -1
  114. package/test/invariants/handlers/Phase3Handler.sol +31 -1
  115. package/test/invariants/handlers/RulesetsHandler.sol +5 -1
  116. package/test/invariants/handlers/TerminalStoreHandler.sol +6 -1
  117. package/test/invariants/handlers/TokensHandler.sol +1 -1
  118. package/test/mock/MockERC20.sol +0 -2
  119. package/test/mock/MockMaliciousBeneficiary.sol +2 -1
  120. package/test/mock/MockMaliciousSplitHook.sol +2 -1
  121. package/test/mock/MockPriceFeed.sol +1 -1
  122. package/test/regression/HoldFeesCashOutReserved.t.sol +415 -0
  123. package/test/regression/WeightCacheBoundary.t.sol +291 -0
  124. package/test/units/static/JBChainlinkV3PriceFeed/TestPriceFeed.sol +0 -1
  125. package/test/units/static/JBController/JBControllerSetup.sol +10 -1
  126. package/test/units/static/JBController/TestBurnTokensOf.sol +8 -1
  127. package/test/units/static/JBController/TestClaimTokensFor.sol +4 -1
  128. package/test/units/static/JBController/TestDeployErc20For.sol +7 -1
  129. package/test/units/static/JBController/TestLaunchProjectFor.sol +21 -1
  130. package/test/units/static/JBController/TestLaunchRulesetsFor.sol +21 -1
  131. package/test/units/static/JBController/TestMigrateController.sol +10 -1
  132. package/test/units/static/JBController/TestMintTokensOfUnits.sol +10 -1
  133. package/test/units/static/JBController/TestPayReservedTokenToTerminal.sol +4 -1
  134. package/test/units/static/JBController/TestReceiveMigrationFrom.sol +5 -1
  135. package/test/units/static/JBController/TestRulesetViews.sol +7 -1
  136. package/test/units/static/JBController/TestSendReservedTokensToSplitsOf.sol +21 -1
  137. package/test/units/static/JBController/TestSetSplitGroupsOf.sol +6 -1
  138. package/test/units/static/JBController/TestSetTokenFor.sol +13 -1
  139. package/test/units/static/JBController/TestSetUriOf.sol +5 -1
  140. package/test/units/static/JBController/TestTransferCreditsFrom.sol +11 -1
  141. package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +12 -1
  142. package/test/units/static/JBDirectory/JBDirectorySetup.sol +4 -1
  143. package/test/units/static/JBDirectory/TestPrimaryTerminalOf.sol +5 -1
  144. package/test/units/static/JBDirectory/TestSetControllerOf.sol +11 -1
  145. package/test/units/static/JBDirectory/TestSetControllerOfMigrationOrder.sol +7 -1
  146. package/test/units/static/JBDirectory/TestSetPrimaryTerminalOf.sol +11 -1
  147. package/test/units/static/JBDirectory/TestSetTerminalsOf.sol +10 -1
  148. package/test/units/static/JBERC20/JBERC20Setup.sol +2 -1
  149. package/test/units/static/JBERC20/SigUtils.sol +2 -0
  150. package/test/units/static/JBERC20/TestInitialize.sol +1 -1
  151. package/test/units/static/JBERC20/TestName.sol +1 -1
  152. package/test/units/static/JBERC20/TestNonces.sol +3 -1
  153. package/test/units/static/JBERC20/TestSymbol.sol +1 -1
  154. package/test/units/static/JBFeelessAdresses/JBFeelessSetup.sol +2 -1
  155. package/test/units/static/JBFeelessAdresses/TestInterfaces.sol +2 -1
  156. package/test/units/static/JBFeelessAdresses/TestSetFeelessAddress.sol +1 -1
  157. package/test/units/static/JBFees/TestFeesFuzz.sol +1 -1
  158. package/test/units/static/JBFixedPointNumber/TestAdjustDecimals.sol +0 -1
  159. package/test/units/static/JBFixedPointNumber/TestAdjustDecimalsFuzz.sol +0 -1
  160. package/test/units/static/JBFundAccessLimits/JBFundAccessSetup.sol +3 -1
  161. package/test/units/static/JBFundAccessLimits/TestFundAccessLimitsEdge.sol +4 -1
  162. package/test/units/static/JBFundAccessLimits/TestPayoutLimitOf.sol +4 -1
  163. package/test/units/static/JBFundAccessLimits/TestPayoutLimitsOf.sol +8 -1
  164. package/test/units/static/JBFundAccessLimits/TestSetFundAccessLimitsFor.sol +8 -1
  165. package/test/units/static/JBFundAccessLimits/TestSurplusAllowanceOf.sol +4 -1
  166. package/test/units/static/JBFundAccessLimits/TestSurplusAllowancesOf.sol +7 -1
  167. package/test/units/static/JBMetadataResolver/TestGetDataFor.sol +1 -1
  168. package/test/units/static/JBMetadataResolver/TestMetadataResolverEdgeCases.sol +2 -1
  169. package/test/units/static/JBMetadataResolver/TestMetadataResolverFuzz.sol +2 -1
  170. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +12 -1
  171. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +9 -1
  172. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +18 -2
  173. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +44 -9
  174. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +48 -23
  175. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +18 -2
  176. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +13 -3
  177. package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +21 -4
  178. package/test/units/static/JBMultiTerminal/TestPay.sol +35 -7
  179. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +206 -19
  180. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +15 -1
  181. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +297 -1
  182. package/test/units/static/JBPermissions/JBPermissionsSetup.sol +2 -1
  183. package/test/units/static/JBPermissions/TestHasPermission.sol +1 -1
  184. package/test/units/static/JBPermissions/TestHasPermissions.sol +1 -1
  185. package/test/units/static/JBPermissions/TestSetPermissionsFor.sol +3 -1
  186. package/test/units/static/JBPrices/JBPricesSetup.sol +6 -1
  187. package/test/units/static/JBPrices/TestAddPriceFeedFor.sol +6 -1
  188. package/test/units/static/JBPrices/TestPricePerUnitOf.sol +4 -1
  189. package/test/units/static/JBPrices/TestPrices.sol +4 -1
  190. package/test/units/static/JBProjects/JBProjectsSetup.sol +2 -1
  191. package/test/units/static/JBProjects/TestCreateFor.sol +3 -1
  192. package/test/units/static/JBProjects/TestInitialProject.sol +2 -1
  193. package/test/units/static/JBProjects/TestInterfaces.sol +0 -1
  194. package/test/units/static/JBProjects/TestSetResolver.sol +2 -1
  195. package/test/units/static/JBProjects/TestTokenUri.sol +3 -1
  196. package/test/units/static/JBRulesetMetadataResolver/TestSetCashOutTaxRateTo.sol +9 -1
  197. package/test/units/static/JBRulesets/JBRulesetsSetup.sol +3 -1
  198. package/test/units/static/JBRulesets/TestCurrentApprovalStatusForLatestRulesetOf.sol +9 -1
  199. package/test/units/static/JBRulesets/TestCurrentOf.sol +10 -1
  200. package/test/units/static/JBRulesets/TestGetRulesetOf.sol +7 -1
  201. package/test/units/static/JBRulesets/TestLatestQueuedRulesetOf.sol +9 -1
  202. package/test/units/static/JBRulesets/TestRulesets.sol +12 -1
  203. package/test/units/static/JBRulesets/TestRulesetsOf.sol +1 -1
  204. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +10 -1
  205. package/test/units/static/JBRulesets/TestUpdateRulesetWeightCache.sol +6 -1
  206. package/test/units/static/JBSplits/JBSplitsSetup.sol +3 -1
  207. package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +63 -13
  208. package/test/units/static/JBSplits/TestSetSplitGroupsOf.sol +8 -1
  209. package/test/units/static/JBSplits/TestSplitsLockedEdge.sol +6 -1
  210. package/test/units/static/JBSplits/TestSplitsOf.sol +1 -1
  211. package/test/units/static/JBSplits/TestSplitsPacking.sol +5 -2
  212. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +3 -1
  213. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +5 -1
  214. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +14 -1
  215. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +14 -1
  216. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +3 -1
  217. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +92 -1
  218. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +15 -1
  219. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +13 -1
  220. package/test/units/static/JBTerminalStore/TestRecordTerminalMigration.sol +8 -1
  221. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +16 -1
  222. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +15 -1
  223. package/test/units/static/JBTokens/JBTokensSetup.sol +5 -1
  224. package/test/units/static/JBTokens/TestBurnFrom.sol +4 -1
  225. package/test/units/static/JBTokens/TestClaimTokensFor.sol +4 -1
  226. package/test/units/static/JBTokens/TestDeployERC20ForUnits.sol +4 -1
  227. package/test/units/static/JBTokens/TestMintFor.sol +4 -1
  228. package/test/units/static/JBTokens/TestSetTokenFor.sol +4 -1
  229. package/test/units/static/JBTokens/TestTotalBalanceOf.sol +1 -1
  230. package/test/units/static/JBTokens/TestTotalSupplyOf.sol +1 -1
  231. package/test/units/static/JBTokens/TestTransferCreditsFrom.sol +3 -1
package/RISKS.md CHANGED
@@ -1,68 +1,189 @@
1
- # nana-core-v6 Risks
1
+ # nana-core-v6 -- Active Risk Vectors
2
2
 
3
- ## Trust Assumptions
3
+ Known security properties, trust assumptions, active vulnerability surfaces, and operational risks. Intended audience: experienced Solidity auditors looking for where to focus.
4
4
 
5
- ### What You Trust
5
+ ## 1. Trust Assumptions
6
6
 
7
- 1. **Shared Infrastructure** All projects share the same JBMultiTerminal and JBController instances. A bug in these contracts affects every project.
8
- 2. **Project Owner** — The ERC-721 holder can queue new rulesets, set terminals, configure splits, and delegate permissions. A malicious or compromised owner can fundamentally change project economics.
9
- 3. **Data Hooks** — If a ruleset specifies a data hook, that hook has absolute control over token minting weights and cash out parameters. A malicious data hook can drain the entire project treasury.
10
- 4. **Approval Hooks** — Can approve or reject ruleset transitions. A reverting approval hook doesn't freeze the project (try-catch fallback), but a malicious one could allow unexpected transitions.
11
- 5. **Price Feeds** — Surplus calculations depend on Chainlink price feeds. Stale or manipulated feeds affect cash out values and payout calculations. Staleness causes reverts (DoS), not fund loss.
12
- 6. **Fee Project (#1)** — 2.5% fees go to project #1. If project #1's terminal is misconfigured, fees are returned to the originating project's balance (not lost).
7
+ What must be true for the system to remain safe:
13
8
 
14
- ### What You Do NOT Need to Trust
9
+ - **Hooks do not exploit reentrancy.** No `ReentrancyGuard` anywhere in core. All safety relies on checks-effects-interactions ordering and the `JBTerminalStore_InadequateTerminalStoreBalance` backstop. If a hook finds a code path where state is read before a prior write has settled, value extraction may be possible.
10
+ - **Data hooks are honest.** A data hook has absolute control over payment weight, cash out tax rate, `totalSupply`, `cashOutCount`, and fund-forwarding amounts. A malicious data hook can bypass the bonding curve entirely (e.g., set `totalSupply = surplus` to get 1:1 redemptions) or divert 100% of incoming payments to external hooks. The protocol enforces `sum(hook.amount) <= payment.value` and `reclaimAmount + sum(hook.amount) <= project balance`, but within those bounds the hook is omnipotent.
11
+ - **Price feeds do not lie.** Surplus calculations, currency conversions for payouts, and surplus allowance all depend on `JBPrices`. A manipulated or stale feed causes incorrect surplus values. Chainlink feeds have staleness thresholds and sequencer checks, but project-specific feeds registered via `allowAddPriceFeed` have no such guarantee -- a project owner can register a feed that returns any value.
12
+ - **ERC-20 tokens behave standardly.** `_acceptFundsFor` uses a balance-before/after pattern, which handles fee-on-transfer tokens. However, rebasing tokens that change balances between transactions will cause `balanceOf` in `JBTerminalStore` to diverge from actual terminal holdings. Missing-return-value tokens are handled by `SafeERC20`.
13
+ - **Trusted forwarder is not compromised.** The ERC-2771 forwarder is immutable. If compromised, it can spoof `_msgSender()` for all permission-gated functions across `JBController`, `JBMultiTerminal`, `JBProjects`, `JBPrices`, and `JBPermissions`.
14
+ - **Project #1 terminal remains functional.** If the fee beneficiary project's terminal reverts, `_processFee` catches the error and returns the fee amount to the originating project's balance. This is safe but means fees are silently forgiven during outages.
15
+ - **`OMNICHAIN_RULESET_OPERATOR` is trusted.** This immutable address bypasses owner permission checks for `launchRulesetsFor`, `queueRulesetsOf`, and `setTerminalsOf`. A compromised operator can queue arbitrary rulesets for any project.
15
16
 
16
- - **Other projects** — Each project's balance is isolated by terminal address in JBTerminalStore
17
- - **Token holders** — Can only cash out proportional to the bonding curve
18
- - **Permit2** — Optional; projects work without it
17
+ ## 2. Economic Risks
19
18
 
20
- ## Known Risks
19
+ ### Bonding Curve
21
20
 
22
- ### By Design
21
+ - **Zero cash out guard.** `cashOutFrom` returns 0 when `cashOutCount == 0` (line 31 early return). Auditors should verify no code path bypasses this guard or reaches the `cashOutCount >= totalSupply` branch (line 37) with both values at 0.
22
+ - **Pending reserved tokens inflate `totalSupply`.** `totalTokenSupplyWithReservedTokensOf()` adds `pendingReservedTokenBalanceOf` to `totalSupply`, reducing per-token cash out value. A project owner who delays calling `sendReservedTokensToSplitsOf()` can suppress cash out values. Auditors should model the magnitude of this effect for projects with large pending reserves.
23
+ - **`mulDiv` rounding.** The bonding curve's subadditivity property (`cashOutFrom(a) + cashOutFrom(b) <= cashOutFrom(a+b)`) can be violated by <0.01% due to floor rounding. Economically insignificant per operation but could accumulate across many small cash outs.
24
+ - **Binary search in `minCashOutCountFor`.** The inverse cash out function uses binary search over `[1, totalSupply]`. For large supplies (>2^128), this is ~128 iterations of `mulDiv` calls. Verify gas cost remains bounded.
23
25
 
24
- | Risk | Description | Mitigation |
25
- |------|-------------|------------|
26
- | Data hook omnipotence | Data hooks override bonding curve parameters | Only use audited, trusted data hooks |
27
- | Last-holder advantage | Last token holder redeems remaining surplus at 1:1 | Bonding curve math; inherent to the design |
28
- | Pending reserved inflation | Pending reserved tokens dilute cash out values | Call `sendReservedTokensToSplitsOf` regularly |
29
- | No reentrancy guard | Protocol relies on CEI ordering, not mutex | State updates before all external calls |
30
- | Weight cache requirement | Projects with >20k cycles need progressive cache updates | Anyone can call `updateRulesetWeightCache` |
26
+ ### Fee Arithmetic
31
27
 
32
- ### Operational
28
+ - **Forward vs. backward fee asymmetry.** `feeAmountFrom` (forward) uses `mulDiv(amount, 25, 1000)`. `feeAmountResultingIn` (backward) uses `mulDiv(amount, 1000, 975) - amount`. These are algebraically equivalent but rounding differs. In `_returnHeldFees`, both are used on the same held fee entry (forward to compute `feeAmount`, backward when partially returning). Verify the interplay never undercharges.
29
+ - **Held fee amount mutation.** `_returnHeldFees` mutates `heldFee.amount` in-place via unchecked subtraction (line 1583). If the accounting is off by even 1 wei in the wrong direction, this underflows and corrupts the held fee entry.
33
30
 
34
- | Risk | Description | Mitigation |
35
- |------|-------------|------------|
36
- | Price feed DoS | Stale/reverting price feed blocks multi-currency operations | Monitor feed health; single-currency projects unaffected |
37
- | Split gas exhaustion | Very large split arrays (100+) may exceed block gas | Keep split count reasonable (<50) |
38
- | Held fee growth | Held fees array grows without cleanup | `_nextHeldFeeIndexOf` pointer skips processed entries |
31
+ ### First Cycle Behavior
39
32
 
40
- ## Reentrancy Analysis
33
+ - **`currentOf()` returns the stored ruleset directly in the first cycle.** The first cycle (`cycleNumber == 1`) uses the original weight with no decay applied. Weight decay via `weightCutPercent` only takes effect from the second cycle onward. This is by design and verified by test (`TestAuditResponseDesignProofs.test_currentOf_firstCycle_returnsOriginalWeight`).
41
34
 
42
- No `ReentrancyGuard`. Relies on state ordering (checks-effects-interactions):
35
+ ### Weight Decay
43
36
 
44
- | Function | State Updated Before External Call | Risk |
45
- |----------|-----------------------------------|------|
46
- | `_cashOutTokensOf` | Store balance deducted, tokens burned BEFORE transfer | LOW |
47
- | `_pay` | Store balance added, tokens minted BEFORE pay hooks | LOW |
48
- | `executePayout` | Payout limit recorded BEFORE split hook calls | LOW |
49
- | `processHeldFeesOf` | Index updated BEFORE fee processing | LOW |
50
- | `_sendReservedTokensToSplitsOf` | Pending balance zeroed BEFORE minting | LOW |
37
+ - **Weight cache starvation as DoS.** Projects with short duration and nonzero `weightCutPercent` that run >20,000 cycles without a cache update will revert on `currentOf()` with `WeightCacheRequired`. This blocks all operations (pay, cash out, payouts). Anyone can call `updateRulesetWeightCache()` to fix it, but an attacker could create projects designed to hit this.
38
+ - **Weight truncation.** Derived weight is cast to `uint112` in `_simulateCycledRulesetBasedOn`. If the derived weight exceeds `type(uint112).max` (unlikely but theoretically possible if cache state is corrupted), silent truncation occurs.
51
39
 
52
- **Key defense:** `JBTerminalStore_InadequateTerminalStoreBalance` revert prevents extracting more than available balance regardless of reentrancy.
40
+ ### Surplus Manipulation
53
41
 
54
- ## Permission Security
42
+ - **Cross-terminal surplus aggregation.** When `useTotalSurplusForCashOuts` is enabled, `recordCashOutFor` aggregates surplus across all terminals via `JBSurplus.currentSurplusOf()`, which calls `terminal.currentSurplusOf()` on each. If a malicious terminal is added to the project's directory, it could report inflated surplus. Defense: `InadequateTerminalStoreBalance` revert prevents extracting more than the actual terminal balance.
43
+ - **Price feed inconsistency across terminals.** Different tokens in different terminals are converted to a common currency via `JBPrices`. If price feeds between terminals are stale or inconsistent, aggregated surplus can be inflated/deflated, affecting cash out reclaim amounts.
55
44
 
56
- - ROOT (ID 1) grants all permissions but cannot be set for wildcard `projectId = 0`
57
- - ROOT operators cannot grant ROOT to other addresses
58
- - Permission 0 is reserved and cannot be set
59
- - All permission checks support ERC-2771 meta-transactions
45
+ ## 3. Reentrancy Surface
60
46
 
61
- ## Proven Invariants
47
+ No `ReentrancyGuard` is used. The system relies on state ordering and the `InadequateTerminalStoreBalance` backstop.
62
48
 
63
- 1. No flash-loan profit (12 attack vectors tested)
64
- 2. Terminal balance >= sum of recorded project balances
65
- 3. Total inflows >= total outflows
66
- 4. Fee project balance monotonically increases
67
- 5. Token supply = creditSupply + erc20.totalSupply()
68
- 6. Current ruleset always exists after launch
49
+ ### External Call Map
50
+
51
+ | Function | State Changes Before External Call | External Calls | Risk |
52
+ |----------|-----------------------------------|----------------|------|
53
+ | `_pay` | `STORE.recordPaymentFrom` (balance incremented), `controller.mintTokensOf` (tokens minted) | Pay hooks via `_fulfillPayHookSpecificationsFor` | LOW -- full state settlement before hooks |
54
+ | `_cashOutTokensOf` | `STORE.recordCashOutFor` (balance decremented), `controller.burnTokensOf` (tokens burned), `_transferFrom` (beneficiary paid) | Cash out hooks via `_fulfillCashOutHookSpecificationsFor`, then `_takeFeeFrom` | MEDIUM -- beneficiary receives funds before hooks execute; hooks run before fees are taken |
55
+ | `executePayout` | `STORE.recordPayoutFor` already consumed payout limit | `split.hook.processSplitWith`, `terminal.pay/addToBalance` | MEDIUM -- split hook receives funds and can re-enter; payout limit already consumed prevents double-payout |
56
+ | `processHeldFeesOf` | `delete _heldFeesOf[...][currentIndex]`, `_nextHeldFeeIndexOf` incremented | `_processFee` -> `this.executeProcessFee` -> `terminal.pay` | LOW -- index advanced before external call; re-reads from storage each iteration |
57
+ | `_sendReservedTokensToSplitsOf` | `pendingReservedTokenBalanceOf` zeroed, tokens minted to controller | Split hooks, terminal payments | LOW -- pending balance cleared before minting prevents double-distribution |
58
+ | `_useAllowanceOf` | `STORE.recordUsedAllowanceOf` (allowance consumed, balance decremented) | `_takeFeeFrom` (fee payment/holding), `_transferFrom` (beneficiary) | LOW -- allowance consumed before calls |
59
+ | `migrateBalanceOf` | `STORE.recordTerminalMigration` (balance zeroed) | `to.addToBalanceOf` | LOW -- balance zeroed before transfer |
60
+
61
+ ### Cross-Function Reentrancy to Explore
62
+
63
+ - **Pay hook -> `cashOutTokensOf`**: After `_pay` mints tokens, a pay hook could call `cashOutTokensOf`. The cash out sees post-payment balance and post-mint supply. Not profitable after fees in tested scenarios, but verify with data hooks that modify weights.
64
+ - **Cash out hook -> `pay`**: During `_cashOutTokensOf`, after tokens are burned and beneficiary is paid, a cash out hook could call `pay()` adding to the balance. Fees haven't been taken yet at this point. Verify the fee calculation on `amountEligibleForFees` isn't affected.
65
+ - **Split hook -> `pay` on same project**: During `sendPayoutsOf`, a split hook receives funds and calls `pay()` on the same project. Payout limit is consumed, but the payment increases balance and mints tokens. The funds came from the project's own balance, so no value creation -- but verify the accounting.
66
+ - **Fee processing -> any re-entry**: `_processFee` uses `this.executeProcessFee` (external call via try-catch). Inside, it calls `terminal.pay()` on project #1. If project #1 has a pay hook that calls back, the fee amount is already deducted.
67
+
68
+ ### Key Backstop
69
+
70
+ `JBTerminalStore_InadequateTerminalStoreBalance` revert prevents extracting more than the recorded balance from any terminal regardless of reentrancy state. This is the final defense for all value extraction paths. Auditors should verify this check cannot be bypassed by manipulating the recorded balance (e.g., via `recordAddedBalanceFor`, which has no access control -- balance is keyed by `msg.sender`, so only a terminal can inflate its own balance).
71
+
72
+ ## 4. Access Control
73
+
74
+ ### Permission System
75
+
76
+ - **ROOT (ID 1) grants all permissions.** Including permissions not yet defined. Future permission IDs automatically fall under ROOT.
77
+ - **ROOT cannot be set for wildcard `projectId = 0`.** The actual enforcement in `setPermissionsFor` is: if the caller is not the account itself, they must have ROOT for the target project, AND they cannot set ROOT for others, AND they cannot set any permissions on the wildcard project. ROOT holders for a specific project can set non-ROOT permissions for operators on that project. Auditors should verify the exact boundary -- particularly whether a ROOT operator on project X can escalate to ROOT on project Y through any indirect path.
78
+ - **Empty permission arrays pass `hasPermissions`.** By design (vacuous truth). Any caller that expects "the operator has at least one of these permissions" must validate the array is non-empty.
79
+ - **`OMNICHAIN_RULESET_OPERATOR` bypass.** This immutable address can `launchRulesetsFor`, `queueRulesetsOf`, and set terminals for any project without owner permission. The trust assumption is that this operator only queues rulesets that the omnichain deployer's logic permits. If this address is an EOA or an upgradeable contract, it is a single point of failure for all projects.
80
+
81
+ ### Splits GroupId Namespace
82
+
83
+ - **GroupId namespace overlap between terminals and token contracts is prevented.** Terminals use `uint160(tokenAddress)` as the `groupId` for payout split groups -- these have zero upper 96 bits. The self-auth path in `setSplitGroupsOf` now requires the upper 96 bits of the `groupId` to be non-zero (in addition to the lower 160 bits matching `msg.sender`). This means bare-address groupIds (upper 96 bits = 0) are protocol-reserved and always require controller authorization. Without this restriction, an accepted token contract could call `setSplitGroupsOf` to overwrite the terminal's payout splits for its own address. The 721 hook is unaffected since it uses `hookAddress | tierId << 160` (non-zero upper bits).
84
+
85
+ ### Migration
86
+
87
+ - **Controller migration** requires `allowSetController` in the current ruleset. During migration, `JBController.migrate()` reverts if there are pending reserved tokens. An attacker cannot front-run migration to inflate pending reserves (they'd need mint permission), but a project with organic pending reserves must distribute them first.
88
+ - **Terminal migration** requires `allowTerminalMigration` in the current ruleset. Held fees are intentionally NOT migrated -- they belong to project #1. Verify that a project owner cannot use migration to escape held fee obligations.
89
+ - **Directory updates** (`setTerminalsOf`, `setControllerOf`) are gated by `IJBDirectoryAccessControl` checks that read from the current ruleset's metadata flags. If the current ruleset allows these changes, anyone with the appropriate permission can redirect all of a project's fund flows.
90
+
91
+ ### Ruleset Queuing
92
+
93
+ - Only the project's controller can call `RULESETS.queueFor()` (enforced by `onlyControllerOf` modifier).
94
+ - The controller allows queuing by the project owner, anyone with `QUEUE_RULESETS` permission, or the `OMNICHAIN_RULESET_OPERATOR`.
95
+ - For `duration = 0` projects, a queued ruleset takes effect immediately. This means an owner can atomically change all project economics (weight, tax rate, splits, payout limits) in the same transaction as other operations.
96
+
97
+ ## 5. DoS Vectors
98
+
99
+ ### Unbounded Arrays
100
+
101
+ | Array | Growth Mechanism | Cleanup | Risk |
102
+ |-------|-----------------|---------|------|
103
+ | `_heldFeesOf[projectId][token]` | Each held-fee payout appends | `_nextHeldFeeIndexOf` pointer skips processed; full delete when all processed | MODERATE -- if held fees accumulate faster than the 28-day unlock window, the array grows unboundedly. `processHeldFeesOf` takes a `count` param, so partial processing is possible. |
104
+ | `splits[]` | Set by project owner per ruleset | Replaced wholesale | MODERATE -- no explicit cap. At 100+ splits, `_sendPayoutsToSplitGroupOf` gas exceeds 10M. Percentage constraint limits useful splits to ~300-500 but doesn't prevent a malicious owner from setting more. |
105
+ | `_accountingContextsOf[projectId]` | `addAccountingContextsFor` (append-only) | Never shrinks | LOW -- duplicate prevention limits growth; realistic max ~100 tokens. But since it's append-only, a project that accepts many tokens over time cannot remove old ones. |
106
+ | Payout limits / surplus allowances | Set per ruleset | Replaced per ruleset | LOW -- currency ordering constraint limits ~30-50. |
107
+ | `_terminalsOf[projectId]` | `setTerminalsOf` (replaced wholesale) | Replaced | LOW -- realistic max 5-10. |
108
+
109
+ ### Price Feed Reverts
110
+
111
+ - If a Chainlink feed is stale beyond its threshold, `JBChainlinkV3PriceFeed` reverts. This blocks all multi-currency operations for projects using that feed: `pay`, `cashOutTokensOf`, `sendPayoutsOf`, `useAllowanceOf`.
112
+ - L2 sequencer downtime triggers `JBChainlinkV3SequencerPriceFeed` to revert during downtime + grace period.
113
+ - Single-currency projects (where `amount.currency == ruleset.baseCurrency()`) are unaffected.
114
+ - Price feeds are immutable once set in `JBPrices` -- a broken feed cannot be replaced.
115
+
116
+ ### Approval Hook Griefing
117
+
118
+ - A reverting approval hook is caught by try-catch and treated as `Failed`. This causes fallback to `basedOnId` chain.
119
+ - A gas-consuming approval hook (e.g., infinite loop) can DoS `currentOf()` via gas exhaustion. The try-catch does not limit gas. This is accepted risk since the project owner chose their own approval hook, but it means a malicious approval hook can permanently freeze its project's operations.
120
+ - Approval hook rejection at a ruleset boundary triggers complex fallback behavior: the protocol simulates cycling from the last approved ruleset. Verify this simulation always produces economically correct results, especially when multiple rulesets are queued and rejected in sequence.
121
+
122
+ ### Other DoS Surfaces
123
+
124
+ - `sendPayoutsOf` is callable by anyone (unless `ownerMustSendPayouts` is set). A split recipient that always reverts will cause that split's payout to fail, but the try-catch returns the amount to the project balance. Payout limit is still consumed. The project owner must wait until the next cycle.
125
+ - `addAccountingContextsFor` is gated by `allowAddAccountingContext` in the ruleset, but the contexts array is append-only and never shrinks. Over many rulesets, this could grow large enough to cause gas issues in functions that iterate over all contexts (e.g., `currentSurplusOf` when no explicit contexts are passed).
126
+
127
+ ## 6. Integration Risks
128
+
129
+ ### Non-Standard ERC-20s
130
+
131
+ - **Fee-on-transfer tokens**: Handled by `_acceptFundsFor` using balance-before/after pattern. The actual received amount is used, not the passed `amount`. However, `_transferFrom` for outbound transfers uses the nominal amount. If the token charges fees on transfer-out, the terminal's actual balance decreases more than `balanceOf` in the store records. Over time, `terminal.balance(token) < sum(store.balanceOf(projectId, terminal, token))`, breaking the balance conservation invariant.
132
+ - **Rebasing tokens**: Tokens that change balances (e.g., stETH, AMPL) will cause `JBTerminalStore.balanceOf` to diverge from actual terminal holdings. Positive rebases create untracked surplus; negative rebases can cause `InadequateTerminalStoreBalance` reverts on withdrawals.
133
+ - **Tokens with blocklists** (e.g., USDC, USDT): If a split beneficiary or cash out beneficiary is blocklisted, the transfer reverts. For split payouts, try-catch returns the amount to the project. For cash out beneficiaries, the entire `cashOutTokensOf` call reverts.
134
+ - **Low-decimal tokens** (e.g., USDC with 6 decimals): Weight and token counts use 18 decimals internally. The fixed-point conversion in `recordPaymentFrom` uses `mulDiv(amount.value, weight, weightRatio)`. With large weight values and small decimal tokens, precision loss may be significant.
135
+
136
+ ### Permit2 Interactions
137
+
138
+ - `_acceptFundsFor` tries direct ERC-20 `transferFrom` first (if allowance is sufficient), then falls back to Permit2. The Permit2 `permit` call is wrapped in try-catch -- failure emits an event but doesn't revert the payment.
139
+ - `_transferFrom` for outbound transfers also falls back to Permit2 if direct allowance is insufficient. This means outbound transfers (to beneficiaries, split hooks) may unexpectedly use Permit2 state.
140
+ - The `uint160` cast on line 1897 of `JBMultiTerminal.sol` limits Permit2 transfers to `type(uint160).max`. Amounts above this revert with `OverflowAlert`.
141
+
142
+ ### Cross-Terminal Surplus Aggregation
143
+
144
+ - `JBSurplus.currentSurplusOf` calls `terminal.currentSurplusOf()` on each terminal. These are external view calls with no gas limit. A malicious or gas-expensive terminal can cause this aggregation to revert, blocking cash outs for any project that has `useTotalSurplusForCashOuts` enabled and uses that terminal.
145
+ - The surplus calculation converts each terminal's balance to a common currency via price feeds. Rounding accumulates across terminals. With N terminals and M tokens each, there are N*M price conversions, each with up to 1 wei of rounding error.
146
+
147
+ ### `recordAddedBalanceFor` Access Control
148
+
149
+ - `JBTerminalStore.recordAddedBalanceFor` has **no access control**. Any address can call it. The balance is keyed by `msg.sender` (the terminal address), so only a terminal can inflate its own recorded balance. This is safe as long as all terminals correctly track their actual holdings. A buggy or malicious terminal implementation could call `recordAddedBalanceFor` without actually receiving tokens, inflating the recorded balance above actual holdings.
150
+
151
+ ## 7. Invariants to Verify
152
+
153
+ These should hold at all times and are the most productive targets for formal verification or invariant testing:
154
+
155
+ ### Balance Conservation
156
+ - `terminal.balance(token) >= sum(store.balanceOf(projectId, terminal, token))` for all projects sharing a terminal. Fee amounts held but not yet processed are included in the terminal's actual balance but not in any project's store balance. Violation indicates a bug in fee handling or reentrancy.
157
+
158
+ ### Fund Conservation
159
+ - Total inflows to a project (payments + `addToBalance`) >= total outflows (payouts + cash outs + surplus allowance usage + fees). Rounding should favor the protocol (fees round up, reclaims round down).
160
+
161
+ ### Fee Monotonicity
162
+ - Project #1 balance only increases over time (fees flow in, never out via protocol mechanics). Exception: project #1 itself can pay out or cash out.
163
+
164
+ ### Token Supply Consistency
165
+ - `TOKENS.totalSupplyOf(projectId) == creditSupply + erc20.totalSupply()` at all times.
166
+ - `totalTokenSupplyWithReservedTokensOf(projectId) == TOKENS.totalSupplyOf(projectId) + pendingReservedTokenBalanceOf[projectId]`.
167
+
168
+ ### Payout Limit Enforcement
169
+ - `usedPayoutLimitOf[terminal][projectId][token][cycleNumber][currency] <= payoutLimitOf(...)` after every `recordPayoutFor`. Verify this holds even when the same project pays out from multiple terminals in the same cycle.
170
+
171
+ ### Surplus Allowance Enforcement
172
+ - `usedSurplusAllowanceOf[terminal][projectId][token][rulesetId][currency] <= surplusAllowanceOf(...)` after every `recordUsedAllowanceOf`.
173
+
174
+ ### Cash Out Bound
175
+ - `reclaimAmount + sum(hookSpecification.amounts) <= balanceOf[terminal][projectId][token]` after every `recordCashOutFor`. This is the `InadequateTerminalStoreBalance` check. Verify it is never circumvented.
176
+
177
+ ### Ruleset Existence
178
+ - After `launchProjectFor()`, `RULESETS.currentOf(projectId)` always returns a valid ruleset (non-zero `cycleNumber`). A project in a state where `currentOf` returns an empty ruleset cannot accept payments (`RulesetNotFound` revert), but verify this cannot happen accidentally.
179
+
180
+ ### No Flash-Loan Profit
181
+ - `pay() + cashOutTokensOf()` in the same transaction should never be profitable after fees. The 2.5% fee should make single-block round-trips unprofitable. Verify this holds when data hooks modify weights or cash out parameters.
182
+
183
+ ### Same-Terminal Fee Exemption
184
+
185
+ - **Payouts between projects on the same terminal are fee-exempt by design.** When `executePayout` sends funds to a split's project that uses the same `JBMultiTerminal`, no fee is charged — the payout is routed via `addToBalanceOf` (internal accounting) rather than requiring an external transfer. Fees only apply when funds leave the terminal (sent to an EOA beneficiary or a different terminal). This is intentional: fees protect against fund egress, not intra-terminal accounting moves. Verified by test (`TestAuditResponseDesignProofs.test_sameTerminal_payoutNoFee`).
186
+ - **Fee-free surplus tracking prevents round-trip fee bypass.** When a project receives a fee-free intra-terminal payout, `_feeFreeSurplusOf[projectId][token]` is incremented by the payout amount. During cashout with `cashOutTaxRate == 0`, the 2.5% fee is applied only up to this accumulated surplus — then decremented. Once depleted, subsequent cashouts are fee-free again. This scopes the fee precisely to the fee-free inflow: a 1 wei griefing payout only costs the victim fees on 1 wei, not their entire balance. Without this, an attacker could route payouts through a pass-through project with `cashOutTaxRate=0` and cash out fee-free. When `cashOutTaxRate != 0`, the fee applies to the full reclaim amount regardless of surplus. Verified by 7 tests in `TestFeeFreeCashOutBypass.sol`.
187
+
188
+ ### Held Fee Integrity
189
+ - `sum(heldFee.amount for active entries) + sum(processed fees) == total fees ever taken with shouldHoldFees=true`. Active entries are those from `_nextHeldFeeIndexOf` to end of array. Verify `_returnHeldFees`' in-place mutation of `heldFee.amount` preserves this invariant.
package/SKILLS.md CHANGED
@@ -42,6 +42,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
42
42
  | `claimTokensFor(address holder, uint256 projectId, uint256 count, address beneficiary)` | Redeems credits for ERC-20 tokens into beneficiary's wallet. |
43
43
  | `setSplitGroupsOf(uint256 projectId, uint256 rulesetId, JBSplitGroup[] splitGroups)` | Sets the split groups for a project's ruleset. |
44
44
  | `setTokenFor(uint256 projectId, IJBToken token)` | Sets an existing ERC-20 token for the project (requires `allowSetCustomToken` in ruleset). |
45
+ | `setTokenMetadataOf(uint256 projectId, string name, string symbol)` | Sets the name and symbol of a project's ERC-20 token. Requires `SET_TOKEN_METADATA` permission. |
45
46
  | `setUriOf(uint256 projectId, string uri)` | Sets the project's metadata URI. |
46
47
  | `transferCreditsFrom(address holder, uint256 projectId, address recipient, uint256 creditCount)` | Transfers credits between addresses (reverts if `pauseCreditTransfers` is set in ruleset). |
47
48
  | `addPriceFeedFor(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency, IJBPriceFeed feed)` | Registers a price feed (requires `allowAddPriceFeed` in ruleset). |
@@ -75,7 +76,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
75
76
  |----------|--------------|
76
77
  | `recordPaymentFrom(address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` | Records a payment. Applies data hook if enabled. Returns ruleset, token count, hook specifications. |
77
78
  | `recordPayoutFor(uint256 projectId, JBAccountingContext accountingContext, uint256 amount, uint256 currency)` | Records a payout. Enforces payout limits. Returns ruleset and amount paid out. |
78
- | `recordCashOutFor(address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bytes metadata)` | Records a cash out. Computes reclaim via bonding curve. Returns ruleset, reclaim amount, tax rate, and hook specifications. |
79
+ | `recordCashOutFor(address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bool beneficiaryIsFeeless, bytes metadata)` | Records a cash out. Computes reclaim via bonding curve. Returns ruleset, reclaim amount, tax rate, and hook specifications. |
79
80
  | `recordUsedAllowanceOf(uint256 projectId, JBAccountingContext accountingContext, uint256 amount, uint256 currency)` | Records surplus allowance usage. Enforces allowance limits. Returns ruleset and used amount. |
80
81
  | `recordAddedBalanceFor(uint256 projectId, address token, uint256 amount)` | Records funds added to a project's balance. |
81
82
  | `recordTerminalMigration(uint256 projectId, address token)` | Records a terminal migration, returning the full balance. |
@@ -98,8 +99,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
98
99
  | Function | What it does |
99
100
  |----------|--------------|
100
101
  | `setPermissionsFor(address account, JBPermissionsData permissionsData)` | Grants or revokes operator permissions. ROOT operators can set non-ROOT permissions for others. |
101
- | `hasPermission(address operator, address account, uint256 projectId, uint256 permissionId)` | Checks if an operator has a specific permission. |
102
- | `hasPermissions(address operator, address account, uint256 projectId, uint256[] permissionIds)` | Checks if an operator has all specified permissions. |
102
+ | `hasPermission(address operator, address account, uint256 projectId, uint256 permissionId, bool includeRoot, bool includeWildcardProjectId)` | Checks if an operator has a specific permission. |
103
+ | `hasPermissions(address operator, address account, uint256 projectId, uint256[] permissionIds, bool includeRoot, bool includeWildcardProjectId)` | Checks if an operator has all specified permissions. |
103
104
 
104
105
  ### JBDirectory
105
106
 
@@ -129,6 +130,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
129
130
  | `totalBalanceOf(address holder, uint256 projectId)` | Returns combined credit + ERC-20 balance. |
130
131
  | `creditBalanceOf(address holder, uint256 projectId)` | Returns the holder's credit balance. |
131
132
  | `tokenOf(uint256 projectId)` | Returns the ERC-20 token for a project (`IJBToken`). |
133
+ | `setTokenMetadataFor(uint256 projectId, string name, string symbol)` | Sets the name and symbol of a project's token. Controller-only. |
132
134
 
133
135
  ### JBSplits
134
136
 
@@ -136,6 +138,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
136
138
  |----------|--------------|
137
139
  | `splitsOf(uint256 projectId, uint256 rulesetId, uint256 groupId)` | Returns splits for a project/ruleset/group. Falls back to ruleset ID 0 if none set. |
138
140
 
141
+ **Self-auth for `setSplitGroupsOf`**: A contract can set its own split groups without controller authorization if the lower 160 bits of the `groupId` match `msg.sender` AND the upper 96 bits are non-zero. GroupIds with zero upper 96 bits (bare addresses like `uint256(uint160(token))`) are protocol-reserved for terminal payout groups and always require controller auth.
142
+
139
143
  ### Other
140
144
 
141
145
  | Function | What it does |
@@ -170,7 +174,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
170
174
  | Struct | Key Fields | Used In |
171
175
  |--------|------------|---------|
172
176
  | `JBBeforePayRecordedContext` | `terminal`, `payer`, `amount (JBTokenAmount)`, `projectId`, `rulesetId`, `beneficiary`, `weight`, `reservedPercent`, `metadata` | `IJBRulesetDataHook.beforePayRecordedWith()` input |
173
- | `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `useTotalSurplus`, `cashOutTaxRate`, `metadata` | `IJBRulesetDataHook.beforeCashOutRecordedWith()` input |
177
+ | `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `useTotalSurplus`, `cashOutTaxRate`, `beneficiaryIsFeeless`, `metadata` | `IJBRulesetDataHook.beforeCashOutRecordedWith()` input |
174
178
  | `JBAfterPayRecordedContext` | `payer`, `projectId`, `rulesetId`, `amount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `weight`, `newlyIssuedTokenCount`, `beneficiary`, `hookMetadata`, `payerMetadata` | `IJBPayHook.afterPayRecordedWith()` input |
175
179
  | `JBAfterCashOutRecordedContext` | `holder`, `projectId`, `rulesetId`, `cashOutCount`, `reclaimedAmount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `cashOutTaxRate`, `beneficiary`, `hookMetadata`, `cashOutMetadata` | `IJBCashOutHook.afterCashOutRecordedWith()` input |
176
180
  | `JBPayHookSpecification` | `hook (IJBPayHook)`, `amount`, `metadata` | Returned by data hook; specifies which pay hooks to call and how much to forward |
@@ -208,8 +212,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
208
212
  | `weight = 0` | `JBRuleset` / `JBRulesetConfig` | No token issuance for payments. |
209
213
  | `weight = 1` | `JBRuleset` / `JBRulesetConfig` | Inherit decayed weight from previous ruleset (sentinel). |
210
214
  | `duration = 0` | `JBRuleset` / `JBRulesetConfig` | Ruleset never expires; must be explicitly replaced by a new queued ruleset (takes effect immediately). |
211
- | `projectId = 0` | `JBPermissionsData` | Wildcard: permission applies to ALL projects. Cannot be combined with ROOT (255). |
212
- | `permissionId = 255` | `JBPermissions` | ROOT: grants all permissions for the scoped project. |
215
+ | `projectId = 0` | `JBPermissionsData` | Wildcard: permission applies to ALL projects. Cannot be combined with ROOT (1). |
216
+ | `permissionId = 1` | `JBPermissions` | ROOT: grants all permissions for the scoped project. |
213
217
  | `rulesetId = 0` | `JBSplits.splitsOf()` | Fallback split group used when no splits are set for a specific ruleset. |
214
218
  | `projectId = 0` | `JBPrices.addPriceFeedFor()` | Sets a protocol-wide default price feed (owner-only). |
215
219
 
@@ -231,6 +235,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
231
235
  - `JBERC20` is cloned via `Clones.clone()` -- its constructor sets invalid name/symbol; real values set in `initialize()`
232
236
  - Fee is 2.5% (`FEE = 25` out of `MAX_FEE = 1000`)
233
237
  - Project #1 is the fee beneficiary project (receives all protocol fees)
238
+ - **Fee-free cashout exemption is scoped to fee-free intra-terminal payout amounts.** `_feeFreeSurplusOf[projectId][token]` accumulates the value of fee-free payouts. During cashout with `cashOutTaxRate=0`, the 2.5% fee applies only up to this surplus, then depletes. Once consumed, subsequent cashouts are fee-free again. This prevents a round-trip fee bypass (intra-terminal payout → zero-tax cashout) while scoping fees precisely to the fee-free inflow.
234
239
  - `JBProjects` constructor optionally mints project #1 to `feeProjectOwner` -- if `address(0)`, no fee project is created
235
240
  - `JBMultiTerminal` derives `DIRECTORY` and `RULESETS` from the provided `store` in its constructor -- not passed directly
236
241
  - `JBPrices.pricePerUnitOf()` checks project-specific feed, then inverse, then falls back to `DEFAULT_PROJECT_ID = 0`
package/STYLE_GUIDE.md CHANGED
@@ -197,7 +197,7 @@ interface IJBExample is IJBBase {
197
197
  | Public/external function | `camelCase` | `cashOutTokensOf` |
198
198
  | Internal/private function | `_camelCase` | `_processFee` |
199
199
  | Internal storage | `_camelCase` | `_accountingContextForTokenOf` |
200
- | Function parameter | `camelCase` | `projectId`, `cashOutCount` |
200
+ | Function parameter | `camelCase` (no underscores) | `projectId`, `cashOutCount` |
201
201
 
202
202
  ## NatSpec
203
203
 
@@ -253,9 +253,12 @@ uint256 public constant MAX_RESERVED_PERCENT = 10_000;
253
253
 
254
254
  ## Function Calls
255
255
 
256
- Use named parameters for readability when calling functions with 3+ arguments:
256
+ Use named arguments for all function calls with 2 or more arguments — in both `src/` and `script/`:
257
257
 
258
258
  ```solidity
259
+ // Good — named arguments
260
+ token.mint({account: beneficiary, amount: count});
261
+ _transferOwnership({newOwner: address(0), projectId: 0});
259
262
  PERMISSIONS.hasPermission({
260
263
  operator: sender,
261
264
  account: account,
@@ -264,8 +267,18 @@ PERMISSIONS.hasPermission({
264
267
  includeRoot: true,
265
268
  includeWildcardProjectId: true
266
269
  });
270
+
271
+ // Bad — positional arguments with 2+ args
272
+ token.mint(beneficiary, count);
273
+ _transferOwnership(address(0), 0);
267
274
  ```
268
275
 
276
+ Single-argument calls use positional style: `_burn(amount)`.
277
+
278
+ This also applies to constructor calls, struct literals, and inherited/library calls (e.g., OZ `_mint`, `_safeMint`, `safeTransfer`, `allowance`, `Clones.cloneDeterministic`).
279
+
280
+ Named argument keys must use **camelCase** — never underscores. If a function's parameter names use underscores, rename them to camelCase first.
281
+
269
282
  ## Multiline Signatures
270
283
 
271
284
  ```solidity
@@ -553,6 +566,7 @@ CI checks formatting via `forge fmt --check`.
553
566
 
554
567
  CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
555
568
 
569
+
556
570
  ## Repo-Specific Deviations
557
571
 
558
572
  None. This repo follows the standard configuration exactly.