@bananapus/core-v6 0.0.24 → 0.0.25

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 (177) hide show
  1. package/ADMINISTRATION.md +16 -3
  2. package/ARCHITECTURE.md +28 -9
  3. package/AUDIT_INSTRUCTIONS.md +102 -17
  4. package/CHANGE_LOG.md +18 -8
  5. package/README.md +58 -2
  6. package/RISKS.md +13 -20
  7. package/SKILLS.md +158 -11
  8. package/STYLE_GUIDE.md +11 -6
  9. package/USER_JOURNEYS.md +53 -16
  10. package/foundry.toml +1 -1
  11. package/package.json +2 -2
  12. package/script/Deploy.s.sol +2 -2
  13. package/script/DeployPeriphery.s.sol +2 -5
  14. package/script/helpers/CoreDeploymentLib.sol +2 -2
  15. package/src/JBChainlinkV3PriceFeed.sol +1 -1
  16. package/src/JBChainlinkV3SequencerPriceFeed.sol +1 -1
  17. package/src/JBController.sol +14 -7
  18. package/src/JBDeadline.sol +1 -1
  19. package/src/JBDirectory.sol +1 -1
  20. package/src/JBERC20.sol +6 -2
  21. package/src/JBFeelessAddresses.sol +1 -1
  22. package/src/JBFundAccessLimits.sol +1 -1
  23. package/src/JBMultiTerminal.sol +53 -4
  24. package/src/JBPermissions.sol +6 -2
  25. package/src/JBPrices.sol +1 -1
  26. package/src/JBProjects.sol +1 -1
  27. package/src/JBRulesets.sol +1 -1
  28. package/src/JBSplits.sol +1 -1
  29. package/src/JBTerminalStore.sol +57 -53
  30. package/src/JBTokens.sol +5 -1
  31. package/src/interfaces/IJBController.sol +7 -1
  32. package/src/libraries/JBPayoutSplitGroupLib.sol +1 -1
  33. package/src/periphery/JBDeadline1Day.sol +1 -1
  34. package/src/periphery/JBDeadline3Days.sol +1 -1
  35. package/src/periphery/JBDeadline3Hours.sol +1 -1
  36. package/src/periphery/JBDeadline7Days.sol +1 -1
  37. package/src/periphery/JBMatchingPriceFeed.sol +1 -1
  38. package/test/TestAccessToFunds.sol +4 -4
  39. package/test/TestFeeFreeCashOutBypass.sol +332 -0
  40. package/test/TestJBERC20Inheritance.sol +1 -1
  41. package/test/TestMetadataOffsetOverflow.sol +1 -1
  42. package/test/TestMetadataParserLib.sol +1 -1
  43. package/test/TestMultiTerminalSurplus.sol +1 -1
  44. package/test/TestMultiTokenSurplus.sol +1 -1
  45. package/test/TestMultipleAccessLimits.sol +4 -4
  46. package/test/TestPermit2DataHook.t.sol +1 -1
  47. package/test/TestPermit2Terminal.sol +1 -1
  48. package/test/TestTerminalPreviewParity.sol +1 -1
  49. package/test/audit/CashOutReenterPay.t.sol +496 -0
  50. package/test/audit/FeeFreeSurplusLifecycle.t.sol +392 -0
  51. package/test/audit/FeeFreeSurplusStale.t.sol +242 -0
  52. package/test/audit/USDTVoidReturnCompat.t.sol +519 -0
  53. package/test/fork/TestChainlinkPriceFeedFork.sol +1 -1
  54. package/test/fork/TestSequencerPriceFeedFork.sol +1 -1
  55. package/test/fork/TestTerminalPreviewParityFork.sol +1 -1
  56. package/test/helpers/JBTest.sol +1 -1
  57. package/test/helpers/MetadataResolverHelper.sol +1 -1
  58. package/test/mock/MockERC20.sol +1 -1
  59. package/test/mock/MockMaliciousBeneficiary.sol +1 -1
  60. package/test/mock/MockMaliciousSplitHook.sol +1 -1
  61. package/test/mock/MockPriceFeed.sol +1 -1
  62. package/test/mock/MockUSDT.sol +80 -0
  63. package/test/regression/HoldFeesCashOutReserved.t.sol +2 -2
  64. package/test/units/static/JBChainlinkV3PriceFeed/TestPriceFeed.sol +1 -1
  65. package/test/units/static/JBController/JBControllerSetup.sol +1 -1
  66. package/test/units/static/JBController/TestBurnTokensOf.sol +1 -1
  67. package/test/units/static/JBController/TestClaimTokensFor.sol +1 -1
  68. package/test/units/static/JBController/TestDeployErc20For.sol +1 -1
  69. package/test/units/static/JBController/TestLaunchProjectFor.sol +1 -1
  70. package/test/units/static/JBController/TestLaunchRulesetsFor.sol +1 -1
  71. package/test/units/static/JBController/TestMigrateController.sol +1 -1
  72. package/test/units/static/JBController/TestMintTokensOfUnits.sol +1 -1
  73. package/test/units/static/JBController/TestOmnichainRulesetOperator.sol +324 -0
  74. package/test/units/static/JBController/TestPayReservedTokenToTerminal.sol +1 -1
  75. package/test/units/static/JBController/TestPreviewMintOf.sol +1 -1
  76. package/test/units/static/JBController/TestReceiveMigrationFrom.sol +1 -1
  77. package/test/units/static/JBController/TestRulesetViews.sol +1 -1
  78. package/test/units/static/JBController/TestSendReservedTokensToSplitsOf.sol +1 -1
  79. package/test/units/static/JBController/TestSetSplitGroupsOf.sol +1 -1
  80. package/test/units/static/JBController/TestSetTokenFor.sol +1 -1
  81. package/test/units/static/JBController/TestSetUriOf.sol +1 -1
  82. package/test/units/static/JBController/TestTransferCreditsFrom.sol +1 -1
  83. package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +1 -1
  84. package/test/units/static/JBDirectory/JBDirectorySetup.sol +1 -1
  85. package/test/units/static/JBDirectory/TestPrimaryTerminalOf.sol +1 -1
  86. package/test/units/static/JBDirectory/TestSetControllerOf.sol +1 -1
  87. package/test/units/static/JBDirectory/TestSetControllerOfMigrationOrder.sol +1 -1
  88. package/test/units/static/JBDirectory/TestSetPrimaryTerminalOf.sol +1 -1
  89. package/test/units/static/JBDirectory/TestSetTerminalsOf.sol +1 -1
  90. package/test/units/static/JBERC20/JBERC20Setup.sol +11 -4
  91. package/test/units/static/JBERC20/SigUtils.sol +1 -1
  92. package/test/units/static/JBERC20/TestInitialize.sol +8 -1
  93. package/test/units/static/JBERC20/TestName.sol +1 -1
  94. package/test/units/static/JBERC20/TestNonces.sol +1 -1
  95. package/test/units/static/JBERC20/TestSymbol.sol +1 -1
  96. package/test/units/static/JBFeelessAdresses/JBFeelessSetup.sol +1 -1
  97. package/test/units/static/JBFeelessAdresses/TestInterfaces.sol +1 -1
  98. package/test/units/static/JBFeelessAdresses/TestSetFeelessAddress.sol +1 -1
  99. package/test/units/static/JBFees/TestFeesFuzz.sol +1 -1
  100. package/test/units/static/JBFixedPointNumber/TestAdjustDecimals.sol +1 -1
  101. package/test/units/static/JBFixedPointNumber/TestAdjustDecimalsFuzz.sol +1 -1
  102. package/test/units/static/JBFundAccessLimits/JBFundAccessSetup.sol +1 -1
  103. package/test/units/static/JBFundAccessLimits/TestFundAccessLimitsEdge.sol +1 -1
  104. package/test/units/static/JBFundAccessLimits/TestPayoutLimitOf.sol +1 -1
  105. package/test/units/static/JBFundAccessLimits/TestPayoutLimitsOf.sol +1 -1
  106. package/test/units/static/JBFundAccessLimits/TestSetFundAccessLimitsFor.sol +1 -1
  107. package/test/units/static/JBFundAccessLimits/TestSurplusAllowanceOf.sol +1 -1
  108. package/test/units/static/JBFundAccessLimits/TestSurplusAllowancesOf.sol +1 -1
  109. package/test/units/static/JBMetadataResolver/TestGetDataFor.sol +1 -1
  110. package/test/units/static/JBMetadataResolver/TestMetadataResolverEdgeCases.sol +1 -1
  111. package/test/units/static/JBMetadataResolver/TestMetadataResolverFuzz.sol +1 -1
  112. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +1 -1
  113. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +1 -1
  114. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +1 -1
  115. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +1 -1
  116. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +1 -1
  117. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +1 -1
  118. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +1 -1
  119. package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +1 -1
  120. package/test/units/static/JBMultiTerminal/TestPay.sol +1 -1
  121. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +1 -1
  122. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +1 -1
  123. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +1 -1
  124. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +1 -1
  125. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +1 -1
  126. package/test/units/static/JBPermissions/JBPermissionsSetup.sol +1 -1
  127. package/test/units/static/JBPermissions/TestHasPermission.sol +1 -1
  128. package/test/units/static/JBPermissions/TestHasPermissions.sol +1 -1
  129. package/test/units/static/JBPermissions/TestSetPermissionsFor.sol +1 -1
  130. package/test/units/static/JBPrices/JBPricesSetup.sol +1 -1
  131. package/test/units/static/JBPrices/TestAddPriceFeedFor.sol +1 -1
  132. package/test/units/static/JBPrices/TestPricePerUnitOf.sol +1 -1
  133. package/test/units/static/JBPrices/TestPrices.sol +1 -1
  134. package/test/units/static/JBProjects/JBProjectsSetup.sol +1 -1
  135. package/test/units/static/JBProjects/TestCreateFor.sol +1 -1
  136. package/test/units/static/JBProjects/TestInitialProject.sol +1 -1
  137. package/test/units/static/JBProjects/TestInterfaces.sol +1 -1
  138. package/test/units/static/JBProjects/TestSetResolver.sol +1 -1
  139. package/test/units/static/JBProjects/TestTokenUri.sol +1 -1
  140. package/test/units/static/JBRulesetMetadataResolver/TestSetCashOutTaxRateTo.sol +1 -1
  141. package/test/units/static/JBRulesets/JBRulesetsSetup.sol +1 -1
  142. package/test/units/static/JBRulesets/TestCurrentApprovalStatusForLatestRulesetOf.sol +1 -1
  143. package/test/units/static/JBRulesets/TestCurrentOf.sol +1 -1
  144. package/test/units/static/JBRulesets/TestGetRulesetOf.sol +1 -1
  145. package/test/units/static/JBRulesets/TestLatestQueuedRulesetOf.sol +1 -1
  146. package/test/units/static/JBRulesets/TestRulesets.sol +1 -1
  147. package/test/units/static/JBRulesets/TestRulesetsOf.sol +1 -1
  148. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +1 -1
  149. package/test/units/static/JBRulesets/TestUpdateRulesetWeightCache.sol +1 -1
  150. package/test/units/static/JBSplits/JBSplitsSetup.sol +1 -1
  151. package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +1 -1
  152. package/test/units/static/JBSplits/TestSetSplitGroupsOf.sol +1 -1
  153. package/test/units/static/JBSplits/TestSplitsLockedEdge.sol +1 -1
  154. package/test/units/static/JBSplits/TestSplitsOf.sol +1 -1
  155. package/test/units/static/JBSplits/TestSplitsPacking.sol +1 -1
  156. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +1 -1
  157. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +1 -1
  158. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +1 -1
  159. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +1 -1
  160. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +1 -1
  161. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +1 -1
  162. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +1 -1
  163. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +1 -1
  164. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +1 -1
  165. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +1 -1
  166. package/test/units/static/JBTerminalStore/TestRecordTerminalMigration.sol +1 -1
  167. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +1 -1
  168. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +1 -1
  169. package/test/units/static/JBTokens/JBTokensSetup.sol +1 -1
  170. package/test/units/static/JBTokens/TestBurnFrom.sol +1 -1
  171. package/test/units/static/JBTokens/TestClaimTokensFor.sol +1 -1
  172. package/test/units/static/JBTokens/TestDeployERC20ForUnits.sol +1 -1
  173. package/test/units/static/JBTokens/TestMintFor.sol +1 -1
  174. package/test/units/static/JBTokens/TestSetTokenFor.sol +1 -1
  175. package/test/units/static/JBTokens/TestTotalBalanceOf.sol +1 -1
  176. package/test/units/static/JBTokens/TestTotalSupplyOf.sol +1 -1
  177. package/test/units/static/JBTokens/TestTransferCreditsFrom.sol +1 -1
