@bananapus/core-v6 0.0.18 → 0.0.20

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 (46) hide show
  1. package/ADMINISTRATION.md +3 -0
  2. package/ARCHITECTURE.md +24 -0
  3. package/AUDIT_INSTRUCTIONS.md +4 -2
  4. package/CHANGE_LOG.md +29 -1
  5. package/README.md +12 -2
  6. package/RISKS.md +10 -2
  7. package/SKILLS.md +9 -0
  8. package/USER_JOURNEYS.md +6 -0
  9. package/foundry.toml +1 -0
  10. package/package.json +1 -1
  11. package/src/JBController.sol +52 -5
  12. package/src/JBMultiTerminal.sol +197 -179
  13. package/src/JBTerminalStore.sol +367 -171
  14. package/src/interfaces/IJBCashOutTerminal.sol +30 -0
  15. package/src/interfaces/IJBController.sol +15 -0
  16. package/src/interfaces/IJBTerminal.sol +28 -0
  17. package/src/interfaces/IJBTerminalStore.sol +66 -0
  18. package/src/libraries/JBPayoutSplitGroupLib.sol +157 -0
  19. package/src/structs/JBCashOutHookSpecification.sol +2 -0
  20. package/src/structs/JBPayHookSpecification.sol +2 -0
  21. package/test/CoreExploitTests.t.sol +21 -10
  22. package/test/TestCashOutHooks.sol +6 -4
  23. package/test/TestDataHookFuzzing.sol +6 -2
  24. package/test/TestPayHooks.sol +1 -1
  25. package/test/TestRulesetQueueing.sol +4 -5
  26. package/test/TestRulesetQueuingStress.sol +5 -3
  27. package/test/TestTerminalPreviewParity.sol +208 -0
  28. package/test/fork/TestSequencerPriceFeedFork.sol +168 -0
  29. package/test/fork/TestTerminalPreviewParityFork.sol +109 -0
  30. package/test/units/static/JBController/TestPreviewMintOf.sol +116 -0
  31. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +144 -25
  32. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +11 -1
  33. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +15 -2
  34. package/test/units/static/JBMultiTerminal/TestPay.sol +64 -2
  35. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +116 -0
  36. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +98 -0
  37. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +11 -2
  38. package/test/units/static/JBRulesets/TestCurrentOf.sol +8 -6
  39. package/test/units/static/JBRulesets/TestRulesets.sol +25 -24
  40. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +4 -17
  41. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +49 -2
  42. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +215 -0
  43. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +475 -0
  44. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +464 -0
  45. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +113 -2
  46. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +227 -5
package/ADMINISTRATION.md CHANGED
@@ -115,6 +115,7 @@ Admin privileges and their scope in nana-core-v6.
115
115
  | `beforeReceiveMigrationFrom` | JBDirectory only | N/A (msg.sender == DIRECTORY) | Per project | Called before migration to prepare the new controller. Copies metadata URI and distributes pending reserved tokens from the old controller. |
116
116
  | `afterReceiveMigrationFrom` | JBDirectory only | N/A (msg.sender == DIRECTORY) | Per project | Called after migration completes. Currently a no-op. |
117
117
  | `executePayReservedTokenToTerminal` | Self only | N/A (msg.sender == address(this)) | Internal | Pays a terminal with reserved tokens. Called internally via try-catch during reserved token distribution. |
118
+ | `previewMintOf` | Anyone | N/A (view) | Per project | Simulates a mint under the current ruleset and returns the beneficiary and reserved token counts. No state modification. |
118
119
 
119
120
  ### JBMultiTerminal
120
121
 
@@ -131,6 +132,8 @@ Admin privileges and their scope in nana-core-v6.
131
132
  | `executePayout` | Self only | N/A (msg.sender == address(this)) | Internal | Executes a single payout to a split. Called internally via try-catch during payout distribution. |
132
133
  | `executeProcessFee` | Self only | N/A (msg.sender == address(this)) | Internal | Processes a fee payment to the fee beneficiary project. Called internally via try-catch. |
133
134
  | `executeTransferTo` | Self only | N/A (msg.sender == address(this)) | Internal | Transfers tokens to an address. Called internally via try-catch during payout leftover distribution. |
