@bananapus/core-v6 0.0.26 → 0.0.28

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 (169) hide show
  1. package/ADMINISTRATION.md +1 -1
  2. package/AUDIT_INSTRUCTIONS.md +3 -3
  3. package/CHANGE_LOG.md +17 -2
  4. package/README.md +1 -1
  5. package/RISKS.md +3 -3
  6. package/SKILLS.md +1 -1
  7. package/STYLE_GUIDE.md +2 -2
  8. package/USER_JOURNEYS.md +6 -3
  9. package/foundry.toml +1 -1
  10. package/package.json +5 -5
  11. package/script/Deploy.s.sol +1 -1
  12. package/script/DeployPeriphery.s.sol +1 -1
  13. package/script/helpers/CoreDeploymentLib.sol +1 -1
  14. package/src/JBChainlinkV3PriceFeed.sol +1 -1
  15. package/src/JBChainlinkV3SequencerPriceFeed.sol +1 -1
  16. package/src/JBController.sol +1 -1
  17. package/src/JBDeadline.sol +1 -1
  18. package/src/JBDirectory.sol +1 -1
  19. package/src/JBERC20.sol +1 -1
  20. package/src/JBFeelessAddresses.sol +1 -1
  21. package/src/JBFundAccessLimits.sol +1 -1
  22. package/src/JBMultiTerminal.sol +41 -13
  23. package/src/JBPermissions.sol +1 -1
  24. package/src/JBPrices.sol +1 -1
  25. package/src/JBProjects.sol +1 -1
  26. package/src/JBRulesets.sol +1 -1
  27. package/src/JBSplits.sol +1 -1
  28. package/src/JBTerminalStore.sol +1 -1
  29. package/src/JBTokens.sol +1 -1
  30. package/src/libraries/JBPayoutSplitGroupLib.sol +1 -1
  31. package/src/periphery/JBDeadline1Day.sol +1 -1
  32. package/src/periphery/JBDeadline3Days.sol +1 -1
  33. package/src/periphery/JBDeadline3Hours.sol +1 -1
  34. package/src/periphery/JBDeadline7Days.sol +1 -1
  35. package/src/periphery/JBMatchingPriceFeed.sol +1 -1
  36. package/test/TestFees.sol +6 -4
  37. package/test/TestJBERC20Inheritance.sol +1 -1
  38. package/test/TestMetadataOffsetOverflow.sol +1 -1
  39. package/test/TestMetadataParserLib.sol +1 -1
  40. package/test/TestTerminalMigration.sol +104 -2
  41. package/test/TestTerminalPreviewParity.sol +1 -1
  42. package/test/audit/FeeFreeSurplusLifecycle.t.sol +11 -4
  43. package/test/audit/FeeFreeSurplusStale.t.sol +11 -5
  44. package/test/audit/USDTVoidReturnCompat.t.sol +6 -0
  45. package/test/fork/TestChainlinkPriceFeedFork.sol +1 -1
  46. package/test/fork/TestSequencerPriceFeedFork.sol +1 -1
  47. package/test/fork/TestTerminalPreviewParityFork.sol +1 -1
  48. package/test/helpers/JBTest.sol +1 -1
  49. package/test/helpers/MetadataResolverHelper.sol +1 -1
  50. package/test/mock/MockERC20.sol +1 -1
  51. package/test/mock/MockMaliciousBeneficiary.sol +1 -1
  52. package/test/mock/MockMaliciousSplitHook.sol +1 -1
  53. package/test/mock/MockPriceFeed.sol +1 -1
  54. package/test/mock/MockUSDT.sol +1 -1
  55. package/test/units/static/JBChainlinkV3PriceFeed/TestPriceFeed.sol +1 -1
  56. package/test/units/static/JBController/JBControllerSetup.sol +1 -1
  57. package/test/units/static/JBController/TestBurnTokensOf.sol +1 -1
  58. package/test/units/static/JBController/TestClaimTokensFor.sol +1 -1
  59. package/test/units/static/JBController/TestDeployErc20For.sol +1 -1
  60. package/test/units/static/JBController/TestLaunchProjectFor.sol +1 -1
  61. package/test/units/static/JBController/TestLaunchRulesetsFor.sol +1 -1
  62. package/test/units/static/JBController/TestMigrateController.sol +1 -1
  63. package/test/units/static/JBController/TestMintTokensOfUnits.sol +1 -1
  64. package/test/units/static/JBController/TestOmnichainRulesetOperator.sol +1 -1
  65. package/test/units/static/JBController/TestPayReservedTokenToTerminal.sol +1 -1
  66. package/test/units/static/JBController/TestPreviewMintOf.sol +1 -1
  67. package/test/units/static/JBController/TestReceiveMigrationFrom.sol +1 -1
  68. package/test/units/static/JBController/TestRulesetViews.sol +1 -1
  69. package/test/units/static/JBController/TestSendReservedTokensToSplitsOf.sol +1 -1
  70. package/test/units/static/JBController/TestSetSplitGroupsOf.sol +1 -1
  71. package/test/units/static/JBController/TestSetTokenFor.sol +1 -1
  72. package/test/units/static/JBController/TestSetUriOf.sol +1 -1
  73. package/test/units/static/JBController/TestTransferCreditsFrom.sol +1 -1
  74. package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +1 -1
  75. package/test/units/static/JBDirectory/JBDirectorySetup.sol +1 -1
  76. package/test/units/static/JBDirectory/TestPrimaryTerminalOf.sol +1 -1
  77. package/test/units/static/JBDirectory/TestSetControllerOf.sol +1 -1
  78. package/test/units/static/JBDirectory/TestSetControllerOfMigrationOrder.sol +1 -1
  79. package/test/units/static/JBDirectory/TestSetPrimaryTerminalOf.sol +1 -1
  80. package/test/units/static/JBDirectory/TestSetTerminalsOf.sol +1 -1
  81. package/test/units/static/JBERC20/JBERC20Setup.sol +1 -1
  82. package/test/units/static/JBERC20/SigUtils.sol +1 -1
  83. package/test/units/static/JBERC20/TestInitialize.sol +1 -1
  84. package/test/units/static/JBERC20/TestName.sol +1 -1
  85. package/test/units/static/JBERC20/TestNonces.sol +1 -1
  86. package/test/units/static/JBERC20/TestSymbol.sol +1 -1
  87. package/test/units/static/JBFeelessAdresses/JBFeelessSetup.sol +1 -1
  88. package/test/units/static/JBFeelessAdresses/TestInterfaces.sol +1 -1
  89. package/test/units/static/JBFeelessAdresses/TestSetFeelessAddress.sol +1 -1
  90. package/test/units/static/JBFees/TestFeesFuzz.sol +1 -1
  91. package/test/units/static/JBFixedPointNumber/TestAdjustDecimals.sol +1 -1
  92. package/test/units/static/JBFixedPointNumber/TestAdjustDecimalsFuzz.sol +1 -1
  93. package/test/units/static/JBFundAccessLimits/JBFundAccessSetup.sol +1 -1
  94. package/test/units/static/JBFundAccessLimits/TestFundAccessLimitsEdge.sol +1 -1
  95. package/test/units/static/JBFundAccessLimits/TestPayoutLimitOf.sol +1 -1
  96. package/test/units/static/JBFundAccessLimits/TestPayoutLimitsOf.sol +1 -1
  97. package/test/units/static/JBFundAccessLimits/TestSetFundAccessLimitsFor.sol +1 -1
  98. package/test/units/static/JBFundAccessLimits/TestSurplusAllowanceOf.sol +1 -1
  99. package/test/units/static/JBFundAccessLimits/TestSurplusAllowancesOf.sol +1 -1
  100. package/test/units/static/JBMetadataResolver/TestGetDataFor.sol +1 -1
  101. package/test/units/static/JBMetadataResolver/TestMetadataResolverEdgeCases.sol +1 -1
  102. package/test/units/static/JBMetadataResolver/TestMetadataResolverFuzz.sol +1 -1
  103. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +1 -1
  104. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +1 -1
  105. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +1 -1
  106. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +1 -1
  107. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +1 -1
  108. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +3 -42
  109. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +1 -1
  110. package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +16 -1
  111. package/test/units/static/JBMultiTerminal/TestPay.sol +1 -1
  112. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +1 -1
  113. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +1 -1
  114. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +1 -1
  115. package/test/units/static/JBMultiTerminal/TestSelfPayRevert.sol +55 -0
  116. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +1 -1
  117. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +1 -1
  118. package/test/units/static/JBPermissions/JBPermissionsSetup.sol +1 -1
  119. package/test/units/static/JBPermissions/TestHasPermission.sol +1 -1
  120. package/test/units/static/JBPermissions/TestHasPermissions.sol +1 -1
  121. package/test/units/static/JBPermissions/TestSetPermissionsFor.sol +1 -1
  122. package/test/units/static/JBPrices/JBPricesSetup.sol +1 -1
  123. package/test/units/static/JBPrices/TestAddPriceFeedFor.sol +1 -1
  124. package/test/units/static/JBPrices/TestPricePerUnitOf.sol +1 -1
  125. package/test/units/static/JBPrices/TestPrices.sol +1 -1
  126. package/test/units/static/JBProjects/JBProjectsSetup.sol +1 -1
  127. package/test/units/static/JBProjects/TestCreateFor.sol +1 -1
  128. package/test/units/static/JBProjects/TestInitialProject.sol +1 -1
  129. package/test/units/static/JBProjects/TestInterfaces.sol +1 -1
  130. package/test/units/static/JBProjects/TestSetResolver.sol +1 -1
  131. package/test/units/static/JBProjects/TestTokenUri.sol +1 -1
  132. package/test/units/static/JBRulesetMetadataResolver/TestSetCashOutTaxRateTo.sol +1 -1
  133. package/test/units/static/JBRulesets/JBRulesetsSetup.sol +1 -1
  134. package/test/units/static/JBRulesets/TestCurrentApprovalStatusForLatestRulesetOf.sol +1 -1
  135. package/test/units/static/JBRulesets/TestCurrentOf.sol +1 -1
  136. package/test/units/static/JBRulesets/TestGetRulesetOf.sol +1 -1
  137. package/test/units/static/JBRulesets/TestLatestQueuedRulesetOf.sol +1 -1
  138. package/test/units/static/JBRulesets/TestRulesets.sol +1 -1
  139. package/test/units/static/JBRulesets/TestRulesetsOf.sol +1 -1
  140. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +1 -1
  141. package/test/units/static/JBRulesets/TestUpdateRulesetWeightCache.sol +1 -1
  142. package/test/units/static/JBSplits/JBSplitsSetup.sol +1 -1
  143. package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +1 -1
  144. package/test/units/static/JBSplits/TestSetSplitGroupsOf.sol +1 -1
  145. package/test/units/static/JBSplits/TestSplitsLockedEdge.sol +1 -1
  146. package/test/units/static/JBSplits/TestSplitsOf.sol +1 -1
  147. package/test/units/static/JBSplits/TestSplitsPacking.sol +1 -1
  148. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +1 -1
  149. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +1 -1
  150. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +1 -1
  151. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +1 -1
  152. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +1 -1
  153. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +1 -1
  154. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +1 -1
  155. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +1 -1
  156. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +1 -1
  157. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +1 -1
  158. package/test/units/static/JBTerminalStore/TestRecordTerminalMigration.sol +1 -1
  159. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +1 -1
  160. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +1 -1
  161. package/test/units/static/JBTokens/JBTokensSetup.sol +1 -1
  162. package/test/units/static/JBTokens/TestBurnFrom.sol +1 -1
  163. package/test/units/static/JBTokens/TestClaimTokensFor.sol +1 -1
  164. package/test/units/static/JBTokens/TestDeployERC20ForUnits.sol +1 -1
  165. package/test/units/static/JBTokens/TestMintFor.sol +1 -1
  166. package/test/units/static/JBTokens/TestSetTokenFor.sol +1 -1
  167. package/test/units/static/JBTokens/TestTotalBalanceOf.sol +1 -1
  168. package/test/units/static/JBTokens/TestTotalSupplyOf.sol +1 -1
  169. package/test/units/static/JBTokens/TestTransferCreditsFrom.sol +1 -1
