@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/src/JBERC20.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
@@ -36,7 +36,11 @@ contract JBERC20 is ERC20Votes, ERC20Permit, Ownable, IJBToken {
36
36
  // -------------------------- constructor ---------------------------- //
37
37
  //*********************************************************************//
38
38
 
39
- constructor() Ownable(address(this)) ERC20("invalid", "invalid") ERC20Permit("JBToken") {}
39
+ /// @dev Set `_name` on the implementation contract to prevent it from being initialized directly.
40
+ /// Clones start with empty `_name`, so `initialize(...)` works only on clones.
41
+ constructor() Ownable(address(this)) ERC20("invalid", "invalid") ERC20Permit("JBToken") {
42
+ _name = "invalid";
43
+ }
40
44
 
41
45
  //*********************************************************************//
42
46
  // ---------------------- external transactions ---------------------- //
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBControlled} from "./abstract/JBControlled.sol";
5
5
  import {IJBDirectory} from "./interfaces/IJBDirectory.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -123,9 +123,11 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
123
123
  /// `cashOutTaxRate == 0`, fees are applied only up to this amount, then decremented. This prevents a round-trip
124
124
  /// fee bypass (intra-terminal payout → zero-tax cashout) while scoping the fee precisely to the fee-free inflow
125
125
  /// — legitimate cashouts beyond this amount remain fee-free.
126
- /// @dev WARNING: This accumulator persists across rulesets and cannot be cleared. Once a fee-free payout
127
- /// increments it, the balance remains until consumed by a zero-tax cashout. There is no admin function to reset
128
- /// it. Projects switching from zero-tax to non-zero-tax rulesets will carry forward any unconsumed balance.
126
+ /// @dev Lifecycle: incremented on fee-free intra-terminal payouts. After any outflow (payouts, useAllowanceOf,
127
+ /// non-zero-tax or feeless cashouts), capped at remaining balance non-fee-free funds are considered to leave
128
+ /// first, preserving the fee-free counter. Consumed during zero-tax cashouts. Cleared on terminal migration.
129
+ /// @dev Persists across rulesets — projects switching from zero-tax to non-zero-tax carry forward any
130
+ /// unconsumed balance. There is no admin function to reset it.
129
131
  /// @custom:param projectId The ID of the project that received the payout.
130
132
  /// @custom:param token The token that was received.
131
133
  mapping(uint256 projectId => mapping(address token => uint256)) internal _feeFreeSurplusOf;
@@ -490,6 +492,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
490
492
  revert JBMultiTerminal_TerminalTokensIncompatible({projectId: projectId, token: token, terminal: to});
491
493
  }
492
494
 
495
+ // Clear fee-free surplus tracking before migration. The new terminal has no fee-free history.
496
+ // This prevents stale fee-free surplus from persisting if the project later migrates back.
497
+ delete _feeFreeSurplusOf[projectId][token];
498
+
493
499
  // Terminal migration intentionally does not transfer held fees. Held fees belong to the
494
500
  // fee beneficiary (project #1), not the migrating project. They unlock after 28 days regardless of terminal.
495
501
  // Record the migration in the store.
@@ -578,6 +584,16 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
578
584
  /// @notice Process any fees that are being held for the project.
579
585
  /// @dev Reentrancy safety: the loop re-reads `_nextHeldFeeIndexOf` from storage each iteration and advances the
580
586
  /// index before the external `_processFee` call, so a reentrant call cannot double-process the same fee entry.