135
+ | `previewPayFor` | Anyone | N/A (view) | Per project | Simulates a payment and returns the ruleset, beneficiary token count, reserved token count, and hook specifications. No state modification. |
136
+ | `previewCashOutFrom` | Anyone | N/A (view) | Per project | Simulates a cash out and returns the ruleset, reclaim amount, cash out tax rate, and hook specifications. No state modification. |
134
137
 
135
138
  ### JBTokens
136
139
 
package/ARCHITECTURE.md CHANGED
@@ -72,6 +72,30 @@ Holder -> JBMultiTerminal.cashOutTokensOf()
72
72
  OR if cashOutTaxRate == 0 and project has unconsumed fee-free surplus (_feeFreeSurplusOf)
73
73
  ```
74
74
 
75
+ ### Preview Flow
76
+
77
+ Every core user action has a `view` counterpart that simulates the operation without modifying state. These compose the same internal computation paths as their non-preview counterparts and include data hook effects.
78
+
79
+ ```
80
+ Caller -> JBMultiTerminal.previewPayFor(projectId, token, amount, beneficiary, metadata)
81
+ -> JBTerminalStore.previewPayFrom()
82
+ -> Read current ruleset, apply data hook
83
+ -> Calculate token count from weight
84
+ -> JBController.previewMintOf()
85
+ -> Split token count into beneficiary + reserved portions
86
+ -> Returns (ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications)
87
+
88
+ Caller -> JBMultiTerminal.previewCashOutFrom(holder, projectId, cashOutCount, tokenToReclaim, beneficiary, metadata)
89
+ -> JBTerminalStore.previewCashOutFrom()
90
+ -> Calculate surplus, get totalSupply, apply data hook
91
+ -> JBCashOuts.cashOutFrom() — bonding curve
92
+ -> Returns (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications)
93
+
94
+ Caller -> JBController.previewMintOf(projectId, tokenCount, useReservedPercent)
95
+ -> Read current ruleset
96
+ -> Returns (beneficiaryTokenCount, reservedTokenCount)
97
+ ```
98
+
75
99
  ### Payout Flow
76
100
 
77
101
  ```
@@ -26,8 +26,8 @@ Read [RISKS.md](./RISKS.md) for known risks, trust model, and reentrancy analysi
26
26
 
27
27
  | Contract | Lines | Role | Calls |
28
28
  |----------|-------|------|-------|
29
- | **JBMultiTerminal** | ~2024 | Payment terminal. Handles pay, cash out, payouts, surplus allowance, fees. Multi-token. Permit2 integration. | Store, Controller, Splits, Directory, Prices |
30
- | **JBController** | ~1186 | Orchestrator. Project lifecycle, ruleset queuing, token minting/burning, reserved token distribution. ERC-2771 meta-tx. | Rulesets, Tokens, Splits, FundAccessLimits, Directory, Prices |
29
+ | **JBMultiTerminal** | ~2024 | Payment terminal. Handles pay, cash out, payouts, surplus allowance, fees, and previews (`previewPayFor`, `previewCashOutFrom`). Multi-token. Permit2 integration. | Store, Controller, Splits, Directory, Prices |
30
+ | **JBController** | ~1186 | Orchestrator. Project lifecycle, ruleset queuing, token minting/burning, reserved token distribution, mint preview (`previewMintOf`). ERC-2771 meta-tx. | Rulesets, Tokens, Splits, FundAccessLimits, Directory, Prices |
31
31
  | **JBTerminalStore** | ~800 | Bookkeeping. Balances, payout limit tracking, surplus calculation, bonding curve reclaim math. Data hook integration point. | Rulesets, Prices, Directory |
32
32
  | **JBRulesets** | ~1093 | Ruleset lifecycle. Linked-list via `basedOnId`. Weight decay with cache (20k iteration threshold). Approval hooks. Bit-packed storage. | Directory (via JBControlled) |
33
33
  | **JBDirectory** | ~300 | Routes projects to terminals and controllers. Migration lifecycle (before/after). | Projects, Permissions |
@@ -262,6 +262,8 @@ These are the patterns that will trip you up if you are not aware of them:
262
262
  12. **Credits are burned before ERC-20 tokens** in `JBTokens.burnFrom()`.
263
263
  13. **`JBERC20` is cloned via `Clones.clone()`** -- constructor sets invalid name/symbol; real values set in `initialize()`.