package/ADMINISTRATION.md CHANGED
@@ -123,7 +123,7 @@ Admin privileges and their scope in nana-core-v6.
123
123
  |----------|--------------|---------------|-------|-------------|
124
124
  | `addAccountingContextsFor` | Project owner, operator, or the project's controller | ADD_ACCOUNTING_CONTEXTS (20) | Per project | Adds tokens that the terminal will accept for a project. Requires the ruleset's `allowAddAccountingContext` flag (if a ruleset exists). |
125
125
  | `cashOutTokensOf` | Token holder or operator | CASH_OUT_TOKENS (4) | Per project | Cashes out project tokens for a share of the project's surplus. Fees are charged unless the beneficiary is feeless or the cash out tax rate is zero. |
126
- | `migrateBalanceOf` | Project owner or operator | MIGRATE_TERMINAL (6) | Per project | Migrates a project's balance from this terminal to another. The destination terminal must accept the same token. The ruleset must have `allowTerminalMigration` enabled (checked in JBTerminalStore). |
126
+ | `migrateBalanceOf` | Project owner or operator | MIGRATE_TERMINAL (6) | Per project | Migrates a project's balance from this terminal to another. The destination terminal must accept the same token. The ruleset must have `allowTerminalMigration` enabled (checked in JBTerminalStore). The standard 2.5% protocol fee is charged when migrating to a non-feeless terminal. |
127
127
  | `sendPayoutsOf` | Anyone (unless `ownerMustSendPayouts` is set) | SEND_PAYOUTS (5) if `ownerMustSendPayouts` | Per project | Sends payouts to the project's payout split group up to the payout limit. Anyone can call unless the ruleset has `ownerMustSendPayouts` enabled, which requires the project owner or an operator with SEND_PAYOUTS permission. |