package/ADMINISTRATION.md CHANGED
@@ -97,7 +97,7 @@ Admin privileges and their scope in nana-core-v6.
97
97
 
98
98
  | Function | Required Role | Permission ID | Scope | What It Does |
99
99
  |----------|--------------|---------------|-------|-------------|
100
- | `addPriceFeed` | Project owner or operator | ADD_PRICE_FEED (19) | Per project | Adds a price feed for a project. Requires the ruleset's `allowAddPriceFeed` flag. Price feeds are immutable once set. |
100
+ | `addPriceFeedFor` | Project owner or operator | ADD_PRICE_FEED (19) | Per project | Adds a price feed for a project. Requires the ruleset's `allowAddPriceFeed` flag. Price feeds are immutable once set. |
101
101
  | `burnTokensOf` | Token holder, operator with BURN_TOKENS, or a project terminal | BURN_TOKENS (11) | Per project | Burns tokens or credits from a holder's balance. Terminals can burn without explicit permission (for cash outs). |
102
102
  | `claimTokensFor` | Credit holder or operator | CLAIM_TOKENS (12) | Per project | Redeems internal credits for ERC-20 tokens. |
103
103
  | `deployERC20For` | Project owner or operator | DEPLOY_ERC20 (8) | Per project | Deploys a new ERC-20 token contract for the project. Can only be called once per project. |
