@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
@@ -177,6 +177,7 @@ contract JBFundAccessLimits is JBControlled, IJBFundAccessLimits {
177
177
 
178
178
  // If the currencies match, return the value.
179
179
  if (currency == packedPayoutLimitData >> 224) {
180
+ // forge-lint: disable-next-line(unsafe-typecast)
180
181
  return uint256(uint224(packedPayoutLimitData));
181
182
  }
182
183
  }
@@ -217,8 +218,12 @@ contract JBFundAccessLimits is JBControlled, IJBFundAccessLimits {
217
218
  uint256 packedPayoutLimitData = packedPayoutLimitsData[i];
218
219
 
219
220
  // The limit amount is in bits 0-223. The currency is in bits 224-255.
221
+ // forge-lint: disable-next-line(unsafe-typecast)
220
222
  payoutLimits[i] = JBCurrencyAmount({
221
- currency: uint32(packedPayoutLimitData >> 224), amount: uint224(packedPayoutLimitData)
223
+ // forge-lint: disable-next-line(unsafe-typecast)
224
+ currency: uint32(packedPayoutLimitData >> 224),
225
+ // forge-lint: disable-next-line(unsafe-typecast)
226
+ amount: uint224(packedPayoutLimitData)
222
227
  });
223
228
  }
224
229
  }
@@ -258,6 +263,7 @@ contract JBFundAccessLimits is JBControlled, IJBFundAccessLimits {
258
263
 
259
264
  // If the currencies match, return the value.
260
265
  if (currency == packedSurplusAllowanceData >> 224) {
266
+ // forge-lint: disable-next-line(unsafe-typecast)
261
267
  return uint256(uint224(packedSurplusAllowanceData));
262
268
  }
263
269
  }
@@ -300,8 +306,12 @@ contract JBFundAccessLimits is JBControlled, IJBFundAccessLimits {
300
306
  uint256 packedSurplusAllowanceData = packedSurplusAllowancesData[i];
301
307
 
302
308
  // The limit is in bits 0-223. The currency is in bits 224-255.
309
+ // forge-lint: disable-next-line(unsafe-typecast)
303
310
  surplusAllowances[i] = JBCurrencyAmount({
304
- currency: uint32(packedSurplusAllowanceData >> 224), amount: uint224(packedSurplusAllowanceData)
311
+ // forge-lint: disable-next-line(unsafe-typecast)
312
+ currency: uint32(packedSurplusAllowanceData >> 224),
313
+ // forge-lint: disable-next-line(unsafe-typecast)
314
+ amount: uint224(packedSurplusAllowanceData)
305
315
  });
306
316
  }
307
317
  }
@@ -136,6 +136,18 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
136
136
  /// @custom:param projectId The ID of the project to get a list of accepted tokens for.
137
137
  mapping(uint256 projectId => JBAccountingContext[]) internal _accountingContextsOf;
138
138
 
139
+ /// @notice The cumulative amount of fee-free intra-terminal payouts a project has received for a given token.
140
+ /// @dev Incremented each time a fee-free payout lands (same terminal, no fee charged). During cashout with
141
+ /// `cashOutTaxRate == 0`, fees are applied only up to this amount, then decremented. This prevents a round-trip
142
+ /// fee bypass (intra-terminal payout → zero-tax cashout) while scoping the fee precisely to the fee-free inflow
143
+ /// — legitimate cashouts beyond this amount remain fee-free.
144
+ /// @dev WARNING: This accumulator persists across rulesets and cannot be cleared. Once a fee-free payout
145
+ /// increments it, the balance remains until consumed by a zero-tax cashout. There is no admin function to reset
146
+ /// it. Projects switching from zero-tax to non-zero-tax rulesets will carry forward any unconsumed balance.
147
+ /// @custom:param projectId The ID of the project that received the payout.
148
+ /// @custom:param token The token that was received.
149
+ mapping(uint256 projectId => mapping(address token => uint256)) internal _feeFreeSurplusOf;
150
+
139
151
  /// @notice Fees that are being held for each project.
140
152
  /// @dev Projects can temporarily hold fees and unlock them later by adding funds to the project's balance.
141
153
  /// @dev Held fees can be processed at any time by this terminal's owner.
@@ -416,12 +428,21 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
416
428
  revert JBMultiTerminal_RecipientProjectTerminalNotFound(split.projectId, token);
417
429
  }
418
430
 