128
128
  | `useAllowanceOf` | Project owner or operator | USE_ALLOWANCE (17) | Per project | Withdraws funds from the project's surplus up to the surplus allowance. Fees are charged unless the owner or beneficiary is feeless. |
129
129
  | `pay` | Anyone | N/A | N/A | Pays a project with tokens. No permission required. |
@@ -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, ~8,100 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)
@@ -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. 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.
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 (the migration fee settles this liability).
92
92
 
93
93
  ### Payout Flow (`sendPayoutsOf`)
94
94
 
@@ -420,7 +420,7 @@ The 185 test files cover most flows, but these areas have limited or no coverage
420
420
 
421
421
  ## Compiler and Version Info
422
422
 
423
- - **Solidity**: ^0.8.26
423
+ - **Solidity**: 0.8.28
424
424
  - **EVM target**: Cancun (uses transient storage opcodes)
425
425
  - **Optimizer**: 200 runs (no via-IR)
426
426
  - **Dependencies**: OpenZeppelin 5.x, Solady, forge-std
package/CHANGE_LOG.md CHANGED
@@ -1,6 +1,6 @@
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
4
 
5
5
  ## Summary
6
6
 
@@ -48,6 +48,14 @@ Also in this release:
48
48
  - **`via_ir = true`**: Added to `foundry.toml` to enable the Solidity IR optimizer pipeline, reducing deployed bytecode size (EIP-170 compliance).