587
+ /// @dev Phantom balance risk: after a terminal migration, held fees remain in this terminal but the backing tokens
588
+ /// have been transferred to the new terminal via `migrateBalanceOf`. If `_processFee` reverts (e.g. the fee
589
+ /// terminal rejects the payment), the catch block calls `_recordAddedBalanceFor`, which credits the project's
590
+ /// recorded balance without any actual tokens arriving — creating a phantom balance. This is an accepted
591
+ /// trade-off:
592
+ /// the alternative (losing the fee amount entirely on revert) is worse. Callers should be aware that processing
593
+ /// held fees post-migration may inflate the project's recorded balance if any fee payments revert.
594
+ /// @dev The index-increment-before-`_processFee` pattern is intentional: locked (not-yet-unlocked) fees are skipped
595
+ /// via the `unlockTimestamp` check, and advancing the index before the external call prevents reentrancy from
596
+ /// reprocessing the same fee entry.
581
597
  /// @param projectId The ID of the project to process held fees for.
582
598
  /// @param token The token to process held fees for.
583
599
  /// @param count The number of fees to process.
@@ -1097,6 +1113,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1097
1113
  // Non-zero tax: fees apply to the full reclaim amount.
1098
1114
  amountEligibleForFees += reclaimAmount;
1099
1115
  reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: reclaimAmount, feePercent: FEE});
1116
+ // Cap fee-free surplus at remaining balance (non-fee-free funds leave first).
1117
+ _reduceFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
1100
1118
  } else {
1101
1119
  // Zero tax: fees apply only up to the fee-free surplus (round-trip prevention).
1102
1120
  uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][tokenToReclaim];
@@ -1107,6 +1125,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1107
1125
  reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: feeableAmount, feePercent: FEE});
1108
1126
  }
1109
1127
  }
1128
+ } else {
1129
+ // Feeless beneficiary: fee logic skipped, but still cap fee-free surplus at remaining balance.
1130
+ _reduceFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
1110
1131
  }
1111
1132
 
1112
1133
  // Subtract the fee from the reclaim amount.
@@ -1652,6 +1673,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1652
1673
  (ruleset, amountPaidOut) =
1653
1674
  STORE.recordPayoutFor({projectId: projectId, token: token, amount: amount, currency: currency});
1654
1675
 
1676
+ // Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
1677
+ _reduceFeeFreeSurplus({projectId: projectId, token: token});
1678
+
1655
1679
  // Get a reference to the project's owner.
1656
1680
  // The owner will receive tokens minted by paying the platform fee and receive any leftover funds not sent to
1657
1681
  // payout splits.
@@ -1850,6 +1874,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1850
1874
  (ruleset, amountPaidOut) =
1851
1875
  STORE.recordUsedAllowanceOf({projectId: projectId, token: token, amount: amount, currency: currency});
1852
1876
 
1877
+ // Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
1878
+ _reduceFeeFreeSurplus({projectId: projectId, token: token});
1879
+
1853
1880
  // Take a fee from the `amountPaidOut`, if needed.
1854
1881
  // The net amount is the final amount withdrawn after the fee has been taken.
1855
1882
  // slither-disable-next-line reentrancy-events
@@ -1884,6 +1911,28 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1884
1911
  }
1885
1912
  }
1886
1913
 
1914
+ /// @notice Cap fee-free surplus at the project's remaining balance after an outflow.
1915
+ /// @dev Non-fee-free funds are considered to leave first. Fee-free surplus only decreases when the remaining
1916
+ /// balance can no longer support it. This prevents attackers from using outflows to drain the fee-free counter
1917
+ /// and then cashing out without incurring fees.
1918
+ /// @param projectId The ID of the project.
1919
+ /// @param token The token whose fee-free surplus to cap.
1920
+ function _reduceFeeFreeSurplus(uint256 projectId, address token) internal {
1921
+ // Get the current fee-free surplus for this project/token pair.
1922
+ uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][token];
1923
+
1924
+ // Nothing to reduce if there's no fee-free surplus tracked.
1925
+ if (feeFreeSurplus == 0) return;
1926
+
1927
+ // Get the project's remaining balance (already decremented by the store's record call).
1928
+ uint256 remainingBalance = STORE.balanceOf({terminal: address(this), projectId: projectId, token: token});
1929
+
1930
+ // Cap fee-free surplus at the remaining balance.
1931
+ if (feeFreeSurplus > remainingBalance) {
1932
+ _feeFreeSurplusOf[projectId][token] = remainingBalance;
1933
+ }
1934
+ }
1935
+
1887
1936
  //*********************************************************************//