431
+ // Fees apply to fund egress, not intra-terminal accounting. When both projects share this terminal,
432
+ // funds stay within the contract (addToBalance or pay) so no fee is charged. This is intentional:
433
+ // the fee model taxes value leaving the protocol ecosystem, not internal rebalancing.
419
434
  // This payout is eligible for a fee if the funds are leaving this contract and the receiving terminal isn't
420
- // a feelss address.
435
+ // a feeless address.
421
436
  if (terminal != this && !_isFeeless(address(terminal))) {
422
437
  netPayoutAmount -= JBFees.feeAmountFrom({amountBeforeFee: amount, feePercent: FEE});
423
438
  }
424
439
 
440
+ // Track the fee-free payout amount. During cashout at zero tax rate, fees apply
441
+ // only up to this accumulated amount, preventing round-trip fee bypass.
442
+ if (terminal == this) {
443
+ _feeFreeSurplusOf[split.projectId][token] += netPayoutAmount;
444
+ }
445
+
425
446
  // Send the `projectId` in the metadata as a referral.
426
447
  bytes memory metadata = bytes(abi.encodePacked(projectId));
427
448
 
@@ -541,6 +562,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
541
562
  revert JBMultiTerminal_TerminalTokensIncompatible({projectId: projectId, token: token, terminal: to});
542
563
  }
543
564
 
565
+ // Terminal migration intentionally does not transfer held fees. Held fees belong to the
566
+ // fee beneficiary (project #1), not the migrating project. They unlock after 28 days regardless of terminal.
544
567
  // Record the migration in the store.
545
568
  // slither-disable-next-line reentrancy-events
546
569
  balance = STORE.recordTerminalMigration({projectId: projectId, token: token});
@@ -625,6 +648,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
625
648
  }
626
649
 
627
650
  /// @notice Process any fees that are being held for the project.
651
+ /// @dev Reentrancy safety: the loop re-reads `_nextHeldFeeIndexOf` from storage each iteration and advances the
652
+ /// index before the external `_processFee` call, so a reentrant call cannot double-process the same fee entry.
628
653
  /// @param projectId The ID of the project to process held fees for.
629
654
  /// @param token The token to process held fees for.
630
655
  /// @param count The number of fees to process.
@@ -1049,6 +1074,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1049
1074
  accountingContext: accountingContext,
1050
1075
  balanceAccountingContexts: balanceAccountingContexts,
1051
1076
  cashOutCount: cashOutCount,
1077
+ beneficiaryIsFeeless: _isFeeless(beneficiary),
1052
1078
  metadata: metadata
1053
1079
  });
1054
1080
  }
@@ -1064,12 +1090,22 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1064
1090
 
1065
1091
  // Send the reclaimed funds to the beneficiary.
1066
1092
  if (reclaimAmount != 0) {
1067
- // Determine if a fee should be taken. Fees are not taked if the cash out tax rate is zero,
1068
- // if the beneficiary is feeless, or if the fee beneficiary doesn't accept the given token.
1069
- if (!_isFeeless(beneficiary) && cashOutTaxRate != 0) {
1070
- amountEligibleForFees += reclaimAmount;
1071
- // Subtract the fee for the reclaimed amount.
1072
- reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: reclaimAmount, feePercent: FEE});
1093
+ // Determine if a fee should be taken. Fees are not taken if the beneficiary is feeless.
1094
+ if (!_isFeeless(beneficiary)) {
1095
+ if (cashOutTaxRate != 0) {
1096
+ // Non-zero tax: fees apply to the full reclaim amount.
1097
+ amountEligibleForFees += reclaimAmount;
1098
+ reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: reclaimAmount, feePercent: FEE});
1099
+ } else {
1100
+ // Zero tax: fees apply only up to the fee-free surplus (round-trip prevention).
1101
+ uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][tokenToReclaim];
1102
+ if (feeFreeSurplus != 0) {
1103
+ uint256 feeableAmount = reclaimAmount < feeFreeSurplus ? reclaimAmount : feeFreeSurplus;
1104
+ _feeFreeSurplusOf[projectId][tokenToReclaim] = feeFreeSurplus - feeableAmount;
1105
+ amountEligibleForFees += feeableAmount;
1106
+ reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: feeableAmount, feePercent: FEE});
1107
+ }
1108
+ }
1073
1109
  }
1074
1110
 
1075
1111
  // Subtract the fee from the reclaim amount.
@@ -1623,7 +1659,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1623
1659
  internal
1624
1660
  returns (uint256)