49
49
  - Internal helpers extracted: `_accountingContextOf` and `_tokenAmountOf` on `JBMultiTerminal`, `_splitTokenCount` on `JBController`.
50
50
 
51
+ ### 0.6 JBMultiTerminal -- Migration Fee
52
+
53
+ `migrateBalanceOf` now charges the standard 2.5% protocol fee when migrating to a non-feeless terminal, consistent with all other fund egress. This also settles any `_feeFreeSurplusOf` liability that would otherwise be lost on the new terminal. The fee is deducted from the migrated balance before transfer. Feeless terminals are exempt.
54
+
55
+ ### 0.7 JBMultiTerminal -- Self-Pay Revert
56
+
57
+ `_pay` now reverts with `JBMultiTerminal_MintNotAllowed()` when `payer == address(this)`. This prevents same-project intra-terminal payout splits (where `preferAddToBalance == false`) from minting tokens against existing balance without new funds entering the system. The try-catch in `JBPayoutSplitGroupLib` catches this revert and restores the balance via `recordAddedBalanceFor`. Projects that want to mint should do so explicitly via the controller.
58
+
51
59
  ---
52
60
 
53
61
  ## 1. Breaking Changes
@@ -182,6 +190,13 @@ Parameters changed from `memory` to `calldata` for gas efficiency.
182
190
 
183
191
  ## 3. Event Changes