@@ -200,6 +200,19 @@ JBTerminalStore has no explicit access control modifiers. Instead, it uses `msg.
200
200
  | `recordTerminalMigration` | Any address (terminal) | Records a full balance migration. Requires the ruleset's `allowTerminalMigration` flag. Zeros out the caller's balance. |
201
201
  | `recordUsedAllowanceOf` | Any address (terminal) | Records surplus allowance usage. Decrements the caller's balance. |
202
202
 
203
+ ## Deployment Dependencies
204
+
205
+ Contracts must be deployed in dependency order. The constructor dependency graph:
206
+
207
+ 1. **No dependencies:** JBPermissions, JBProjects, JBERC20 (implementation), JBFeelessAddresses
208
+ 2. **Depends on (1):** JBDirectory (← JBPermissions, JBProjects)
209
+ 3. **Depends on (2):** JBRulesets, JBSplits, JBFundAccessLimits, JBTokens (← JBDirectory); JBPrices (← JBDirectory, JBPermissions, JBProjects)
210
+ 4. **Depends on (3):** JBTerminalStore (← JBDirectory, JBPrices, JBRulesets)
211
+ 5. **Depends on (1-4):** JBController (← JBDirectory, JBFundAccessLimits, JBPermissions, JBPrices, JBProjects, JBRulesets, JBSplits, JBTokens + omnichainRulesetOperator address)
212
+ 6. **Depends on (1-4):** JBMultiTerminal (← JBTerminalStore, JBFeelessAddresses, JBPermissions, JBProjects, JBSplits, JBTokens, Permit2)
213
+
214
+ After deployment, `JBDirectory.setIsAllowedToSetFirstController()` must be called to authorize the controller. The first project (ID 1, the fee project) is created automatically in the `JBProjects` constructor.
215
+
203
216
  ## Permission System
204
217
 
205
218
  JBPermissions implements a 256-bit packed permission bitmap system:
@@ -232,7 +245,7 @@ JBPermissions implements a 256-bit packed permission bitmap system:
232
245
  | 16 | SET_PRIMARY_TERMINAL | `JBDirectory.setPrimaryTerminalOf` |
233
246
  | 17 | USE_ALLOWANCE | `JBMultiTerminal.useAllowanceOf` |
234
247
  | 18 | SET_SPLIT_GROUPS | `JBController.setSplitGroupsOf` |
235
- | 19 | ADD_PRICE_FEED | `JBController.addPriceFeed` |
248
+ | 19 | ADD_PRICE_FEED | `JBController.addPriceFeedFor` |
236
249
  | 20 | ADD_ACCOUNTING_CONTEXTS | `JBMultiTerminal.addAccountingContextsFor` |
237
250
  | 21 | SET_TOKEN_METADATA | `JBController.setTokenMetadataOf` |
238
251
 
@@ -308,7 +321,7 @@ Several admin functions are further gated by boolean flags in the active ruleset
308
321
  | `allowSetTerminals` | Whether `setTerminalsOf` can be called by non-controller callers. |
309
322
  | `allowSetController` | Whether `setControllerOf` can be called (checked via `IJBDirectoryAccessControl`). |
310
323
  | `allowAddAccountingContext` | Whether `addAccountingContextsFor` can add new token contexts. |
311
- | `allowAddPriceFeed` | Whether `addPriceFeed` can add new price feeds. |
324
+ | `allowAddPriceFeed` | Whether `addPriceFeedFor` can add new price feeds. |
312
325
  | `ownerMustSendPayouts` | Whether `sendPayoutsOf` requires SEND_PAYOUTS permission (otherwise anyone can call it). |
313
326
  | `pausePay` | Whether payments are paused (checked in JBTerminalStore). |
314
327
  | `pauseCreditTransfers` | Whether credit transfers are paused. |
package/ARCHITECTURE.md CHANGED
@@ -27,14 +27,27 @@ src/
27
27
  ├── abstract/
28
28
  │ ├── JBPermissioned.sol — Base for permission-checked contracts
29
29
  │ └── JBControlled.sol — Base for controller-gated contracts
30
+ ├── enums/
31
+ │ └── JBApprovalStatus.sol — Approval hook status enum
32
+ ├── periphery/
33
+ │ ├── JBDeadline1Day.sol — 1-day approval hook
34
+ │ ├── JBDeadline3Days.sol — 3-day approval hook
35
+ │ ├── JBDeadline3Hours.sol — 3-hour approval hook
36
+ │ ├── JBDeadline7Days.sol — 7-day approval hook
37
+ │ └── JBMatchingPriceFeed.sol — 1:1 price feed
38
+ ├── interfaces/ — 30 interface files (IJBController, IJBTerminal, etc.)
39
+ ├── structs/ — 22 struct files (JBRuleset, JBSplit, etc.)
30
40
  └── libraries/
31
41
  ├── JBCashOuts.sol — Bonding curve math
42
+ ├── JBConstants.sol — Protocol constants
43
+ ├── JBCurrencyIds.sol — Currency ID constants (ETH, USD)
32
44
  ├── JBFees.sol — Fee calculation (forward/backward)
33
- ├── JBRulesetMetadataResolver.sol — Bit-packed metadata (256 bits)
34
- ├── JBMetadataResolver.sol — Variable-length key-value metadata
35
45
  ├── JBFixedPointNumber.sol — Decimal adjustment
36
- ├── JBConstants.sol Protocol constants
37
- └── JBSplitGroupIds.sol Split group ID constants
46
+ ├── JBMetadataResolver.sol Variable-length key-value metadata
47
+ ├── JBPayoutSplitGroupLib.sol Payout split group helpers
48
+ ├── JBRulesetMetadataResolver.sol — Bit-packed metadata (256 bits)
49
+ ├── JBSplitGroupIds.sol — Split group ID constants
50
+ └── JBSurplus.sol — Cross-terminal surplus calculation
38
51
  ```
39
52
 
40
53
  ## Key Data Flows
@@ -66,10 +79,16 @@ Holder -> JBMultiTerminal.cashOutTokensOf()
66
79
  -> JBCashOuts.cashOutFrom() — bonding curve
67
80
  -> Deduct balance
68
81
  -> JBController.burnTokensOf()
69
- -> Transfer reclaimed tokens to beneficiary
82
+ -> Deduct fee from reclaim amount (if applicable), transfer remainder to beneficiary
70
83
  -> [Optional] Cash out hooks execute