264
264
  14. **Named returns auto-return** -- several functions use named return variables without explicit `return` statements.
265
+ 15. **Preview functions call data hooks** -- `previewPayFor`, `previewCashOutFrom`, and their store-level counterparts invoke data hooks during simulation. A reverting data hook will cause the preview to revert. Preview functions are `view` but still make external calls to hooks.
266
+ 16. **Store preview functions use `msg.sender` as terminal** -- `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` use `msg.sender` (not an explicit parameter) as the terminal context. Only terminal contracts calling these will get correct balance/surplus lookups. Use the terminal-level `JBMultiTerminal.previewPayFor` / `previewCashOutFrom` instead for general-purpose previews.
265
267
 
266
268
  ## Priority Areas to Audit
267
269
 
package/CHANGE_LOG.md CHANGED
@@ -14,6 +14,28 @@ This document describes all changes between `nana-core` (v5, Solidity 0.8.23) an
14
14
 
15
15
  A new `_feeFreeSurplusOf` mapping (`projectId => token => uint256`) tracks cumulative fee-free intra-terminal payouts received by each project. When a split payout lands on the same terminal (intra-terminal routing, i.e. `terminal == this`), the net payout amount is added to `_feeFreeSurplusOf[projectId][token]`. During a cashout with `cashOutTaxRate == 0`, fees are now charged on the reclaim amount up to the tracked fee-free surplus (and the tracker is decremented accordingly). Cashouts beyond the fee-free surplus remain fee-free. This closes a round-trip fee bypass where funds could be routed fee-free into a project via an intra-terminal split payout and then cashed out fee-free via a zero-tax cashout.
16
16
 
17
+ ### 0.4 JBTerminalStore -- Preview Functions
18
+
19
+ Two `view` functions on `JBTerminalStore` and `IJBTerminalStore`:
20
+
21
+ - `previewPayFrom(address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` -- Simulates a payment and returns `(JBRuleset ruleset, uint256 tokenCount, JBPayHookSpecification[] hookSpecifications)`. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Does not modify state.
22
+ - `previewCashOutFrom(address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bool beneficiaryIsFeeless, bytes metadata)` -- Simulates a cash out and returns `(JBRuleset ruleset, uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] hookSpecifications)`. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Does not modify state.
23
+
24
+ Internal computation logic was extracted into shared `_computePayFrom` and `_computeCashOutFrom` view helpers; the existing `recordPaymentFrom` and `recordCashOutFor` functions were refactored to call these helpers before writing state.
25
+
26
+ ### 0.5 Terminal-Level Preview APIs
27
+
28
+ New `view` functions on `JBMultiTerminal`, `JBController`, and their interfaces provide user-facing preview entry points that compose the store-level previews with the mint token split:
29
+
30
+ - `JBMultiTerminal.previewPayFor(uint256 projectId, address token, uint256 amount, address beneficiary, bytes metadata)` -- Simulates a full payment including the reserved/beneficiary token split. Calls `STORE.previewPayFrom` then `controller.previewMintOf`. Returns `(JBRuleset ruleset, uint256 beneficiaryTokenCount, uint256 reservedTokenCount, JBPayHookSpecification[] hookSpecifications)`.
31
+ - `JBMultiTerminal.previewCashOutFrom(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, address beneficiary, bytes metadata)` -- Simulates a full cash out. Resolves accounting context internally and delegates to `STORE.previewCashOutFrom`. Returns `(JBRuleset ruleset, uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] hookSpecifications)`.
32
+ - `JBController.previewMintOf(uint256 projectId, uint256 tokenCount, bool useReservedPercent)` -- Previews how a mint splits between beneficiary and reserved tokens under the current ruleset. Returns `(uint256 beneficiaryTokenCount, uint256 reservedTokenCount)`.
33
+
34
+ Also in this release:
35
+ - **Error consolidation**: Three separate `UnderMin*` errors on `JBMultiTerminal` consolidated into a single `JBMultiTerminal_UnderMin()` error for bytecode size reduction.
36
+ - **`via_ir = true`**: Added to `foundry.toml` to enable the Solidity IR optimizer pipeline, reducing deployed bytecode size (EIP-170 compliance).
37
+ - Internal helpers extracted: `_accountingContextOf` and `_tokenAmountOf` on `JBMultiTerminal`, `_splitTokenCount` on `JBController`.
38
+
17
39
  ### 0.3 JBBeforeCashOutRecordedContext -- beneficiaryIsFeeless Field