1888
1937
  // -------------------------- internal views ------------------------- //
1889
1938
  //*********************************************************************//
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -56,7 +56,11 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
56
56
  //*********************************************************************//
57
57
 
58
58
  /// @notice Sets permissions for an operator.
59
- /// @dev Only an address can give permissions to or revoke permissions from its operators.
59
+ /// @dev Only the `account` itself (i.e. `msg.sender == account`) can grant or revoke its operators' permissions
60
+ /// without restriction — including ROOT on the wildcard project ID (`projectId = 0`).
61
+ /// @dev A third-party caller who holds ROOT for a specific project may set *non-ROOT* permissions for that project
62
+ /// on someone else's behalf, but **cannot**: (a) grant ROOT to others, or (b) set any permissions on the wildcard
63
+ /// project ID. This prevents ROOT operators from escalating their own privileges.
60
64
  /// @param account The account setting its operators' permissions.
61
65
  /// @param permissionsData The data which specifies the permissions the operator is being given.
62
66
  function setPermissionsFor(address account, JBPermissionsData calldata permissionsData) external override {
package/src/JBPrices.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {ERC2771Context, Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {mulDiv} from "@prb/math/src/Common.sol";
5
5
 
package/src/JBSplits.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBControlled} from "./abstract/JBControlled.sol";
5
5
  import {IJBDirectory} from "./interfaces/IJBDirectory.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {mulDiv} from "@prb/math/src/Common.sol";
5
5
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
@@ -591,57 +591,6 @@ contract JBTerminalStore is IJBTerminalStore {
591
591
  });
592
592
  }
593
593
 
594
- /// @notice Returns the number of surplus terminal tokens that would be reclaimed from terminals by cashing out a
595
- /// given number of tokens, considering only specific tokens.
596
- /// @param projectId The ID of the project whose tokens would be cashed out.
597
- /// @param cashOutCount The number of tokens that would be cashed out, as a fixed point number with 18 decimals.
598
- /// @param terminals The terminals that would be cashed out from. If this is an empty array, surplus within all
599
- /// the project's terminals are considered.
600
- /// @param tokens The tokens to include in the surplus calculation.
601
- /// @param decimals The number of decimals to include in the resulting fixed point number.
602
- /// @param currency The currency that the resulting number will be in terms of.
603
- /// @return The amount of surplus terminal tokens that would be reclaimed by cashing out `cashOutCount`
604
- /// tokens.
605
- function currentReclaimableSurplusOf(
606
- uint256 projectId,
607
- uint256 cashOutCount,
608
- IJBTerminal[] calldata terminals,
609
- address[] calldata tokens,
610
- uint256 decimals,
611
- uint256 currency
612
- )
613
- external
614
- view
615
- override
616
- returns (uint256)
617
- {
618
- // Aggregate surplus across the terminals, optionally filtered by the specified tokens.
619
- uint256 currentSurplus = _currentSurplusOf({
620
- projectId: projectId, terminals: terminals, tokens: tokens, decimals: decimals, currency: currency
621
- });
622
-
623
- // If there's no surplus, nothing can be reclaimed.
624
- if (currentSurplus == 0) return 0;
625
-
626
- // Get the project token's total supply.
627
- uint256 totalSupply =
628
- IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
629
-
630
- // Can't cash out more tokens than are in the total supply.
631
- if (cashOutCount > totalSupply) return 0;
632
-
633
- // Get the cash out tax rate from the current ruleset.
634
- uint256 cashOutTaxRate = RULESETS.currentOf(projectId).cashOutTaxRate();
635
-
636
- // Return the amount of surplus terminal tokens that would be reclaimed.
637
- return JBCashOuts.cashOutFrom({
638
- surplus: currentSurplus,
639
- cashOutCount: cashOutCount,
640
- totalSupply: totalSupply,
641
- cashOutTaxRate: cashOutTaxRate
642
- });
643
- }
644
-
645
594
  /// @notice Gets the current surplus amount for a project across specified terminals and tokens.