71
- -> Take fees (2.5% to project #1) if cashOutTaxRate > 0
72
- OR if cashOutTaxRate == 0 and project has unconsumed fee-free surplus (_feeFreeSurplusOf)
84
+ -> Send accumulated fees to project #1
85
+ Fees apply when cashOutTaxRate > 0 (on full reclaim)
86
+ OR when cashOutTaxRate == 0 and project has unconsumed _feeFreeSurplusOf (on that portion only)
87
+ _feeFreeSurplusOf lifecycle:
88
+ - Incremented on fee-free intra-terminal payouts
89
+ - Capped at remaining balance after any outflow (payouts, useAllowanceOf, non-zero-tax/feeless cashouts) — non-fee-free funds leave first
90
+ - Consumed (decremented by feeable amount) during zero-tax cashouts
91
+ - Cleared to zero on terminal migration (migrateBalanceOf)
73
92
  ```
74
93
 
75
94
  ### Preview Flow
@@ -117,7 +136,7 @@ Owner -> JBMultiTerminal.sendPayoutsOf()
117
136
  | Data Hook (cashout) | `IJBRulesetDataHook.beforeCashOutRecordedWith` | JBTerminalStore |
118
137
  | Pay Hook | `IJBPayHook.afterPayRecordedWith` | JBMultiTerminal |
119
138
  | Cash Out Hook | `IJBCashOutHook.afterCashOutRecordedWith` | JBMultiTerminal |
120
- | Split Hook | `IJBSplitHook.processSplitWith` | JBMultiTerminal |
139
+ | Split Hook | `IJBSplitHook.processSplitWith` | JBMultiTerminal, JBController |
121
140
  | Approval Hook | `IJBRulesetApprovalHook.approvalStatusOf` | JBRulesets |
122
141
 
123
142
  ## Dependencies
@@ -130,7 +149,7 @@ Owner -> JBMultiTerminal.sendPayoutsOf()
130
149
 
131
150
  ## Key Constants
132
151
 
133
- - FEE = 25 (2.5%), MAX_FEE = 1000
152
+ - FEE = 25 (2.5%) — defined on JBMultiTerminal, MAX_FEE = 1000 — defined in JBConstants
134
153
  - MAX_RESERVED_PERCENT = 10,000 (basis points)
135
154
  - MAX_CASH_OUT_TAX_RATE = 10,000
136
155
  - MAX_WEIGHT_CUT_PERCENT = 1,000,000,000 (9 decimals)
@@ -6,7 +6,7 @@ Read [RISKS.md](./RISKS.md) for known risks, trust model, and reentrancy analysi
6
6
 
7
7
  ## Architecture Overview
8
8
 
9
- 16 contracts, ~6,300 lines in main contracts. All contracts use Solidity 0.8.26.
9
+ 16 contracts, ~8,100 lines in main contracts. All contracts use Solidity 0.8.28.
10
10
 
11
11
  ```
12
12
  JBProjects (ERC-721)
@@ -27,21 +27,21 @@ Read [RISKS.md](./RISKS.md) for known risks, trust model, and reentrancy analysi
27
27
  | Contract | Lines | Role | Calls |
28
28
  |----------|-------|------|-------|
29
29
  | **JBMultiTerminal** | ~2024 | Payment terminal. Handles pay, cash out, payouts, surplus allowance, fees, and previews (`previewPayFor`, `previewCashOutFrom`). Multi-token. Permit2 integration. | Store, Controller, Splits, Directory, Prices |
30
- | **JBController** | ~1186 | Orchestrator. Project lifecycle, ruleset queuing, token minting/burning, reserved token distribution, mint preview (`previewMintOf`). ERC-2771 meta-tx. | Rulesets, Tokens, Splits, FundAccessLimits, Directory, Prices |
31
- | **JBTerminalStore** | ~800 | Bookkeeping. Balances, payout limit tracking, surplus calculation, bonding curve reclaim math. Data hook integration point. | Rulesets, Prices, Directory |
30
+ | **JBController** | ~1,253 | Orchestrator. Project lifecycle, ruleset queuing, token minting/burning, reserved token distribution, mint preview (`previewMintOf`). ERC-2771 meta-tx. | Rulesets, Tokens, Splits, FundAccessLimits, Directory, Prices |
31
+ | **JBTerminalStore** | ~1,267 | Bookkeeping. Balances, payout limit tracking, surplus calculation, bonding curve reclaim math. Data hook integration point. | Rulesets, Prices, Directory |
32
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 |
33
+ | **JBDirectory** | ~344 | Routes projects to terminals and controllers. Migration lifecycle (before/after). | Projects, Permissions |
34
+ | **JBTokens** | ~415 | Dual token system: credits (internal) + ERC-20. Credits burned first on burn. 18-decimal requirement. | JBERC20 (clone) |
35
+ | **JBSplits** | ~333 | Packed split storage per project/ruleset/group. Locked splits enforcement. Fallback to ruleset 0. | -- |
36
+ | **JBFundAccessLimits** | ~318 | Payout limits and surplus allowances per terminal/token/currency. Strictly increasing currency order. | -- |
37
+ | **JBPrices** | ~233 | Price feed registry. Project-specific + default fallback. Immutable once set. Inverse auto-calculation. | Chainlink feeds |
38
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()`. | -- |
39
+ | **JBProjects** | ~126 | ERC-721 project ownership. Auto-incrementing IDs. | -- |
40
+ | **JBERC20** | ~144 | Cloneable ERC20Votes+Permit. Owned by JBTokens. Deployed via `Clones.clone()`. | -- |
41
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 |
42
+ | **JBDeadline** | ~76 | Approval hook. Rejects rulesets queued within DURATION seconds of start. Ships as 3h, 1d, 3d, 7d variants. | -- |
43
+ | **JBChainlinkV3PriceFeed** | ~74 | Chainlink v3 feed with staleness threshold. Rejects negative/zero/incomplete. | Chainlink AggregatorV3 |
44
+ | **JBChainlinkV3SequencerPriceFeed** | ~75 | L2 sequencer-aware Chainlink feed. Grace period after restart. | Chainlink AggregatorV3 + Sequencer feed |
45
45
 
46
46
  ## Key Flows
47
47
 
@@ -88,7 +88,7 @@ Holder -> JBMultiTerminal.cashOutTokensOf()
88
88
  -> Fee skipped if beneficiary is feeless
89
89
  ```
90
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.
91
+ **Critical note**: The beneficiary receives the reclaim amount BEFORE cash out hooks execute. Fees are taken AFTER hooks. `_feeFreeSurplusOf[projectId][token]` accumulates the value of fee-free intra-terminal payouts. After any outflow (payouts, `useAllowanceOf`, non-zero-tax or feeless cashouts), the counter is capped at the remaining balance — non-fee-free funds leave first. During zero-tax cashout, the 2.5% fee applies only up to this surplus amount (then depletes it), preventing a round-trip fee bypass. Cleared on terminal migration.
92
92
 
93
93
  ### Payout Flow (`sendPayoutsOf`)
94
94
 
@@ -99,7 +99,7 @@ Anyone -> JBMultiTerminal.sendPayoutsOf()
99
99
  -> Deduct amount from balance
100
100
  -> Increment usedPayoutLimitOf
101
101
  -> Validate against FUND_ACCESS_LIMITS.payoutLimitOf()
102
- -> _sendPayoutsToSplitGroupOf()
102
+ -> JBPayoutSplitGroupLib.sendPayoutsToSplitGroupOf()
103
103
  -> Get splits from JBSplits.splitsOf()
104
104
  -> For each split (try-catch per split):
105
105
  -> Split to hook: deduct fee, call hook.processSplitWith() with funds
@@ -258,7 +258,7 @@ These are the patterns that will trip you up if you are not aware of them:
258
258
  8. **`sendPayoutsOf()` reverts when `amount > payout limit`** -- does NOT auto-cap to limit.
259
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
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.
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
262
  12. **Credits are burned before ERC-20 tokens** in `JBTokens.burnFrom()`.
263
263
  13. **`JBERC20` is cloned via `Clones.clone()`** -- constructor sets invalid name/symbol; real values set in `initialize()`.
264
264
  14. **Named returns auto-return** -- several functions use named return variables without explicit `return` statements.
@@ -301,7 +301,7 @@ forge test --match-contract Invariant
301
301
  forge test --gas-report
302
302
  ```
303
303
 
304
- The existing test suite has 165 test files including:
304
+ The existing test suite has 185 test files including:
305
305
  - **Integration tests**: Full flow tests for pay, cash out, payouts
306
306
  - **Formal property tests**: 7 bonding curve properties + 6 fee properties
307
307
  - **Invariant tests**: TerminalStore (5), Phase3Deep (8), Rulesets (4), Tokens (4)
@@ -341,4 +341,89 @@ For each finding:
341
341
  - **MEDIUM**: Value leakage, griefing with cost to attacker, incorrect accounting, degraded functionality.
342
342
  - **LOW**: Informational, cosmetic inconsistency, edge-case-only with no material impact.
343
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
360
+
361
+ The cash out formula: `reclaimAmount = (surplus * count / supply) * [(MAX - tax) + tax * (count / supply)] / MAX`
362
+
363
+ **Sequences:**
364
+ - Pay → immediate cash out in same block: should never profit after fees. Test with different hook configurations.
365
+ - Pay with data hook that inflates weight → cash out before reserved tokens are distributed. Pending reserved tokens inflate `totalSupply` (H-4 finding).
366
+ - Cash out with `cashOutCount >= totalSupply` returns entire surplus (C-5 finding, by design). Can you engineer this condition without being the last holder? (e.g., front-running a burn)
367
+ - Cross-terminal surplus aggregation: `useTotalSurplusForCashOuts` aggregates surplus across all terminals via `JBSurplus`. Can you manipulate surplus in one terminal to inflate cashout value in another?
368
+
369
+ ### 3. Reentrancy Through CEI Ordering
370
+
371
+ No contract uses `ReentrancyGuard`. The protocol relies on checks-effects-interactions ordering.
372
+
373
+ **Critical surfaces (in order of risk):**
374
+ 1. **Pay hook → cash out**: After `recordPaymentFrom` + `mintTokensOf`, the hook executes. It could call `cashOutTokensOf`. Store balance and tokens are updated. Is the resulting cashout computed against post-payment state correctly?
375
+ 2. **Split hook → pay**: During `sendPayoutsOf`, splits receive funds. A hook could call `pay()`. Payout limit is consumed, but new payment updates state.
376
+ 3. **Fee processing → re-entry**: `_processFee` calls `terminal.pay()` on project #1's terminal. If project #1 has a pay hook that calls back into the originating terminal, the fee is already deducted.
377
+ 4. **processHeldFeesOf**: Re-reads storage index each iteration, updates index BEFORE external call. Verify this ordering prevents re-entrancy exploitation.
378
+
379
+ ### 4. Ruleset Transition Timing
380
+
381
+ Rulesets transition at exact block timestamps. Transaction ordering at boundaries matters.
382
+
383
+ **What to test:**
384
+ - Payment at last second of ruleset vs. first second of next: both should execute with correct weights.
385
+ - Approval hook rejection at boundary: fallback to `basedOnId` chain simulates cycling from last approved ruleset. Is this always equivalent?
386
+ - `duration = 0` rulesets immediately replaced when new one queued. Can you pay and queue in the same tx to get old weight but new parameters?
387
+ - Weight decay across 20,000+ cycles without cache: `WeightCacheRequired` revert = DoS. Can an attacker force a project into this state?
388
+
389
+ ## Anti-Patterns to Hunt
390
+
391
+ | Pattern | Where to Look | Why It's Dangerous |
392
+ |---------|--------------|-------------------|
393
+ | `try-catch` swallowing errors | JBMultiTerminal (hooks, fees, splits) | Failed external calls silently change control flow. Fee try-catch enables temporary fee avoidance. |
394
+ | `mulDiv` rounding direction | JBCashOuts, JBFees, JBTerminalStore | Rounding in attacker's favor compounds over many transactions. Verify rounding favors the protocol. |
395
+ | Currency type confusion | JBTerminalStore, JBFundAccessLimits | Abstract (1=ETH, 2=USD) vs concrete (`uint32(address)`) currencies. `groupId` (`uint256`) vs `currency` (`uint32`) truncation. |
396
+ | Named returns without explicit `return` | JBTerminalStore, JBRulesets | Auto-return of named variables. Easy to miss intermediate assignments that change the return value. |
397
+ | Bit-packed metadata shifts | JBRulesetMetadataResolver | Off-by-one in shift amounts or mask widths corrupts adjacent fields. 256-bit layout with 17 fields. |
398
+ | State before validation | `recordPayoutFor` | Balance deducted and limit incremented BEFORE validation. Safe due to atomic revert, but matters for reentrancy analysis. |
399
+ | Lazy evaluation of pending state | `pendingReservedTokenBalanceOf` | Reserved tokens accumulate but aren't minted until `sendReservedTokensToSplitsOf`. `totalSupply` includes pending. |
400
+ | External call in loop | Payout splits, `processHeldFeesOf` | Gas griefing via reverting external calls. Each caught by try-catch but still costs gas. |
401
+
402
+ ## Previous Audit Findings
403
+
404
+ | ID | Severity | Status | Description |
405
+ |----|----------|--------|-------------|
406
+ | C-5 | Critical | Known/Accepted | `cashOut(0)` with `totalSupply == 0` returns entire surplus. By design — last holder gets everything. Documented in `FlashLoanAttacks.t.sol`. |
407
+ | H-4 | High | Known/Accepted | Pending reserved tokens inflate `totalSupply` in bonding curve calculation, reducing cashout value by 50%+ in extreme cases. Distributing reserved tokens via `sendReservedTokensToSplitsOf` before cashing out mitigates. |
408
+ | FV-1 | Low | Known/Accepted | Bonding curve subadditivity violation from `mulDiv` rounding. Measured at <0.01%, economically insignificant. Proven bounded in formal property tests. |
409
+
410
+ ## Coverage Gaps
411
+
412
+ The 185 test files cover most flows, but these areas have limited or no coverage:
413
+
414
+ - **Multi-hook composition**: No end-to-end tests for data hook + pay hook + cashout hook interacting in a single flow with reentrancy.
415
+ - **Extreme weight decay**: Weight decay beyond 20,000 cycles tested for revert, but not for precision loss at exactly the cache threshold boundary.
416
+ - **Cross-terminal surplus manipulation**: No tests where surplus is manipulated in terminal A to inflate cashout value in terminal B via `useTotalSurplusForCashOuts`.
417
+ - **Approval hook edge cases**: Limited testing of approval hook state changes between `queueRulesetsOf` and ruleset start time.
418
+ - **Concurrent held fee processing**: `processHeldFeesOf` tested sequentially but not under reentrancy from fee payment hooks.
419
+ - **ERC-2771 meta-tx spoofing**: No tests verifying that a malicious forwarder cannot spoof `_msgSender()` to bypass permissions.
420
+
421
+ ## Compiler and Version Info
422
+
423
+ - **Solidity**: 0.8.28
424
+ - **EVM target**: Cancun (uses transient storage opcodes)
425
+ - **Optimizer**: 200 runs (no via-IR)
426
+ - **Dependencies**: OpenZeppelin 5.x, Solady, forge-std
427
+ - **Build**: `forge build` (Foundry)
428
+
344
429
  Go break it.
package/CHANGE_LOG.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # nana-core-v6 Changelog (v5 -> v6)
2
2
 
3
- This document describes all changes between `nana-core` (v5, Solidity 0.8.23) and `nana-core-v6` (v6, Solidity 0.8.26).
3
+ This document describes all changes between `nana-core` (v5, Solidity 0.8.23) and `nana-core-v6` (v6, Solidity 0.8.28).
4
+
5
+ ## Summary
6
+
7
+ - **Preview APIs**: New `previewPayFor` and `previewCashOutFrom` view functions on terminal and store for simulating payments/cashouts without state changes.
8
+ - **Fee-free cashout bypass closed**: Intra-terminal payouts now tracked via `_feeFreeSurplusOf` to prevent round-trip fee evasion through zero-tax cashouts.
9
+ - **Approval hook hardening**: Reverting approval hooks now return `Failed` status instead of propagating, preventing permanent project freezing.
10
+ - **Weight cache overhaul**: Cache threshold raised from 1,000 to 20,000 iterations; exceeding the threshold now reverts instead of silently iterating.
11
+ - **Token metadata now mutable**: New `setTokenMetadataOf` allows changing a project token's name and symbol post-deployment.
4
12
 
5
13
  ---
6
14
 
@@ -12,7 +20,11 @@ This document describes all changes between `nana-core` (v5, Solidity 0.8.23) an
12
20
 
13
21
  ### 0.2 JBMultiTerminal -- Fee-Free Cashout Bypass Prevention
14
22
 
15
- A new `_feeFreeSurplusOf` mapping (`projectId => token => uint256`) tracks cumulative fee-free intra-terminal payouts received by each project. When a split payout lands on the same terminal (intra-terminal routing, i.e. `terminal == this`), the net payout amount is added to `_feeFreeSurplusOf[projectId][token]`. During a cashout with `cashOutTaxRate == 0`, fees are now charged on the reclaim amount up to the tracked fee-free surplus (and the tracker is decremented accordingly). Cashouts beyond the fee-free surplus remain fee-free. This closes a round-trip fee bypass where funds could be routed fee-free into a project via an intra-terminal split payout and then cashed out fee-free via a zero-tax cashout.
23
+ A new `_feeFreeSurplusOf` mapping (`projectId => token => uint256`) tracks cumulative fee-free intra-terminal payouts received by each project. When a split payout lands on the same terminal (intra-terminal routing, i.e. `terminal == this`), the net payout amount is added to `_feeFreeSurplusOf[projectId][token]`. After any outflow (payouts via `sendPayoutsOf`, surplus allowance via `useAllowanceOf`, non-zero-tax or feeless cashouts), the counter is capped at the remaining balance — non-fee-free funds are considered to leave first, preserving the fee-free counter as long as possible. During a cashout with `cashOutTaxRate == 0`, fees are charged on the reclaim amount up to the tracked fee-free surplus (and the tracker is decremented accordingly). Cashouts beyond the fee-free surplus remain fee-free. The counter is cleared on terminal migration. This closes a round-trip fee bypass where funds could be routed fee-free into a project via an intra-terminal split payout and then cashed out fee-free via a zero-tax cashout.
24
+
25
+ ### 0.3 JBBeforeCashOutRecordedContext -- beneficiaryIsFeeless Field
26
+
27
+ A `bool beneficiaryIsFeeless` field was added to the `JBBeforeCashOutRecordedContext` struct (before the `metadata` field). `recordCashOutFor` in `IJBTerminalStore` gained a corresponding `bool beneficiaryIsFeeless` parameter. The terminal passes the result of its feeless address check, allowing data hooks to skip their own fees when the beneficiary is already feeless (e.g., project-to-project routing via the router terminal). This is a **breaking change** to both the struct layout and the `recordCashOutFor` function signature.
16
28
 
17
29
  ### 0.4 JBTerminalStore -- Preview Functions
18
30
 
@@ -36,10 +48,6 @@ Also in this release:
36
48
  - **`via_ir = true`**: Added to `foundry.toml` to enable the Solidity IR optimizer pipeline, reducing deployed bytecode size (EIP-170 compliance).
37
49
  - Internal helpers extracted: `_accountingContextOf` and `_tokenAmountOf` on `JBMultiTerminal`, `_splitTokenCount` on `JBController`.
38
50
 
39
- ### 0.3 JBBeforeCashOutRecordedContext -- beneficiaryIsFeeless Field
40
-
41
- A `bool beneficiaryIsFeeless` field was added to the `JBBeforeCashOutRecordedContext` struct (before the `metadata` field). `recordCashOutFor` in `IJBTerminalStore` gained a corresponding `bool beneficiaryIsFeeless` parameter. The terminal passes the result of its feeless address check, allowing data hooks to skip their own fees when the beneficiary is already feeless (e.g., project-to-project routing via the router terminal). This is a **breaking change** to both the struct layout and the `recordCashOutFor` function signature.
42
-
43
51
  ---
44
52
 
45
53
  ## 1. Breaking Changes
@@ -81,6 +89,7 @@ The return variable name was corrected from `netLeftoverPayoutAmount` to `amount
81
89
  | `launchRulesetsFor` terminal configs | `JBTerminalConfig[] memory terminalConfigurations` | `JBTerminalConfig[] calldata terminalConfigurations` |
82
90
 
83
91
  Parameters changed from `memory` to `calldata` for gas efficiency.
92
+ > **Cross-repo impact**: The `calldata` change affects `nana-omnichain-deployers-v6` and `revnet-core-v6`, which call `launchProjectFor`/`launchRulesetsFor`.
84
93
 
85
94
  #### IJBSplits
86
95
 
@@ -167,6 +176,7 @@ Parameters changed from `memory` to `calldata` for gas efficiency.
167
176
  |------------|-------------|
168
177
  | `LAUNCH_RULESETS` | Required for `launchRulesetsFor`. In v5, `QUEUE_RULESETS` was used. |
169
178
  | `SET_TOKEN_METADATA` | Required for `setTokenMetadataOf`. |
179
+ > **Cross-repo impact**: `nana-permission-ids-v6` defines these new IDs. `nana-omnichain-deployers-v6` and `revnet-core-v6` use `LAUNCH_RULESETS` for their deployment flows.
170
180
 
171
181
  ---
172
182
 
@@ -314,7 +324,7 @@ No changes.
314
324
  | **Migration held fees** | Migration intentionally does not transfer held fees (documented: held fees belong to fee beneficiary, not the migrating project). |
315
325
  | **Held fee processing (reentrancy hardening)** | `processHeldFeesOf` now re-reads the storage index each iteration (instead of caching), deletes the entry before the external call, and updates the index before the external call. |
316
326
  | **Split payout documentation** | Failed split payouts documented as consuming payout limit by design. |
317
- | **Fee-free cashout bypass prevention** | New `_feeFreeSurplusOf` mapping tracks cumulative fee-free intra-terminal payouts per project/token. During zero-tax cashouts, fees are charged up to this tracked amount (then decremented), preventing round-trip fee bypass. See Section 0.2. |
327
+ | **Fee-free cashout bypass prevention** | New `_feeFreeSurplusOf` mapping tracks cumulative fee-free intra-terminal payouts per project/token. Capped at remaining balance after outflows including non-zero-tax/feeless cashouts (non-fee-free funds leave first). During zero-tax cashouts, fees are charged up to this tracked amount (then decremented). Cleared on migration. See Section 0.2. |
318
328
  | **beneficiaryIsFeeless passthrough** | `cashOutTokensOf` now passes `_isFeeless(beneficiary)` to `recordCashOutFor`, which forwards it to data hooks via `JBBeforeCashOutRecordedContext.beneficiaryIsFeeless`. |
319
329
 
320
330
  ### 8.3 JBRulesets
@@ -365,7 +375,7 @@ No changes.
365
375
 
366
376
  ### 8.10 Solidity Version
367
377
 
368
- All contracts upgraded from `pragma solidity 0.8.23` to `pragma solidity 0.8.26`.
378
+ All contracts upgraded from `pragma solidity 0.8.23` to `pragma solidity 0.8.28`.
369
379
 
370
380
  ### 8.11 Named Arguments
371
381
 
package/README.md CHANGED
@@ -78,6 +78,8 @@ Hooks are customizable contracts that plug into protocol flows:
78
78
  - Surplus allowance usage.
79
79
  - Cash outs when the cash out tax rate is above 0%. When the cash out tax rate is 0%, fees apply only up to the project's accumulated fee-free intra-terminal payout surplus (`_feeFreeSurplusOf`) — once that surplus is consumed, subsequent cashouts are fee-free.
80
80
 
81
+ `_feeFreeSurplusOf` lifecycle: (a) incremented on fee-free intra-terminal payouts, (b) capped at remaining balance after any outflow (payouts, `useAllowanceOf`, non-zero-tax or feeless cashouts) — non-fee-free funds leave first, (c) consumed (decremented by feeable amount) during zero-tax cashouts, and (d) cleared to zero on terminal migration via `migrateBalanceOf`.
82
+
81
83
  Fees are paid to **project #1** (the fee beneficiary project, minted in the `JBProjects` constructor). Addresses on the `JBFeelessAddresses` allowlist are exempt from fees.
82
84
 
83
85
  When a ruleset has `holdFees` enabled, fees are held for 28 days before being processed. During this period, if funds are returned to the project via `addToBalanceOf`, held fees can be unlocked and returned.
@@ -98,7 +100,31 @@ Projects can migrate between controllers using the `IJBMigratable` interface. Th
98
100
 
99
101
  Juicebox V6 separates concerns across specialized contracts that coordinate through a central directory. Projects are represented as ERC-721 NFTs. Each project configures rulesets that dictate how payments, payouts, cash outs, and token minting behave over time.
100
102
 
101
- All contracts use Solidity `0.8.26`.
103
+ All contracts use Solidity `0.8.28`.
104
+
105
+ ```mermaid
106
+ graph TD;
107
+ User([User / Frontend])
108
+ User -->|pay / cashOut / sendPayoutsOf| Terminal[JBMultiTerminal]
109
+ User -->|launchProjectFor / queueRulesetsOf / mintTokensOf| Controller[JBController]
110
+ Controller -->|reads & writes| Rulesets[JBRulesets]
111
+ Controller -->|reads & writes| Tokens[JBTokens]
112
+ Controller -->|reads & writes| Splits[JBSplits]
113
+ Controller -->|reads & writes| FAL[JBFundAccessLimits]
114
+ Controller -->|adds feeds to| Prices[JBPrices]
115
+ Terminal -->|records inflows & outflows| Store[JBTerminalStore]
116
+ Store -->|reads rulesets from| Rulesets
117
+ Store -->|reads limits from| FAL
118
+ Store -->|reads prices from| Prices
119
+ Terminal -->|distributes payouts via| Splits
120
+ Directory[JBDirectory] -->|maps projects to| Controller
121
+ Directory -->|maps projects to| Terminal
122
+ Controller -->|registers in| Directory
123
+ Terminal -->|looks up controllers via| Directory
124
+ Projects[JBProjects] -->|ERC-721 ownership| Directory
125
+ Permissions[JBPermissions] -->|authorizes calls on| Controller
126
+ Permissions -->|authorizes calls on| Terminal
127
+ ```
102
128
 
103
129
  ### Core Contracts
104
130
 
@@ -151,6 +177,7 @@ All contracts use Solidity `0.8.26`.
151
177
  | `JBFees` | Fee calculation helpers. `feeAmountFrom` (forward) and `feeAmountResultingIn` (backward). |
152
178
  | `JBFixedPointNumber` | Decimal adjustment between fixed-point number precisions. |
153
179
  | `JBMetadataResolver` | Packs and unpacks variable-length `{id: data}` metadata entries with a lookup table. Used by pay/cash-out hooks. |
180
+ | `JBPayoutSplitGroupLib` | External library for payout split-group distribution, called via `DELEGATECALL` from `JBMultiTerminal` to reduce terminal bytecode. Distributes payouts to splits with try-catch per split. |
154
181
  | `JBRulesetMetadataResolver` | Packs and unpacks the `uint256 metadata` field on `JBRuleset` into `JBRulesetMetadata`. Bit layout: version (4 bits), reservedPercent (16), cashOutTaxRate (16), baseCurrency (32), 14 boolean flags (1 bit each), dataHook address (160), metadata (14). |
155
182
 
156
183
  ### Hook Interfaces
@@ -163,6 +190,35 @@ All contracts use Solidity `0.8.26`.
163
190
  | `IJBCashOutHook` | Called after a cash out is recorded. Implements `afterCashOutRecordedWith`. |
164
191
  | `IJBSplitHook` | Called when processing a split. Implements `processSplitWith`. |
165
192
 
193
+ ## Risks
194
+
195
+ This section summarizes known risks and design trade-offs. None of these are unmitigated vulnerabilities -- they are deliberate design decisions with well-understood boundaries.
196
+
197
+ ### Reentrancy
198
+
199
+ The protocol does not use an explicit `ReentrancyGuard`. Instead, it relies on **state ordering**: all storage writes (balance updates, token mints/burns, payout limit usage) are completed before any external calls (hooks, split payouts, fee processing). This means re-entering a function sees already-updated state, preventing double-spends and double-payouts. The `try-catch` pattern around external calls ensures that hook failures do not leave the protocol in an inconsistent state -- failed calls return funds to the project balance.
200
+
201
+ ### Unbounded Arrays
202
+
203
+ Several data structures grow without explicit caps:
204
+
205
+ - **Held fees**: Bounded in practice by the 28-day holding window and auto-cleanup when processed. `processHeldFeesOf` accepts a `count` parameter to limit gas per call.
206
+ - **Splits**: No explicit cap, but the percentage constraint (`SPLITS_TOTAL_PERCENT = 1_000_000_000`) limits the useful number to roughly 300-500. Gas cost scales linearly (~100k gas per split during payout distribution), so very large split groups may cause out-of-gas reverts.
207
+ - **Accounting contexts**: Duplicate prevention limits growth. Realistic maximum is ~100-200 tokens per terminal.
208
+ - **Payout limits / surplus allowances**: Currency ordering constraint limits each group to ~30-50 entries.
209
+
210
+ ### Price Feed DoS
211
+
212
+ If a Chainlink price feed reverts (stale data, negative price, sequencer downtime on L2), any operation that requires that currency conversion will also revert. This is a **liveness** risk, not a fund-loss risk: no funds are at risk, but payments, payouts, or cash outs denominated in the affected currency pair will be temporarily blocked until the feed recovers. Projects using multiple currencies should be aware of this dependency.
213
+
214
+ ### Weight Cache Requirement
215
+
216
+ Ruleset weight decay is calculated iteratively. For long-running projects with many elapsed cycles, the iteration count can exceed the block gas limit. The protocol caps iteration at 20,000 cycles per call and provides `updateRulesetWeightCache()` for progressive caching. Projects with very short durations (e.g., 1-second rulesets) that run for extended periods must periodically call this function to keep weight calculations within gas limits.
217
+
218
+ ### Flash Loan Considerations
219
+
220
+ **C-5 (known, documented)**: Calling `cashOut` with `cashOutCount = 0` when `totalSupply == 0` returns the project's entire surplus. This is a known edge case -- it requires a project to have surplus but zero outstanding tokens, which is not a normal operating state. Projects that accumulate surplus without token holders should be aware of this behavior. The protocol's test suite includes 12 flash-loan attack vectors, all of which confirm that no profit is extractable under normal conditions (tokens minted during a payment are worth at most what was paid).
221
+
166
222
  ## Install
167
223
 
168
224
  ```bash
@@ -178,5 +234,5 @@ npm install
178
234
  | `forge test -vvvv` | Run tests with full traces |
179
235
  | `forge fmt` | Format code |
180
236
  | `forge fmt --check` | Check formatting (CI lint) |
181
- | `FOUNDRY_PROFILE=fork forge test` | Run fork tests |
237
+ | `FOUNDRY_PROFILE=CI forge test` | Run fork tests |
182
238
  | `forge coverage --match-path "./src/*.sol"` | Generate coverage report |
package/RISKS.md CHANGED
@@ -12,13 +12,13 @@ What must be true for the system to remain safe:
12
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
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
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
+ - **`OMNICHAIN_RULESET_OPERATOR` is trusted.** This immutable address bypasses owner permission checks for `launchRulesetsFor` and `queueRulesetsOf`. It can also indirectly set terminals through `launchRulesetsFor`, but cannot call `setTerminalsOf` directly. A compromised operator can queue arbitrary rulesets for any project.
16
16
 
17
17
  ## 2. Economic Risks
18
18
 
19
19
  ### Bonding Curve
20
20
 
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.
21
+ - **Zero cash out guard.** `cashOutFrom` returns 0 when `cashOutCount == 0` (early return). Auditors should verify no code path bypasses this guard or reaches the `cashOutCount >= totalSupply` branch with both values at 0.
22
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
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
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.
@@ -26,11 +26,7 @@ What must be true for the system to remain safe:
26
26
  ### Fee Arithmetic
27
27
 
28
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.
30
-
31
- ### First Cycle Behavior
32
-
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`).
29
+ - **Held fee amount mutation.** `_returnHeldFees` mutates `heldFee.amount` in-place via unchecked subtraction. If the accounting is off by even 1 wei in the wrong direction, this underflows and corrupts the held fee entry.
34
30
 