1625
1661
  {
1626
- // Attempt to distribute this split.
1662
+ // Failed split payouts consume the payout limit by design. The try-catch prevents a single
1663
+ // split from DoS-ing the entire payout. Failed splits' amounts are returned to the project balance via
1664
+ // `_recordAddedBalanceFor`. Payout limit consumption is correct because the project authorized the
1665
+ // distribution.
1627
1666
  // slither-disable-next-line reentrancy-events
1628
1667
  try this.executePayout({
1629
1668
  split: split, projectId: projectId, token: token, amount: amount, originalMessageSender: _msgSender()
@@ -1697,7 +1736,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1697
1736
  ? 0
1698
1737
  : JBFees.feeAmountFrom({amountBeforeFee: leftoverPayoutAmount, feePercent: FEE});
1699
1738
 
1700
- // Transfer the amount to the project owner.
1739
+ // Failed owner transfer consumes the payout limit by design. Same pattern as split payouts:
1740
+ // the try-catch prevents revert, failed amount is returned to project balance, and the owner can retry
1741
+ // via addToBalanceOf or in the next cycle.
1701
1742
  try this.executeTransferTo({addr: projectOwner, token: token, amount: leftoverPayoutAmount - fee}) {
1702
1743
  if (fee > 0) {
1703
1744
  amountEligibleForFees += leftoverPayoutAmount;
@@ -1834,6 +1875,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1834
1875
  JBFee({
1835
1876
  amount: amount,
1836
1877
  beneficiary: beneficiary,
1878
+ // forge-lint: disable-next-line(unsafe-typecast)
1837
1879
  unlockTimestamp: uint48(block.timestamp + _FEE_HOLDING_SECONDS)
1838
1880
  })
1839
1881
  );
@@ -1877,7 +1919,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1877
1919
  }
1878
1920
 
1879
1921
  // If there's sufficient approval, transfer normally.
1880
- if (IERC20(token).allowance(address(from), address(this)) >= amount) {
1922
+ if (IERC20(token).allowance({owner: address(from), spender: address(this)}) >= amount) {
1881
1923
  return IERC20(token).safeTransferFrom({from: from, to: to, value: amount});
1882
1924
  }
1883
1925
 
@@ -1885,6 +1927,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1885
1927
  if (amount > type(uint160).max) revert JBMultiTerminal_OverflowAlert(amount, type(uint160).max);
1886
1928
 
1887
1929
  // Otherwise we attempt to use the PERMIT2 method.
1930
+ // forge-lint: disable-next-line(unsafe-typecast)
1888
1931
  PERMIT2.transferFrom({from: from, to: to, amount: uint160(amount), token: token});
1889
1932
  }
1890
1933
 
@@ -154,6 +154,8 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
154
154
  uint256 operatorAccountWildcardProjectPermissions =
155
155
  includeWildcardProjectId ? permissionsOf[operator][account][WILDCARD_PROJECT_ID] : 0;
156
156
 
157
+ // Returns true for empty permission arrays by design (vacuous truth). An empty set of
158
+ // required permissions is trivially satisfied. Callers should validate non-empty permission arrays if needed.
157
159
  for (uint256 i; i < permissionIds.length; i++) {
158
160
  // Set the permission being iterated on.
159
161
  uint256 permissionId = permissionIds[i];
@@ -250,6 +252,7 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
250
252
  uint256 permissionId = permissionIds[i];
251
253
 
252
254
  // Turn on the bit at the ID.
255
+ // forge-lint: disable-next-line(incorrect-shift)
253
256
  packed |= 1 << permissionId;
254
257
  }
255
258
  }
package/src/JBPrices.sol CHANGED
@@ -15,8 +15,10 @@ import {IJBProjects} from "./interfaces/IJBProjects.sol";
15
15
 
16
16
  /// @notice Manages and normalizes price feeds. Price feeds are contracts which return the "pricing currency" cost of 1
17
17
  /// "unit currency".
18
- /// @dev Price feeds are immutable once set and cannot be replaced or removed. If a price feed needs to be changed,
19
- /// a new JBPrices contract must be deployed and projects must migrate to use it.
18
+ /// @dev Price feeds are immutable once set and cannot be replaced or removed. This prevents oracle manipulation via
19
+ /// admin-key attacks, but means a misconfigured or failing feed will cause operations using that currency pair to
20
+ /// revert (DoS, not fund loss). Select feeds carefully — recovery requires deploying a new JBPrices contract and
21
+ /// migrating projects.
20
22
  contract JBPrices is JBControlled, JBPermissioned, ERC2771Context, Ownable, IJBPrices {
21
23
  //*********************************************************************//
22
24
  // --------------------------- custom errors ------------------------- //
@@ -134,6 +136,10 @@ contract JBPrices is JBControlled, JBPermissioned, ERC2771Context, Ownable, IJBP
134
136
  : priceFeedFor[projectId][unitCurrency][pricingCurrency]);
135
137
  }
136
138
 
139
+ // Price feed immutability is by design to prevent admin-key attacks on price oracles.
140
+ // If a feed fails, operations using that currency pair revert (DoS but not fund loss). Projects can use
141
+ // alternative currency pairs. A default feed for a currency pair prevents per-project overrides to ensure
142
+ // price consistency; projects should use unused currency IDs for custom pricing.
137
143
  // Store the feed.
138
144
  priceFeedFor[projectId][pricingCurrency][unitCurrency] = feed;
139
145
 
@@ -74,7 +74,7 @@ contract JBProjects is ERC721, ERC2771Context, Ownable, IJBProjects {
74
74
  emit Create({projectId: projectId, owner: owner, caller: _msgSender()});
75
75
 
76
76
  // Mint the project.
77
- _safeMint(owner, projectId);
77
+ _safeMint({to: owner, tokenId: projectId});
78
78
  }
79
79
 
80
80
  //*********************************************************************//
@@ -247,6 +247,7 @@ contract JBRulesets is JBControlled, IJBRulesets {
247
247
  // Calculate the weight cut multiple.
248
248
  uint168 weightCutMultiple;
249
249
  unchecked {
250
+ // forge-lint: disable-next-line(unsafe-typecast)
250
251
  weightCutMultiple = uint168(startDistance / targetRuleset.duration);
251
252
  }
252
253
 
@@ -352,6 +353,9 @@ contract JBRulesets is JBControlled, IJBRulesets {
352
353
 
353
354
  /// @notice The ruleset that is currently active for the specified project.
354
355
  /// @dev If a current ruleset of the project is not found, returns an empty ruleset with all properties set to 0.
356
+ /// @dev The first cycle returns the stored ruleset directly (cycleNumber=1, original weight). Subsequent cycles
357
+ /// simulate cycling with weight decay via `_simulateCycledRulesetBasedOn`. Payout limits reset each cycle because
358
+ /// the terminal store keys usage by rulesetId, and each cycle produces a new simulated rulesetId.
355
359
  /// @param projectId The ID of the project to get the current ruleset of.
356
360
  /// @return ruleset The project's current ruleset.
357
361
  function currentOf(uint256 projectId) external view override returns (JBRuleset memory ruleset) {
@@ -955,26 +959,34 @@ contract JBRulesets is JBControlled, IJBRulesets {
955
959
  // slither-disable-next-line incorrect-equality
956
960
  if (rulesetId == 0) return ruleset;
957
961
 
962
+ // forge-lint: disable-next-line(unsafe-typecast)
958
963
  ruleset.id = uint48(rulesetId);
959
964
 
960
965
  uint256 packedIntrinsicProperties = _packedIntrinsicPropertiesOf[projectId][rulesetId];
961
966
 
962
967
  // `weight` in bits 0-111 bits.
968
+ // forge-lint: disable-next-line(unsafe-typecast)
963
969
  ruleset.weight = uint112(packedIntrinsicProperties);
964
970
  // `basedOnId` in bits 112-159 bits.
971
+ // forge-lint: disable-next-line(unsafe-typecast)
965
972
  ruleset.basedOnId = uint48(packedIntrinsicProperties >> 112);
966
973
  // `start` in bits 160-207 bits.
974
+ // forge-lint: disable-next-line(unsafe-typecast)
967
975
  ruleset.start = uint48(packedIntrinsicProperties >> 160);
968
976
  // `cycleNumber` in bits 208-255 bits.
977
+ // forge-lint: disable-next-line(unsafe-typecast)
969
978
  ruleset.cycleNumber = uint48(packedIntrinsicProperties >> 208);
970
979
 
971
980
  uint256 packedUserProperties = _packedUserPropertiesOf[projectId][rulesetId];
972
981
 
973
982
  // approval hook in bits 0-159 bits.
983
+ // forge-lint: disable-next-line(unsafe-typecast)
974
984
  ruleset.approvalHook = IJBRulesetApprovalHook(address(uint160(packedUserProperties)));
975
985
  // `duration` in bits 160-191 bits.
986
+ // forge-lint: disable-next-line(unsafe-typecast)
976
987
  ruleset.duration = uint32(packedUserProperties >> 160);
977
988
  // weight cut percent in bits 192-223 bits.
989
+ // forge-lint: disable-next-line(unsafe-typecast)
978
990
  ruleset.weightCutPercent = uint32(packedUserProperties >> 192);
979
991
 
980
992
  ruleset.metadata = _metadataOf[projectId][rulesetId];
@@ -1021,9 +1033,11 @@ contract JBRulesets is JBControlled, IJBRulesets {
1021
1033
  });
1022
1034
 
1023
1035
  return JBRuleset({
1036
+ // forge-lint: disable-next-line(unsafe-typecast)
1024
1037
  cycleNumber: uint48(rulesetCycleNumber),
1025
1038
  id: baseRuleset.id,
1026
1039
  basedOnId: baseRuleset.basedOnId,
1040
+ // forge-lint: disable-next-line(unsafe-typecast)
1027
1041
  start: uint48(start),
1028
1042
  duration: baseRuleset.duration,
1029
1043
  weight: uint112(
package/src/JBSplits.sol CHANGED
@@ -77,8 +77,9 @@ contract JBSplits is JBControlled, IJBSplits {
77
77
 
78
78
  /// @notice Sets a project's split groups.
79
79
  /// @dev Only a project's controller can set its splits, unless the first 160 bits of the group's ID match
80
- /// `msg.sender`, in which case the caller can set its own splits. The remaining upper 96 bits are free for the
81
- /// caller to use as sub-categorization.
80
+ /// `msg.sender` AND the upper 96 bits are non-zero, in which case the caller can set its own splits.
81
+ /// GroupIds with zero upper 96 bits (i.e. bare addresses) are reserved for protocol use (e.g. terminal
82
+ /// payout groups keyed by token address) and always require controller authorization.
82
83
  /// @dev The new split groups must include any currently set splits that are locked.
83
84
  /// @param projectId The ID of the project to set the split groups of.
84
85
  /// @param rulesetId The ID of the ruleset the split groups should be active in. Send
@@ -101,9 +102,12 @@ contract JBSplits is JBControlled, IJBSplits {
101
102
  // Get a reference to the grouped split being iterated on.
102
103
  JBSplitGroup memory splitGroup = splitGroups[i];
103
104
 
104
- // Allow contracts to set splits in their own namespace (first 160 bits of groupId == msg.sender).
105
- // Otherwise, require controller (checked once).
106
- if (address(uint160(splitGroup.groupId)) != msg.sender && !controllerChecked) {
105
+ // Self-auth: lower 160 bits must match msg.sender AND upper 96 bits must be non-zero.
106
+ // GroupIds with zero upper bits are reserved for protocol use (e.g. terminal payout groups)
107
+ // and always require controller authorization to prevent token contracts from hijacking payouts.
108
+ bool isSelfManaged = splitGroup.groupId >> 160 != 0 && address(uint160(splitGroup.groupId)) == msg.sender;
109
+
110
+ if (!isSelfManaged && !controllerChecked) {
107
111
  _onlyControllerOf(projectId);
108
112
  controllerChecked = true;
109
113
  }
@@ -271,10 +275,13 @@ contract JBSplits is JBControlled, IJBSplits {
271
275
  JBSplit memory split;
272
276
 
273
277
  // `percent` in bits 0-31.
278
+ // forge-lint: disable-next-line(unsafe-typecast)
274
279
  split.percent = uint32(packedSplitPart1);
275
280
  // `projectId` in bits 32-95.
281
+ // forge-lint: disable-next-line(unsafe-typecast)
276
282
  split.projectId = uint64(packedSplitPart1 >> 32);
277
283
  // `beneficiary` in bits 96-255.
284
+ // forge-lint: disable-next-line(unsafe-typecast)
278
285
  split.beneficiary = payable(address(uint160(packedSplitPart1 >> 96)));
279
286
 
280
287
  // Get a reference to the second part of the split's packed data.
@@ -285,8 +292,10 @@ contract JBSplits is JBControlled, IJBSplits {
285
292
  // `preferAddToBalance` in bit 0.
286
293
  split.preferAddToBalance = packedSplitPart2 & 1 == 1;
287
294
  // `lockedUntil` in bits 1-48.
295
+ // forge-lint: disable-next-line(unsafe-typecast)
288
296
  split.lockedUntil = uint48(packedSplitPart2 >> 1);
289
297
  // `hook` in bits 49-208.
298
+ // forge-lint: disable-next-line(unsafe-typecast)
290
299
  split.hook = IJBSplitHook(address(uint160(packedSplitPart2 >> 49)));
291
300
  }
292
301
 
@@ -156,6 +156,8 @@ contract JBTerminalStore is IJBTerminalStore {
156
156
  /// @param accountingContext The accounting context of the token being reclaimed by the cash out.
157
157
  /// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
158
158
  /// surplus being reclaimed from.
159
+ /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address. Passed through to data
160
+ /// hooks so they can skip their own fees when value stays in the protocol (e.g. project-to-project routing).
159
161
  /// @param metadata Bytes to send to the data hook, if the project's current ruleset specifies one.
160
162
  /// @return ruleset The ruleset during the cash out was made during, as a `JBRuleset` struct. This ruleset will
161
163
  /// have a cash out tax rate provided by the cash out hook if applicable.
@@ -170,6 +172,7 @@ contract JBTerminalStore is IJBTerminalStore {
170
172
  uint256 cashOutCount,
171
173
  JBAccountingContext calldata accountingContext,
172
174
  JBAccountingContext[] calldata balanceAccountingContexts,
175
+ bool beneficiaryIsFeeless,
173
176
  bytes memory metadata
174
177
  )
175
178
  external
@@ -184,10 +187,9 @@ contract JBTerminalStore is IJBTerminalStore {
184
187
  // Get a reference to the project's current ruleset.
185
188
  ruleset = RULESETS.currentOf(projectId);
186
189
 
187
- // Get the current surplus amount.
188
- // Use the local surplus if the ruleset specifies that it should be used. Otherwise, use the project's total
189
- // surplus across all of its terminals.
190
- uint256 currentSurplus = ruleset.useTotalSurplusForCashOuts()
190
+ // Store the current surplus in `reclaimAmount` temporarily to avoid allocating a separate local variable
191
+ // (saves one stack slot, which is needed to fit the 7th parameter without hitting stack-too-deep).
192
+ reclaimAmount = ruleset.useTotalSurplusForCashOuts()
191
193
  ? JBSurplus.currentSurplusOf({
192
194
  projectId: projectId,
193
195
  terminals: DIRECTORY.terminalsOf(projectId),
@@ -204,54 +206,59 @@ contract JBTerminalStore is IJBTerminalStore {
204
206
  targetCurrency: accountingContext.currency
205
207
  });
206
208
 
207
- // Get the total number of outstanding project tokens.
208
- uint256 totalSupply =
209
- IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
210
-
211
- // Can't cash out more tokens than are in the supply.
212
- if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
213
-
214
- // SECURITY NOTE: The data hook has absolute control over cash-out economics.
215
- // It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
216
- // completely overriding the terminal's bonding curve math. For example, setting
217
- // totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
218
- // Project owners MUST audit their data hooks with the same rigor as the terminal.
219
-
220
- // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
221
- if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
222
- // Create the cash out context that'll be sent to the data hook.
223
- JBBeforeCashOutRecordedContext memory context = JBBeforeCashOutRecordedContext({
224
- terminal: msg.sender,
225
- holder: holder,
226
- projectId: projectId,
227
- rulesetId: ruleset.id,
228
- cashOutCount: cashOutCount,
229
- totalSupply: totalSupply,
230
- surplus: JBTokenAmount({
209
+ // Scoped to keep `totalSupply` and `context` off the outer stack.
210
+ {
211
+ // Get the total number of outstanding project tokens.
212
+ uint256 totalSupply = IJBController(address(DIRECTORY.controllerOf(projectId)))
213
+ .totalTokenSupplyWithReservedTokensOf(projectId);
214
+
215
+ // Can't cash out more tokens than are in the supply.
216
+ if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
217
+
218
+ // SECURITY NOTE: The data hook has absolute control over cash-out economics.
219
+ // It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
220
+ // completely overriding the terminal's bonding curve math. For example, setting
221
+ // totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
222
+ // Project owners MUST audit their data hooks with the same rigor as the terminal.
223
+
224
+ // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
225
+ if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
226
+ // Build the cash out context field-by-field to avoid stack-too-deep
227
+ // (the struct has 11 fields — a struct literal would require all values on the stack at once).
228
+ JBBeforeCashOutRecordedContext memory context;
229
+ context.terminal = msg.sender;
230
+ context.holder = holder;
231
+ context.projectId = projectId;
232
+ context.rulesetId = ruleset.id;
233
+ context.cashOutCount = cashOutCount;
234
+ context.totalSupply = totalSupply;
235
+ context.surplus = JBTokenAmount({
231
236
  token: accountingContext.token,
232
- value: currentSurplus,
237
+ value: reclaimAmount, // reclaimAmount temporarily holds the current surplus.
233
238
  decimals: accountingContext.decimals,
234
239
  currency: accountingContext.currency
235
- }),
236
- useTotalSurplus: ruleset.useTotalSurplusForCashOuts(),
237
- cashOutTaxRate: ruleset.cashOutTaxRate(),
238
- metadata: metadata
239
- });
240
+ });
241
+ context.useTotalSurplus = ruleset.useTotalSurplusForCashOuts();
242
+ context.cashOutTaxRate = ruleset.cashOutTaxRate();
243
+ context.beneficiaryIsFeeless = beneficiaryIsFeeless;
244
+ context.metadata = metadata;
240
245
 
241
- (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
242
- IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
243
- } else {
244
- cashOutTaxRate = ruleset.cashOutTaxRate();
245
- }
246
+ (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
247
+ IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
248
+ } else {
249
+ cashOutTaxRate = ruleset.cashOutTaxRate();
250
+ }
246
251
 
247
- if (currentSurplus != 0) {
248
- // Calculate reclaim amount using the current surplus amount.
249
- reclaimAmount = JBCashOuts.cashOutFrom({
250
- surplus: currentSurplus,
251
- cashOutCount: cashOutCount,
252
- totalSupply: totalSupply,
253
- cashOutTaxRate: cashOutTaxRate
254
- });
252
+ // Calculate the reclaim amount. `reclaimAmount` currently holds the surplus — overwrite it with the
253
+ // result.
254
+ if (reclaimAmount != 0) {
255
+ reclaimAmount = JBCashOuts.cashOutFrom({
256
+ surplus: reclaimAmount,
257
+ cashOutCount: cashOutCount,
258
+ totalSupply: totalSupply,
259
+ cashOutTaxRate: cashOutTaxRate
260
+ });
261
+ }
255
262
  }
256
263
 
257
264
  // Keep a reference to the amount that should be added to the project's balance.
@@ -868,6 +875,7 @@ contract JBTerminalStore is IJBTerminalStore {
868
875
  terminal
869
876
  ][projectId][accountingContext.token][ruleset.cycleNumber][payoutLimit.currency];
870
877
  if (remaining > type(uint224).max) revert JBTerminalStore_Uint224Overflow(remaining);
878
+ // forge-lint: disable-next-line(unsafe-typecast)
871
879
  payoutLimit.amount = uint224(remaining);
872
880
  }
873
881
 
@@ -877,6 +885,7 @@ contract JBTerminalStore is IJBTerminalStore {
877
885
  value: payoutLimit.amount, decimals: accountingContext.decimals, targetDecimals: targetDecimals
878
886
  });
879
887
  if (adjusted > type(uint224).max) revert JBTerminalStore_Uint224Overflow(adjusted);
888
+ // forge-lint: disable-next-line(unsafe-typecast)
880
889
  payoutLimit.amount = uint224(adjusted);
881
890
  }
882
891
 
@@ -894,6 +903,7 @@ contract JBTerminalStore is IJBTerminalStore {
894
903
  })
895
904
  );
896
905
  if (converted > type(uint224).max) revert JBTerminalStore_Uint224Overflow(converted);
906
+ // forge-lint: disable-next-line(unsafe-typecast)
897
907
  payoutLimit.amount = uint224(converted);
898
908
  }
899
909
 
package/src/JBTokens.sol CHANGED
@@ -128,7 +128,7 @@ contract JBTokens is JBControlled, IJBTokens {
128
128
  });
129
129
 
130
130
  // Burn the tokens.
131
- if (tokensToBurn > 0) token.burn(holder, tokensToBurn);
131
+ if (tokensToBurn > 0) token.burn({account: holder, amount: tokensToBurn});
132
132
  }
133
133
 
134
134
  /// @notice Redeem credits to claim tokens into a holder's wallet.
@@ -177,7 +177,7 @@ contract JBTokens is JBControlled, IJBTokens {
177
177
  });
178
178
 
179
179
  // Mint the equivalent amount of the project's token for the holder.
180
- token.mint(beneficiary, count);
180
+ token.mint({account: beneficiary, amount: count});
181
181
  }
182
182
 
183
183
  /// @notice Deploys an ERC-20 token for a project. It will be used when claiming tokens.
@@ -211,7 +211,11 @@ contract JBTokens is JBControlled, IJBTokens {
211
211
 
212
212
  token = salt == bytes32(0)
213
213
  ? IJBToken(Clones.clone(address(TOKEN)))
214
- : IJBToken(Clones.cloneDeterministic(address(TOKEN), keccak256(abi.encode(msg.sender, salt))));
214
+ : IJBToken(
215
+ Clones.cloneDeterministic({
216
+ implementation: address(TOKEN), salt: keccak256(abi.encode(msg.sender, salt))
217
+ })
218
+ );
215
219
 
216
220
  // Store the token contract.
217
221
  tokenOf[projectId] = token;
@@ -257,7 +261,7 @@ contract JBTokens is JBControlled, IJBTokens {
257
261
  if (tokensWereClaimed) {
258
262
  // If tokens should be claimed, mint tokens into the holder's wallet.
259
263
  // slither-disable-next-line reentrancy-events
260
- token.mint(holder, count);
264
+ token.mint({account: holder, amount: count});
261
265
  } else {
262
266
  // Otherwise, add the tokens to their credits and the credit supply.
263
267
  creditBalanceOf[holder][projectId] += count;
@@ -271,6 +275,9 @@ contract JBTokens is JBControlled, IJBTokens {
271
275
 
272
276
  /// @notice Set a project's token if not already set.
273
277
  /// @dev Only a project's controller can set its token.
278
+ /// @dev If the provided ERC-20 has a pre-existing supply (minted outside this contract), that supply will be
279
+ /// included in `totalSupplyOf` and will dilute cash-out calculations for all token holders. Project owners are
280
+ /// responsible for ensuring the token's supply is appropriate before calling this function.
274
281
  /// @param projectId The ID of the project to set the token of.
275
282
  /// @param token The new token's address.
276
283
  function setTokenFor(uint256 projectId, IJBToken token) external override onlyControllerOf(projectId) {
@@ -298,6 +305,38 @@ contract JBTokens is JBControlled, IJBTokens {
298
305
  emit SetToken({projectId: projectId, token: token, caller: msg.sender});
299
306
  }
300
307
 
308
+ /// @notice Sets the name and symbol of a project's token.
309
+ /// @dev Only a project's controller can set the token's name and symbol.
310
+ /// @param projectId The ID of the project whose token is being updated.
311
+ /// @param name The new name.
312
+ /// @param symbol The new symbol.
313
+ function setTokenMetadataFor(
314
+ uint256 projectId,
315
+ string calldata name,
316
+ string calldata symbol
317
+ )
318
+ external
319
+ override
320
+ onlyControllerOf(projectId)
321
+ {
322
+ // Get a reference to the project's current token.
323
+ IJBToken token = tokenOf[projectId];
324
+
325
+ // The project must have a token contract attached.
326
+ if (token == IJBToken(address(0))) revert JBTokens_TokenNotFound();
327
+
328
+ // There must be a name.
329
+ if (bytes(name).length == 0) revert JBTokens_EmptyName();
330
+
331
+ // There must be a symbol.
332
+ if (bytes(symbol).length == 0) revert JBTokens_EmptySymbol();
333
+
334
+ emit SetTokenMetadata({projectId: projectId, name: name, symbol: symbol, caller: msg.sender});
335
+
336
+ // Set the name and symbol.
337
+ token.setMetadata({name: name, symbol: symbol});
338
+ }
339
+
301
340
  /// @notice Allows a holder to transfer credits to another account.
302
341
  /// @dev Only a project's controller can transfer credits for that project.
303
342
  /// @param holder The address to transfer credits from.
@@ -345,6 +345,12 @@ interface IJBController is IERC165, IJBProjectUriRegistry, IJBDirectoryAccessCon
345
345
  /// @param token The new token's address.
346
346
  function setTokenFor(uint256 projectId, IJBToken token) external;
347
347
 
348
+ /// @notice Sets the name and symbol of a project's token.
349
+ /// @param projectId The ID of the project whose token is being updated.
350
+ /// @param name The new name.
351
+ /// @param symbol The new symbol.
352
+ function setTokenMetadataOf(uint256 projectId, string calldata name, string calldata symbol) external;
353
+
348
354
  /// @notice Transfers credits from one address to another.
349
355
  /// @param holder The address to transfer credits from.
350
356
  /// @param projectId The ID of the project whose credits are being transferred.
@@ -14,5 +14,6 @@ interface IJBPermitTerminal is IJBTerminal {
14
14
  event Permit2AllowanceFailed(address indexed token, address indexed owner, bytes reason);
15
15
 
16
16
  /// @notice The Permit2 contract used for token approvals.
17
+ // forge-lint: disable-next-line(mixed-case-function)
17
18
  function PERMIT2() external returns (IPermit2);
18
19
  }