18
40
 
19
41
  A `bool beneficiaryIsFeeless` field was added to the `JBBeforeCashOutRecordedContext` struct (before the `metadata` field). `recordCashOutFor` in `IJBTerminalStore` gained a corresponding `bool beneficiaryIsFeeless` parameter. The terminal passes the result of its feeless address check, allowing data hooks to skip their own fees when the beneficiary is already feeless (e.g., project-to-project routing via the router terminal). This is a **breaking change** to both the struct layout and the `recordCashOutFor` function signature.
@@ -91,6 +113,12 @@ Parameters changed from `memory` to `calldata` for gas efficiency.
91
113
  | `setTokenMetadataOf(uint256 projectId, string name, string symbol)` | Sets the name and symbol of a project's ERC-20 token. Requires the `SET_TOKEN_METADATA` permission. |
92
114
  | `afterReceiveMigrationFrom(IERC165 from, uint256 projectId)` | Called by the directory after this controller has been set as the active controller. Added to the `IJBMigratable` interface. |
93
115
 
116
+ #### IJBTerminalStore / JBTerminalStore
117
+
118
+ | Function | Description |
119
+ |----------|-------------|
120
+ | `currentTotalReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 decimals, uint256 currency)` | Convenience view that returns the reclaimable surplus across all terminals using all accounting contexts. Delegates to `currentReclaimableSurplusOf` with empty `terminals` and `accountingContexts` arrays. Mirrors the `currentTotalSurplusOf` pattern. |
121
+
94
122
  #### IJBTokens / JBTokens
95
123
 
96
124
  | Function | Description |
@@ -353,7 +381,7 @@ Throughout the codebase, function calls were updated to use named argument synta
353
381
  |----|----|-------|
354
382
  | `IJBController` | `IJBController` | Gained `setTokenMetadataOf`. `calldata` for terminal configs. |
355
383
  | `IJBRulesets` | `IJBRulesets` | `updateRulesetWeightCache` gained `rulesetId` parameter |
356
- | `IJBTerminalStore` | `IJBTerminalStore` | `tokenCount` renamed to `cashOutCount` in `currentReclaimableSurplusOf`. `recordCashOutFor` gained `beneficiaryIsFeeless` param. View functions reordered. |
384
+ | `IJBTerminalStore` | `IJBTerminalStore` | `tokenCount` renamed to `cashOutCount` in `currentReclaimableSurplusOf`. `recordCashOutFor` gained `beneficiaryIsFeeless` param. New `currentTotalReclaimableSurplusOf` convenience view. View functions reordered. |
357
385
  | `IJBPayoutTerminal` | `IJBPayoutTerminal` | `sendPayoutsOf` returns `amountPaidOut` (was `netLeftoverPayoutAmount`). `SendPayoutToSplit` event moved. |
358
386
  | `IJBPermitTerminal` | `IJBPermitTerminal` | Gained `Permit2AllowanceFailed` event |
359
387
  | `IJBMigratable` | `IJBMigratable` | Gained `afterReceiveMigrationFrom` function |
package/README.md CHANGED
@@ -39,6 +39,16 @@ Credits and tokens can be **cashed out** to reclaim surplus funds along a bondin
39
39
  - A **100% tax rate** means nothing can be reclaimed (all surplus is locked).
40
40
  - Tax rates between 0% and 100% create a bonding curve that incentivizes holding -- later cashers-out get a better rate per token.
41
41
 