646
595
  /// @param projectId The ID of the project to get surplus for.
647
596
  /// @param terminals The terminals to include. If empty, all project terminals are used.
@@ -684,7 +633,7 @@ contract JBTerminalStore is IJBTerminalStore {
684
633
  override
685
634
  returns (uint256)
686
635
  {
687
- return this.currentReclaimableSurplusOf({
636
+ return currentReclaimableSurplusOf({
688
637
  projectId: projectId,
689
638
  cashOutCount: cashOutCount,
690
639
  terminals: new IJBTerminal[](0),
@@ -797,6 +746,61 @@ contract JBTerminalStore is IJBTerminalStore {
797
746
  });
798
747
  }
799
748
 
749
+ //*********************************************************************//
750
+ // -------------------------- public views --------------------------- //
751
+ //*********************************************************************//
752
+
753
+ /// @notice Returns the number of surplus terminal tokens that would be reclaimed from terminals by cashing out a
754
+ /// given number of tokens, considering only specific tokens.
755
+ /// @param projectId The ID of the project whose tokens would be cashed out.
756
+ /// @param cashOutCount The number of tokens that would be cashed out, as a fixed point number with 18 decimals.
757
+ /// @param terminals The terminals that would be cashed out from. If this is an empty array, surplus within all
758
+ /// the project's terminals are considered.
759
+ /// @param tokens The tokens to include in the surplus calculation.
760
+ /// @param decimals The number of decimals to include in the resulting fixed point number.
761
+ /// @param currency The currency that the resulting number will be in terms of.
762
+ /// @return The amount of surplus terminal tokens that would be reclaimed by cashing out `cashOutCount`
763
+ /// tokens.
764
+ function currentReclaimableSurplusOf(
765
+ uint256 projectId,
766
+ uint256 cashOutCount,
767
+ IJBTerminal[] memory terminals,
768
+ address[] memory tokens,
769
+ uint256 decimals,
770
+ uint256 currency
771
+ )
772
+ public
773
+ view
774
+ override
775
+ returns (uint256)
776
+ {
777
+ // Aggregate surplus across the terminals, optionally filtered by the specified tokens.
778
+ uint256 currentSurplus = _currentSurplusOf({
779
+ projectId: projectId, terminals: terminals, tokens: tokens, decimals: decimals, currency: currency
780
+ });
781
+
782
+ // If there's no surplus, nothing can be reclaimed.
783
+ if (currentSurplus == 0) return 0;
784
+
785
+ // Get the project token's total supply.
786
+ uint256 totalSupply =
787
+ IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
788
+
789
+ // Can't cash out more tokens than are in the total supply.
790
+ if (cashOutCount > totalSupply) return 0;
791
+
792
+ // Get the cash out tax rate from the current ruleset.
793
+ uint256 cashOutTaxRate = RULESETS.currentOf(projectId).cashOutTaxRate();
794
+
795
+ // Return the amount of surplus terminal tokens that would be reclaimed.
796
+ return JBCashOuts.cashOutFrom({
797
+ surplus: currentSurplus,
798
+ cashOutCount: cashOutCount,
799
+ totalSupply: totalSupply,
800
+ cashOutTaxRate: cashOutTaxRate
801
+ });
802
+ }
803
+
800
804
  //*********************************************************************//
801
805
  // -------------------------- internal views ------------------------- //
802
806
  //*********************************************************************//
package/src/JBTokens.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
5
5
 
@@ -398,6 +398,10 @@ contract JBTokens is JBControlled, IJBTokens {
398
398
  //*********************************************************************//
399
399
 
400
400
  /// @notice The total supply for a specific project, including both tokens and token credits.
401
+ /// @dev WARNING: Projects that use `setTokenFor` with an external ERC-20 inherit that token's supply manipulation
402
+ /// surface. If the external token has a separate minting authority, `totalSupply()` can be inflated outside of
403
+ /// this contract, which dilutes cash-out values for all holders. Projects using `deployERC20For` are safe because
404
+ /// the resulting `JBERC20` is exclusively owned by this `JBTokens` contract.
401
405
  /// @param projectId The ID of the project to get the total supply of.
402
406
  /// @return totalSupply The total supply of the project's tokens and token credits.
403
407
  function totalSupplyOf(uint256 projectId) public view override returns (uint256 totalSupply) {
@@ -251,7 +251,13 @@ interface IJBController is IERC165, IJBProjectUriRegistry, IJBDirectoryAccessCon
251
251
  /// @param pricingCurrency The currency the feed's output price is in terms of.
252
252
  /// @param unitCurrency The currency being priced by the feed.
253
253
  /// @param feed The price feed to add.
254
- function addPriceFeed(uint256 projectId, uint256 pricingCurrency, uint256 unitCurrency, IJBPriceFeed feed) external;
254
+ function addPriceFeedFor(
255
+ uint256 projectId,
256
+ uint256 pricingCurrency,
257
+ uint256 unitCurrency,
258
+ IJBPriceFeed feed
259
+ )
260
+ external;
255
261
 
256
262
  /// @notice Burns a holder's project tokens or credits.
257
263
  /// @param holder The address whose tokens are being burned.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {mulDiv} from "@prb/math/src/Common.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBDeadline} from "../JBDeadline.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBDeadline} from "../JBDeadline.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBDeadline} from "../JBDeadline.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBDeadline} from "../JBDeadline.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBPriceFeed} from "src/interfaces/IJBPriceFeed.sol";
5
5
 
@@ -1221,14 +1221,14 @@ contract TestAccessToFunds_Local is TestBaseWorkflow {
1221
1221
  MockPriceFeed _priceFeedNativeUsd = new MockPriceFeed(_USD_PRICE_PER_NATIVE, _PRICE_FEED_DECIMALS);
1222
1222
  vm.label(address(_priceFeedNativeUsd), "Mock Price Feed Native-USDC");
1223
1223
 
1224
- _controller.addPriceFeed({
1224
+ _controller.addPriceFeedFor({
1225
1225
  projectId: 1,
1226
1226
  pricingCurrency: uint32(uint160(address(_usdcToken))),
1227
1227
  unitCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
1228
1228
  feed: _priceFeedNativeUsd
1229
1229
  });
1230
1230
 
1231
- _controller.addPriceFeed({
1231
+ _controller.addPriceFeedFor({
1232
1232
  projectId: 2,
1233
1233
  pricingCurrency: uint32(uint160(address(_usdcToken))),
1234
1234
  unitCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
@@ -1898,14 +1898,14 @@ contract TestAccessToFunds_Local is TestBaseWorkflow {
1898
1898
  MockPriceFeed _priceFeedNativeUsd = new MockPriceFeed(_USD_PRICE_PER_NATIVE, _PRICE_FEED_DECIMALS);
1899
1899
  vm.label(address(_priceFeedNativeUsd), "Mock Price Feed Native-USDC");
1900
1900
 
1901
- _controller.addPriceFeed({
1901
+ _controller.addPriceFeedFor({
1902
1902
  projectId: 1,
1903
1903
  pricingCurrency: uint32(uint160(address(_usdcToken))),
1904
1904
  unitCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
1905
1905
  feed: _priceFeedNativeUsd
1906
1906
  });
1907
1907
 
1908
- _controller.addPriceFeed({
1908
+ _controller.addPriceFeedFor({
1909
1909
  projectId: 2,
1910
1910
  pricingCurrency: uint32(uint160(address(_usdcToken))),
1911
1911
  unitCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),