184
192
 
193
+ ### 3.0 Indexer Notes
194
+
195
+ For subgraph migrations, this repo is the protocol-level anchor:
196
+ - when an event signature gains parameters, prefer widening the existing entity schema instead of treating it as an unrelated event stream;
197
+ - preview/noop behavior in core-v6 means some routing diagnostics now come from returned hook specs rather than only from emitted callback events;
198
+ - if your v5 graph correlated protocol actions to hook callbacks only, re-check those assumptions against v6 preview/noop patterns.
199
+
185
200
  ### 3.1 New Events
186
201
 
187
202
  See section 2.2 above.
@@ -375,7 +390,7 @@ No changes.
375
390
 
376
391
  ### 8.10 Solidity Version
377
392
 
378
- All contracts upgraded from `pragma solidity 0.8.23` to `pragma solidity ^0.8.26`.
393
+ All contracts upgraded from `pragma solidity 0.8.23` to `pragma solidity 0.8.28`.
379
394
 
380
395
  ### 8.11 Named Arguments
381
396
 
package/README.md CHANGED
@@ -100,7 +100,7 @@ Projects can migrate between controllers using the `IJBMigratable` interface. Th
100
100
 
101
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.
102
102
 
103
- All contracts use Solidity `^0.8.26`.
103
+ All contracts use Solidity `0.8.28`.
104
104
 
105
105
  ```mermaid
106
106
  graph TD;
package/RISKS.md CHANGED
@@ -52,13 +52,13 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
52
52
  | `processHeldFeesOf` | `delete _heldFeesOf[...][currentIndex]`, `_nextHeldFeeIndexOf` incremented | `_processFee` -> `this.executeProcessFee` -> `terminal.pay` | LOW -- index advanced before external call; re-reads from storage each iteration |
53
53
  | `_sendReservedTokensToSplitsOf` | `pendingReservedTokenBalanceOf` zeroed, tokens minted to controller | Split hooks, terminal payments | LOW -- pending balance cleared before minting prevents double-distribution |
54
54
  | `_useAllowanceOf` | `STORE.recordUsedAllowanceOf` (allowance consumed, balance decremented) | `_takeFeeFrom` (fee payment/holding), `_transferFrom` (beneficiary) | LOW -- allowance consumed before calls |
55
- | `migrateBalanceOf` | `STORE.recordTerminalMigration` (balance zeroed) | `to.addToBalanceOf` | LOW -- balance zeroed before transfer |
55
+ | `migrateBalanceOf` | `STORE.recordTerminalMigration` (balance zeroed), `_takeFeeFrom` (if non-feeless destination) | `to.addToBalanceOf` | LOW -- balance zeroed before transfer, fee deducted before transfer |
56
56
 
57
57
  ### Cross-Function Reentrancy to Explore
58
58
 
59
59
  - **Pay hook -> `cashOutTokensOf`**: After `_pay` mints tokens, a pay hook could call `cashOutTokensOf`. The cash out sees post-payment balance and post-mint supply. Not profitable after fees in tested scenarios, but verify with data hooks that modify weights.
60
60
  - **Cash out hook -> `pay`**: During `_cashOutTokensOf`, after tokens are burned and beneficiary is paid, a cash out hook could call `pay()` adding to the balance. Fees haven't been taken yet at this point. Verify the fee calculation on `amountEligibleForFees` isn't affected.
61
- - **Split hook -> `pay` on same project**: During `sendPayoutsOf`, a split hook receives funds and calls `pay()` on the same project. Payout limit is consumed, but the payment increases balance and mints tokens. The funds came from the project's own balance, so no value creation -- but verify the accounting.
61
+ - **Split hook -> `pay` on same project**: During `sendPayoutsOf`, a split hook receives funds and calls `pay()` on the same project. Payout limit is consumed, but the payment increases balance and mints tokens. The funds came from the project's own balance, so no value creation -- but verify the accounting. Note: `_pay` now reverts with `MintNotAllowed` when `payer == address(this)` to prevent same-project intra-terminal payout splits from minting tokens against existing balance. The try-catch in the split group lib catches this and restores the balance.
62
62
  - **Fee processing -> any re-entry**: `_processFee` uses `this.executeProcessFee` (external call via try-catch). Inside, it calls `terminal.pay()` on project #1. If project #1 has a pay hook that calls back, the fee amount is already deducted.
63
63
 
64
64
  ### Key Backstop
@@ -77,7 +77,7 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
77
77
  ### Migration
78
78
 
79
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.
80
- - **Terminal migration** requires `allowTerminalMigration` in the current ruleset. Held fees are intentionally NOT migrated -- they belong to project #1. Verify that a project owner cannot use migration to escape held fee obligations.
80
+ - **Terminal migration** requires `allowTerminalMigration` in the current ruleset. Held fees are intentionally NOT migrated -- they belong to project #1. Migration to a non-feeless terminal charges the standard 2.5% protocol fee on the full balance, settling any `_feeFreeSurplusOf` liability.
81
81
  - **Directory updates** (`setTerminalsOf`, `setControllerOf`) are gated by `IJBDirectoryAccessControl` checks that read from the current ruleset's metadata flags. If the current ruleset allows these changes, anyone with the appropriate permission can redirect all of a project's fund flows.
82
82
 
83
83
  ### Ruleset Queuing
package/SKILLS.md CHANGED
@@ -417,7 +417,7 @@ Returns whether `addr` is allowed to mint tokens for the project. Called by `JBC
417
417
 