42
+ ### Preview APIs
43
+
44
+ Every core user action -- paying, cashing out, and minting -- has a corresponding **preview** function that simulates the operation on-chain without modifying state. These are essential for building UIs with accurate slippage estimates and for integrators who need to know exact outcomes before committing transactions.
45
+
46
+ - `JBMultiTerminal.previewPayFor(projectId, token, amount, beneficiary, metadata)` -- Returns the ruleset, beneficiary token count, reserved token count, and hook specifications that a payment would produce. Includes data hook effects.
47
+ - `JBMultiTerminal.previewCashOutFrom(holder, projectId, cashOutCount, tokenToReclaim, beneficiary, metadata)` -- Returns the ruleset, reclaim amount, cash out tax rate, and hook specifications that a cash out would produce. Includes data hook effects.
48
+ - `JBController.previewMintOf(projectId, tokenCount, useReservedPercent)` -- Returns the beneficiary and reserved token counts that a mint would produce under the current ruleset.
49
+
50
+ All preview functions are `view` -- they read current state (including calling data hooks) but never write. They mirror the exact computation paths of their non-preview counterparts, so the returned values match what the real operation would produce at the same block.
51
+
42
52
  ### Reserved Tokens
43
53
 
44
54
  Each ruleset can define a `reservedPercent` (0-10,000 basis points). When tokens are minted from payments, this percentage is set aside. Reserved tokens accumulate in `pendingReservedTokenBalanceOf` and are distributed to the reserved token split group when `sendReservedTokensToSplitsOf` is called.
@@ -97,8 +107,8 @@ All contracts use Solidity `0.8.26`.
97
107
  | `JBProjects` | ERC-721 registry of projects. Minting an NFT creates a project. Optionally mints project #1 to a fee beneficiary owner. |
98
108
  | `JBPermissions` | Bitmap-based permission system. Accounts grant operators specific permissions scoped to project IDs. Supports ROOT (1) for all-permissions and wildcard project ID (0). |
99
109
  | `JBDirectory` | Maps each project to its controller and terminals. Entry point for looking up where to interact with a project. Manages an allowlist of addresses permitted to set a project's first controller. |
100
- | `JBController` | Coordinates rulesets, tokens, splits, and fund access limits. Entry point for launching projects, queuing rulesets, minting/burning tokens, deploying ERC-20s, updating token metadata, sending reserved tokens, setting project URIs, adding price feeds, and transferring credits. |
101
- | `JBMultiTerminal` | Accepts payments (native ETH and ERC-20s), processes cash outs, distributes payouts, manages surplus allowances, and handles fees. Integrates with Permit2 for ERC-20 approvals. |
110
+ | `JBController` | Coordinates rulesets, tokens, splits, and fund access limits. Entry point for launching projects, queuing rulesets, minting/burning tokens, deploying ERC-20s, updating token metadata, sending reserved tokens, setting project URIs, adding price feeds, transferring credits, and previewing mint outcomes via `previewMintOf`. |
111
+ | `JBMultiTerminal` | Accepts payments (native ETH and ERC-20s), processes cash outs, distributes payouts, manages surplus allowances, and handles fees. Provides `previewPayFor` and `previewCashOutFrom` for simulating operations. Integrates with Permit2 for ERC-20 approvals. |
102
112
  | `JBTerminalStore` | Bookkeeping engine for all terminal inflows and outflows. Tracks balances, enforces payout limits and surplus allowances, computes cash out reclaim amounts via a bonding curve, and integrates with data hooks. |
103
113
  | `JBRulesets` | Stores and manages project rulesets. Handles queuing, cycling, weight decay, approval hook validation, and weight caching for long-running projects. |
104
114
  | `JBTokens` | Manages dual-balance token accounting (credits + ERC-20). Credits are minted by default; once an ERC-20 is deployed or set, credits can be claimed as tokens. Credits are burned before ERC-20 tokens. |
package/RISKS.md CHANGED
@@ -124,7 +124,15 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
124
124
  - `sendPayoutsOf` is callable by anyone (unless `ownerMustSendPayouts` is set). A split recipient that always reverts will cause that split's payout to fail, but the try-catch returns the amount to the project balance. Payout limit is still consumed. The project owner must wait until the next cycle.
125
125
  - `addAccountingContextsFor` is gated by `allowAddAccountingContext` in the ruleset, but the contexts array is append-only and never shrinks. Over many rulesets, this could grow large enough to cause gas issues in functions that iterate over all contexts (e.g., `currentSurplusOf` when no explicit contexts are passed).
126
126
 