35
31
  ### Weight Decay
36
32
 
@@ -74,14 +70,10 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
74
70
  ### Permission System
75
71
 
76
72
  - **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.
73
+ - **ROOT + wildcard (`projectId = 0`) is allowed for self-grants only.** An account can grant its own operator ROOT on the wildcard project, giving that operator god-mode across all of the account's projects. This is powerful but legitimate the account owner is explicitly choosing to delegate full control. However, a third-party caller who holds ROOT for a specific project **cannot** grant ROOT to others or set any permissions on the wildcard project on someone else's behalf. This prevents ROOT operators from escalating their own privileges beyond what the account owner originally granted. Auditors should verify that no indirect path allows a ROOT operator on project X to escalate to ROOT on project Y without the account owner's direct action.
78
74
  - **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
75
  - **`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
76
 
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
77
  ### Migration
86
78
 
87
79
  - **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.
@@ -131,6 +123,8 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
131
123
  - **Data hooks are called during previews.** `previewPayFor` and `previewCashOutFrom` invoke `beforePayRecordedWith` and `beforeCashOutRecordedWith` on data hooks. A reverting data hook causes the preview to revert. A gas-consuming hook can cause the preview to run out of gas.
132
124
  - **Store previews 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 functions (`JBMultiTerminal.previewPayFor` / `previewCashOutFrom`) handle this automatically by passing `address(this)`.
133
125
  - **No state modification risk.** Preview functions cannot change balances, mint/burn tokens, or consume limits. They are safe to call from any context.
126
+ - **Preview-execution divergence.** Preview functions and their corresponding real operations share computation logic but execute in different contexts. `previewPayFor` calls `STORE.previewPayFrom` which invokes the data hook's `beforePayRecordedWith` via `staticcall`. The real `pay` path invokes the same hook but state changes between preview and execution (other payments, cash outs, ruleset transitions) can change the data hook's response. Callers should treat preview results as estimates, not guarantees — especially for projects with stateful data hooks.
127
+ - **Gas griefing via data hooks in previews.** Since `previewPayFor` and `previewCashOutFrom` invoke data hooks, a gas-expensive data hook can make preview calls prohibitively expensive. This affects frontends and indexers that rely on preview functions for quote display. Unlike the real operations (which have economic incentive to complete), preview calls have no built-in gas limit on the data hook invocation.
134
128
 
135
129
  ## 7. Integration Risks
136
130
 
@@ -143,15 +137,19 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
143
137
 
144
138
  ### Permit2 Interactions
145
139
 
146
- - `_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.
147
- - `_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.
148
- - The `uint160` cast on line 1897 of `JBMultiTerminal.sol` limits Permit2 transfers to `type(uint160).max`. Amounts above this revert with `OverflowAlert`.
140
+ - **Permit2 is only used for inbound transfers.** `_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.
141
+ - **Outbound transfers never use Permit2.** All outbound `_transferFrom` calls pass `from: address(this)`, which takes the direct transfer path (`safeTransfer` for ERC-20s, `Address.sendValue` for native token) and returns before reaching the Permit2 fallback. The Permit2 fallback in `_transferFrom` only exists for the inbound case where `from` is the payer (`_msgSender()`).
142
+ - The `uint160` cast in `JBMultiTerminal._acceptFundsFor` limits Permit2 transfers to `type(uint160).max`. Amounts above this revert with `OverflowAlert`.
149
143
 
150
144
  ### Cross-Terminal Surplus Aggregation
151
145
 
152
146
  - `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. When `useTotalSurplusForCashOuts` is false, surplus is computed internally by the store for only the reclaimed token, avoiding cross-terminal external calls.
153
147
  - 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.
154
148
 
149
+ ### `addToBalanceOf` with Arbitrary Metadata
150
+
151
+ - `addToBalanceOf` accepts arbitrary `metadata` which is not validated by the terminal or store. The metadata is passed through to `afterAddToBalanceRecordedWith` callbacks. If a project's data hook or terminal extension interprets this metadata, malformed metadata could cause unexpected behavior. The core protocol ignores metadata in `addToBalanceOf` — it only affects hook processing.
152
+
155
153
  ### `recordAddedBalanceFor` Access Control
156
154
 
157
155
  - `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.
@@ -188,10 +186,5 @@ These should hold at all times and are the most productive targets for formal ve
188
186
  ### No Flash-Loan Profit
189
187
  - `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.
190
188
 
191
- ### Same-Terminal Fee Exemption
192
-
193
- - **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`).
194
- - **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`.
195
-
196
189
  ### Held Fee Integrity
197
190
  - `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.