418
418
  ```solidity
419
419
  // SPDX-License-Identifier: MIT
420
- pragma solidity ^0.8.26;
420
+ pragma solidity 0.8.28;
421
421
 
422
422
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
423
423
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
package/STYLE_GUIDE.md CHANGED
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
21
21
 
22
22
  ```solidity
23
23
  // Contracts — pin to exact version
24
- pragma solidity ^0.8.26;
24
+ pragma solidity 0.8.28;
25
25
 
26
26
  // Interfaces, structs, enums — caret for forward compatibility
27
27
  pragma solidity ^0.8.0;
@@ -331,7 +331,7 @@ Standard config across all repos:
331
331
 
332
332
  ```toml
333
333
  [profile.default]
334
- solc = '0.8.26'
334
+ solc = '0.8.28'
335
335
  evm_version = 'cancun'
336
336
  optimizer_runs = 200
337
337
  libs = ["node_modules", "lib"]
package/USER_JOURNEYS.md CHANGED
@@ -300,15 +300,18 @@ All user paths through the Juicebox V6 core protocol. For each journey: entry po
300
300
  - `to` -- Destination terminal
301
301
 
302
302
  **State changes**:
303
- 1. `JBTerminalStore.balanceOf[oldTerminal][projectId][token]` set to 0
304
- 2. Funds transferred to destination terminal via `to.addToBalanceOf()`
305
- 3. Destination terminal records the added balance
303
+ 1. `_feeFreeSurplusOf[projectId][token]` cleared
304
+ 2. `JBTerminalStore.balanceOf[oldTerminal][projectId][token]` set to 0
305
+ 3. If destination is non-feeless: 2.5% protocol fee deducted from balance via `_takeFeeFrom`
306
+ 4. Remaining funds transferred to destination terminal via `to.addToBalanceOf()`
307
+ 5. Destination terminal records the added balance
306
308
 
307
309
  **Events**: `MigrateTerminal(projectId, token, to, amount, caller)`
308
310
 
309
311
  **Edge cases**:
310
312
  - Requires `allowTerminalMigration` in current ruleset
311
313
  - Destination terminal must have accounting context for the token (validated via `accountingContextForTokenOf`)
314
+ - **Standard 2.5% protocol fee** is charged when migrating to a non-feeless terminal, consistent with all other fund egress. This also settles any `_feeFreeSurplusOf` liability.
312
315
  - **Held fees are NOT transferred** -- they remain in the old terminal. Held fees belong to the fee beneficiary (project #1), not the migrating project.
313
316
  - If balance is 0, no transfer occurs
314
317
  - This only migrates one token's balance. Must be called once per token.
package/foundry.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  [profile.default]
2
- solc = '0.8.26'
2
+ solc = '0.8.28'
3
3
  evm_version = 'cancun'
4
4
  optimizer_runs = 200
5
5
  libs = ["node_modules", "lib"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,14 +26,14 @@
26
26
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
27
27
  },
28
28
  "dependencies": {
29
- "@bananapus/address-registry-v6": "^0.0.14",
30
- "@bananapus/permission-ids-v6": "^0.0.12",
31
- "@chainlink/contracts": "^1.3.0",
29
+ "@bananapus/address-registry-v6": "^0.0.16",
30
+ "@bananapus/permission-ids-v6": "^0.0.14",
31
+ "@chainlink/contracts": "^1.5.0",
32
32
  "@openzeppelin/contracts": "^5.6.1",
33
33
  "@prb/math": "^4.1.1",
34
34
  "@uniswap/permit2": "github:Uniswap/permit2"
35
35
  },
36
36
  "devDependencies": {
37
- "@sphinx-labs/plugins": "^0.33.2"
37
+ "@sphinx-labs/plugins": "^0.33.3"
38
38
  }
39
39
  }
@@ -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 {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
5
5
  import {Script} from "forge-std/Script.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 {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
5
5
  import {Script} from "forge-std/Script.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 {stdJson} from "forge-std/Script.sol";
5
5
  import {Vm} from "forge-std/Vm.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 {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.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 {AggregatorV2V3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV2V3Interface.sol";
5
5
  import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.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";
@@ -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 {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.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 {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
5
5
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
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";
@@ -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";
@@ -59,6 +59,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
59
59
  //*********************************************************************//
60
60
 
61
61
  error JBMultiTerminal_FeeTerminalNotFound(address token);
62
+ error JBMultiTerminal_MintNotAllowed();
62
63
  error JBMultiTerminal_NoMsgValueAllowed(uint256 value);
63
64
  error JBMultiTerminal_OverflowAlert(uint256 value, uint256 limit);
64
65
  error JBMultiTerminal_PermitAllowanceNotEnough(uint256 amount, uint256 allowance);
@@ -386,6 +387,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
386
387
  metadata: metadata
387
388
  });
388
389
  } else {
390
+ // Revert if this is a self-referencing payout (project paying itself via a split).
391
+ // Same-project pay splits would mint tokens against existing balance without new funds entering.
392
+ // Projects that want to mint should do so explicitly via the controller.
393
+ // Cross-project pay splits on the same terminal are allowed (different project receives the funds).
394
+ // The try-catch in the split group lib catches this revert and restores the balance.
395
+ if (terminal == this && split.projectId == projectId) {
396
+ revert JBMultiTerminal_MintNotAllowed();
397
+ }
398
+
389
399
  // Keep a reference to the beneficiary of the payment.
390
400
  address beneficiary = split.beneficiary != address(0) ? split.beneficiary : originalMessageSender;
391
401
 
@@ -492,12 +502,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
492
502
  revert JBMultiTerminal_TerminalTokensIncompatible({projectId: projectId, token: token, terminal: to});
493
503
  }
494
504
 
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.
505
+ // Clear fee-free surplus tracking the fee-free liability is settled by the migration fee below.
497
506
  delete _feeFreeSurplusOf[projectId][token];
498
507
 
499
508
  // Terminal migration intentionally does not transfer held fees. Held fees belong to the
500
509
  // fee beneficiary (project #1), not the migrating project. They unlock after 28 days regardless of terminal.
510
+ // After migration, `processHeldFeesOf()` on this terminal still works — it reads from `_heldFeesOf` and
511
+ // sends fees to the fee project terminal. The migrated project's balance on this terminal is zero, but held
512
+ // fees are backed by the terminal's own token balance (not the project's recorded balance).
501
513
  // Record the migration in the store.
502
514
  // slither-disable-next-line reentrancy-events
503
515
  balance = STORE.recordTerminalMigration({projectId: projectId, token: token});
@@ -506,17 +518,33 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
506
518
 
507
519
  // Transfer the balance if needed.
508
520
  if (balance != 0) {
521
+ // Migration to a non-feeless terminal incurs the standard 2.5% fee, same as any other fund egress.
522
+ // This also settles any fee-free surplus liability that would otherwise be lost on the new terminal.
523
+ uint256 feeAmount;
524
+ if (!_isFeeless(address(to)) && projectId != _FEE_BENEFICIARY_PROJECT_ID) {
525
+ feeAmount = _takeFeeFrom({
526
+ projectId: projectId,
527
+ token: token,
528
+ amount: balance,
529
+ beneficiary: payable(_ownerOf(projectId)),
530
+ shouldHoldFees: false
531
+ });
532
+ }
533
+
534
+ // Transfer the balance minus the fee to the new terminal.
535
+ uint256 migrationAmount = balance - feeAmount;
536
+
509
537
  // Trigger any inherited pre-transfer logic.
510
538
  // If this terminal's token is the native token, send it in `msg.value`.
511
539
  // slither-disable-next-line reentrancy-events
512
- uint256 payValue = _beforeTransferTo({to: address(to), token: token, amount: balance});
540
+ uint256 payValue = _beforeTransferTo({to: address(to), token: token, amount: migrationAmount});
513
541
 
514
- // Withdraw the balance to transfer to the new terminal;
542
+ // Withdraw the balance to transfer to the new terminal.
515
543
  // slither-disable-next-line reentrancy-events
516
544
  to.addToBalanceOf{value: payValue}({
517
545
  projectId: projectId,
518
546
  token: token,
519
- amount: balance,
547
+ amount: migrationAmount,
520
548
  shouldReturnHeldFees: false,
521
549
  memo: "",
522
550
  metadata: bytes("")
@@ -584,13 +612,13 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
584
612
  /// @notice Process any fees that are being held for the project.
585
613
  /// @dev Reentrancy safety: the loop re-reads `_nextHeldFeeIndexOf` from storage each iteration and advances the
586
614
  /// 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.
615
+ /// @dev Held fees after migration: held fees remain in this terminal after `migrateBalanceOf` because their backing
616
+ /// tokens are not part of `balanceOf` they were already deducted from the recorded balance during the payout
617
+ /// that
618
+ /// created them. The actual fee-backing tokens remain in this terminal's token holdings. If `_processFee` reverts
619
+ /// (e.g. the fee terminal rejects the payment), the catch block calls `_recordAddedBalanceFor` to credit the fee
620
+ /// amount back to the project. This credit is backed by the tokens that failed to transfer out. No phantom balance
621
+ /// is created because the tokens never left.
594
622
  /// @dev The index-increment-before-`_processFee` pattern is intentional: locked (not-yet-unlocked) fees are skipped
595
623
  /// via the `unlockTimestamp` check, and advancing the index before the external call prevents reentrancy from
596
624
  /// reprocessing the same fee entry.
@@ -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";
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";
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
 
@@ -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
 
package/test/TestFees.sol CHANGED
@@ -216,8 +216,9 @@ contract TestFees_Local is TestBaseWorkflow {
216
216
  // Send: Migration to terminal2
217
217
  _terminal.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, _terminal2);
218
218
 
219
- // Check: Held Fee is processed and feeAmount remains in terminal
220
- assertEq(address(_terminal).balance, _feeAmount);
219
+ // Check: Held fee remains in terminal, plus migration fee paid to fee project (on same terminal)
220
+ uint256 _migrationFee = _nativeDistLimit * _terminal.FEE() / JBConstants.MAX_FEE;
221
+ assertEq(address(_terminal).balance, _feeAmount + _migrationFee);
221
222
 
222
223
  vm.stopPrank();
223
224
  }
@@ -265,8 +266,9 @@ contract TestFees_Local is TestBaseWorkflow {
265
266
  // Send: Migration to terminal2
266
267
  _terminal.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, _terminal2);
267
268
 
268
- // Check: Held fee has been repaid, no balance remains.
269
- assertEq(address(_terminal).balance, 0);
269
+ // Check: Held fee has been repaid. Migration fee paid to fee project remains on this terminal.
270
+ uint256 _migrationFee = _nativePayAmount * _terminal.FEE() / JBConstants.MAX_FEE;
271
+ assertEq(address(_terminal).balance, _migrationFee);
270
272
 
271
273
  vm.stopPrank();
272
274
  }
@@ -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 {TestBaseWorkflow} from "./helpers/TestBaseWorkflow.sol";
5
5
  import {JBERC20} from "../src/JBERC20.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 {Test} from "forge-std/Test.sol";
5
5
  import {MetadataResolverHelper} from "./helpers/MetadataResolverHelper.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 {Test} from "forge-std/Test.sol";
5
5
  import {MetadataResolverHelper} from "./helpers/MetadataResolverHelper.sol";