127
- ## 6. Integration Risks
127
+ ## 6. Preview Functions
128
+
129
+ `JBMultiTerminal.previewPayFor`, `JBMultiTerminal.previewCashOutFrom`, and `JBController.previewMintOf` are `view` functions that simulate operations without modifying state. They compose the same computation paths as the real operations.
130
+
131
+ - **Data hooks are called during previews.** `previewPayFor` and `previewCashOutFrom` invoke `beforePayRecordedWith` and `beforeCashOutRecordedWith` on data hooks. A reverting data hook causes the preview to revert. A gas-consuming hook can cause the preview to run out of gas.
132
+ - **Store previews use `msg.sender` as terminal.** `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` use `msg.sender` for balance/surplus lookups. Only a registered terminal calling these will get correct results. External callers should use the terminal-level functions (`JBMultiTerminal.previewPayFor` / `previewCashOutFrom`) which handle this automatically.
133
+ - **No state modification risk.** Preview functions cannot change balances, mint/burn tokens, or consume limits. They are safe to call from any context.
134
+
135
+ ## 7. Integration Risks
128
136
 
129
137
  ### Non-Standard ERC-20s
130
138
 
@@ -148,7 +156,7 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
148
156
 
149
157
  - `JBTerminalStore.recordAddedBalanceFor` has **no access control**. Any address can call it. The balance is keyed by `msg.sender` (the terminal address), so only a terminal can inflate its own recorded balance. This is safe as long as all terminals correctly track their actual holdings. A buggy or malicious terminal implementation could call `recordAddedBalanceFor` without actually receiving tokens, inflating the recorded balance above actual holdings.
150
158
 
151
- ## 7. Invariants to Verify
159
+ ## 8. Invariants to Verify
152
160
 
153
161
  These should hold at all times and are the most productive targets for formal verification or invariant testing:
154
162
 
package/SKILLS.md CHANGED
@@ -52,6 +52,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
52
52
  | `allRulesetsOf(uint256 projectId, uint256 startingId, uint256 size)` | Returns an array of rulesets with metadata, paginated. |
53
53
  | `pendingReservedTokenBalanceOf(uint256 projectId)` | Returns accumulated reserved tokens not yet distributed. |
54
54
  | `totalTokenSupplyWithReservedTokensOf(uint256 projectId)` | Returns total supply including pending reserved tokens. |
55
+ | `previewMintOf(uint256 projectId, uint256 tokenCount, bool useReservedPercent)` | Simulates a mint under the current ruleset. Returns `(beneficiaryTokenCount, reservedTokenCount)`. Reverts if `tokenCount` is 0. |
55
56
 
56
57
  ### JBMultiTerminal
57
58
 
@@ -68,6 +69,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
68
69
  | `currentSurplusOf(uint256 projectId, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the project's current surplus in the specified currency. |
69
70
  | `accountingContextForTokenOf(uint256 projectId, address token)` | Returns the accounting context for a specific token. |
70
71
  | `accountingContextsOf(uint256 projectId)` | Returns all accounting contexts for a project. |
72
+ | `previewPayFor(uint256 projectId, address token, uint256 amount, address beneficiary, bytes metadata)` | Simulates a full payment including the reserved/beneficiary token split. Returns `(ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications)`. Composes `STORE.previewPayFrom` + `controller.previewMintOf`. |
73
+ | `previewCashOutFrom(address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, address beneficiary, bytes metadata)` | Simulates a full cash out including bonding curve and data hook effects. Returns `(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications)`. Delegates to `STORE.previewCashOutFrom`. |
71
74
  | `heldFeesOf(uint256 projectId, address token, uint256 count)` | Returns up to `count` held fees for a project/token. |
72
75
 
73
76
  ### JBTerminalStore
@@ -83,7 +86,13 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
83
86
  | `balanceOf(address terminal, uint256 projectId, address token)` | Returns the balance of a project at a terminal for a given token. |
84
87
  | `usedPayoutLimitOf(address terminal, uint256 projectId, address token, uint256 rulesetCycleNumber, uint256 currency)` | Returns the used payout limit for a project in a given cycle. |
85
88
  | `usedSurplusAllowanceOf(address terminal, uint256 projectId, address token, uint256 rulesetId, uint256 currency)` | Returns the used surplus allowance for a project in a given ruleset. |
89
+ | `currentReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 totalSupply, uint256 surplus)` | Returns the reclaimable surplus given raw total supply and surplus values. Applies bonding curve from current ruleset. |
90
+ | `currentReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, IJBTerminal[] terminals, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the reclaimable surplus across specified terminals. Empty arrays default to all terminals/contexts. |
91
+ | `currentTotalReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 decimals, uint256 currency)` | Convenience view: reclaimable surplus across all terminals using all accounting contexts. Mirrors `currentTotalSurplusOf`. |
86
92
  | `currentSurplusOf(address terminal, uint256 projectId, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the current surplus for a project at a terminal. |
93
+ | `currentTotalSurplusOf(uint256 projectId, uint256 decimals, uint256 currency)` | Returns the total surplus across all terminals. |
94
+ | `previewPayFrom(address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` | Simulates a payment without modifying state. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Returns ruleset, token count, and hook specifications. |
95
+ | `previewCashOutFrom(address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bool beneficiaryIsFeeless, bytes metadata)` | Simulates a cash out without modifying state. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Returns ruleset, reclaim amount, tax rate, and hook specifications. |
87
96
 
88
97
  ### JBRulesets
89
98
 
package/USER_JOURNEYS.md CHANGED
@@ -69,6 +69,8 @@ All user paths through the Juicebox V6 core protocol. For each journey: entry po
69
69
  - Data hook can return empty weight (0) to suppress minting while still recording payment
70
70
  - Fee-on-transfer tokens: actual amount received is `_balanceOf(token) - balanceBefore` (measured via balance diff)
71
71
 
72
+ **Preview**: Call `JBMultiTerminal.previewPayFor(projectId, token, amount, beneficiary, metadata)` to simulate the full payment on-chain -- including data hook effects and the reserved/beneficiary token split. Returns `(ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications)`. This is a `view` function that does not modify state. For lower-level access without the mint split, the terminal delegates to `JBTerminalStore.previewPayFrom(payer, amount, projectId, beneficiary, metadata)` which returns the raw `(ruleset, tokenCount, hookSpecifications)`.
73
+
72
74
  ---
73
75
 
74
76
  ## 3. Cash Out Tokens
@@ -95,6 +97,8 @@ All user paths through the Juicebox V6 core protocol. For each journey: entry po
95
97
 
96
98
  **Events**: `CashOutTokens(rulesetId, rulesetCycleNumber, projectId, holder, beneficiary, cashOutCount, cashOutTaxRate, reclaimAmount, metadata, caller)`
97
99
 
100
+ **Preview**: Call `JBMultiTerminal.previewCashOutFrom(holder, projectId, cashOutCount, tokenToReclaim, beneficiary, metadata)` to simulate the full cash out on-chain -- including data hook effects on tax rate, supply, and hook specifications. Returns `(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications)`. This is a `view` function that does not modify state. The terminal delegates to `JBTerminalStore.previewCashOutFrom(holder, projectId, cashOutCount, accountingContext, balanceAccountingContexts, beneficiaryIsFeeless, metadata)` for the bonding curve computation. For a simpler estimate without data hook effects, use `currentTotalReclaimableSurplusOf(projectId, cashOutCount, decimals, currency)` or the 6-param `currentReclaimableSurplusOf` overload.
101
+
98
102
  **Edge cases**:
99
103
  - `cashOutCount = 0` with `totalSupply = 0` -- returns entire surplus (C-5 known bug)
100
104
  - `cashOutTaxRate = MAX (10,000)` -- returns 0 (all surplus locked)
@@ -188,6 +192,8 @@ All user paths through the Juicebox V6 core protocol. For each journey: entry po
188
192
 
189
193
  **Events**: `MintTokens(beneficiary, projectId, tokenCount, beneficiaryTokenCount, memo, reservedPercent, caller)`
190
194
 
195
+ **Preview**: Call `JBController.previewMintOf(projectId, tokenCount, useReservedPercent)` to see how a mint would split between the beneficiary and reserved token pools under the current ruleset. Returns `(beneficiaryTokenCount, reservedTokenCount)`. This is a `view` function that does not modify state.
196
+
191
197
  **Edge cases**:
192
198
  - `tokenCount = 0` -- reverts (`ZeroTokensToMint`)
193
199
  - If `allowOwnerMinting` is false in ruleset, only terminals and data hooks can mint
package/foundry.toml CHANGED
@@ -2,6 +2,7 @@
2
2
  solc = '0.8.26'
3
3
  evm_version = 'cancun'
4
4
  optimizer_runs = 200
5
+ via_ir = true
5
6
  libs = ["node_modules", "lib"]
6
7
  fs_permissions = [{ access = "read-write", path = "./"}]
7
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -535,12 +535,10 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
535
535
  // Determine the reserved percent to use.
536
536
  reservedPercent = useReservedPercent ? ruleset.reservedPercent() : 0;
537
537
 
538
- if (reservedPercent != JBConstants.MAX_RESERVED_PERCENT) {
539
- // Calculate the number of (non-reserved) tokens that will be minted to the beneficiary.
540
- beneficiaryTokenCount = mulDiv(
541
- tokenCount, JBConstants.MAX_RESERVED_PERCENT - reservedPercent, JBConstants.MAX_RESERVED_PERCENT
542
- );
538
+ // Split the requested token count into beneficiary and reserved portions.
539
+ (beneficiaryTokenCount,) = _splitTokenCount({tokenCount: tokenCount, reservedPercent: reservedPercent});
543
540
 
541
+ if (beneficiaryTokenCount != 0) {
544
542
  // Mint the tokens.
545
543
  // slither-disable-next-line reentrancy-benign,reentrancy-events,unused-return
546
544
  TOKENS.mintFor({holder: beneficiary, projectId: projectId, count: beneficiaryTokenCount});
@@ -799,6 +797,34 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
799
797
  metadata = ruleset.expandMetadata();
800
798
  }
801
799
 
800
+ /// @notice Previews how many beneficiary and reserved tokens `mintTokensOf(...)` would produce.
801
+ /// @param projectId The ID of the project whose tokens are being minted.
802
+ /// @param tokenCount The number of tokens to mint, including any reserved tokens.
803
+ /// @param useReservedPercent Whether to apply the ruleset's reserved percent.
804
+ /// @return beneficiaryTokenCount The number of tokens that would be minted for the beneficiary.
805
+ /// @return reservedTokenCount The number of tokens that would be reserved.
806
+ function previewMintOf(
807
+ uint256 projectId,
808
+ uint256 tokenCount,
809
+ bool useReservedPercent
810
+ )
811
+ external
812
+ view
813
+ override
814
+ returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
815
+ {
816
+ // Revert if there are no tokens to split.
817
+ if (tokenCount == 0) revert JBController_ZeroTokensToMint();
818
+
819
+ // Keep a reference to the current ruleset.
820
+ JBRuleset memory ruleset = _currentRulesetOf(projectId);
821
+
822
+ return
823
+ _splitTokenCount({
824
+ tokenCount: tokenCount, reservedPercent: useReservedPercent ? ruleset.reservedPercent() : 0
825
+ });
826
+ }
827
+
802
828
  /// @notice Check whether the project's controller can currently be set.
803
829
  /// @param projectId The ID of the project to check.
804
830
  /// @return A `bool` which is true if the project allows controllers to be set.
@@ -1141,6 +1167,27 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1141
1167
  }
1142
1168
  }
1143
1169
 
1170
+ /// @notice Splits a token count into beneficiary and reserved portions.
1171
+ /// @param tokenCount The total token count, including reserved tokens.
1172
+ /// @param reservedPercent The reserved percent to apply.
1173
+ /// @return beneficiaryTokenCount The number of tokens for the beneficiary.
1174
+ /// @return reservedTokenCount The number of tokens to reserve.
1175
+ function _splitTokenCount(
1176
+ uint256 tokenCount,
1177
+ uint256 reservedPercent
1178
+ )
1179
+ internal
1180
+ pure
1181
+ returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
1182
+ {
1183
+ // Compute the beneficiary's portion after removing the reserved share.
1184
+ beneficiaryTokenCount =
1185
+ mulDiv(tokenCount, JBConstants.MAX_RESERVED_PERCENT - reservedPercent, JBConstants.MAX_RESERVED_PERCENT);
1186
+
1187
+ // The remaining tokens are reserved.
1188
+ reservedTokenCount = tokenCount - beneficiaryTokenCount;
1189
+ }
1190
+
1144
1191
  //*********************************************************************//
1145
1192
  // -------------------------- internal views ------------------------- //
1146
1193
  //*********************************************************************//