@bananapus/core-v6 0.0.18 → 0.0.19

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.
package/CHANGE_LOG.md CHANGED
@@ -14,6 +14,15 @@ 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 new `view` functions added to `JBTerminalStore` and `IJBTerminalStore`:
20
+
21
+ - `previewPayFrom(address terminal, address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` -- Simulates a payment and returns `(uint256 tokenCount, JBPayHookSpecification[] hookSpecifications)`. Invokes data hooks if configured. Does not modify state.
22
+ - `previewCashOutFrom(address terminal, address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bool beneficiaryIsFeeless, bytes metadata)` -- Simulates a cash out and returns `(uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] hookSpecifications)`. Invokes data hooks if configured. Does not modify state.
23
+
24
+ Both functions use the explicit `terminal` parameter instead of `msg.sender`, allowing any caller to preview operations for any terminal. 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
+
17
26
  ### 0.3 JBBeforeCashOutRecordedContext -- beneficiaryIsFeeless Field
18
27
 
19
28
  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 +100,12 @@ Parameters changed from `memory` to `calldata` for gas efficiency.
91
100
  | `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
101
  | `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
102
 
103
+ #### IJBTerminalStore / JBTerminalStore
104
+
105
+ | Function | Description |
106
+ |----------|-------------|
107
+ | `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. |
108
+
94
109
  #### IJBTokens / JBTokens
95
110
 
96
111
  | Function | Description |
@@ -353,7 +368,7 @@ Throughout the codebase, function calls were updated to use named argument synta
353
368
  |----|----|-------|
354
369
  | `IJBController` | `IJBController` | Gained `setTokenMetadataOf`. `calldata` for terminal configs. |
355
370
  | `IJBRulesets` | `IJBRulesets` | `updateRulesetWeightCache` gained `rulesetId` parameter |
356
- | `IJBTerminalStore` | `IJBTerminalStore` | `tokenCount` renamed to `cashOutCount` in `currentReclaimableSurplusOf`. `recordCashOutFor` gained `beneficiaryIsFeeless` param. View functions reordered. |
371
+ | `IJBTerminalStore` | `IJBTerminalStore` | `tokenCount` renamed to `cashOutCount` in `currentReclaimableSurplusOf`. `recordCashOutFor` gained `beneficiaryIsFeeless` param. New `currentTotalReclaimableSurplusOf` convenience view. View functions reordered. |
357
372
  | `IJBPayoutTerminal` | `IJBPayoutTerminal` | `sendPayoutsOf` returns `amountPaidOut` (was `netLeftoverPayoutAmount`). `SendPayoutToSplit` event moved. |
358
373
  | `IJBPermitTerminal` | `IJBPermitTerminal` | Gained `Permit2AllowanceFailed` event |
359
374
  | `IJBMigratable` | `IJBMigratable` | Gained `afterReceiveMigrationFrom` function |
package/SKILLS.md CHANGED
@@ -83,7 +83,13 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
83
83
  | `balanceOf(address terminal, uint256 projectId, address token)` | Returns the balance of a project at a terminal for a given token. |
84
84
  | `usedPayoutLimitOf(address terminal, uint256 projectId, address token, uint256 rulesetCycleNumber, uint256 currency)` | Returns the used payout limit for a project in a given cycle. |
85
85
  | `usedSurplusAllowanceOf(address terminal, uint256 projectId, address token, uint256 rulesetId, uint256 currency)` | Returns the used surplus allowance for a project in a given ruleset. |
86
+ | `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. |
87
+ | `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. |
88
+ | `currentTotalReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 decimals, uint256 currency)` | Convenience view: reclaimable surplus across all terminals using all accounting contexts. Mirrors `currentTotalSurplusOf`. |
86
89
  | `currentSurplusOf(address terminal, uint256 projectId, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the current surplus for a project at a terminal. |
90
+ | `currentTotalSurplusOf(uint256 projectId, uint256 decimals, uint256 currency)` | Returns the total surplus across all terminals. |
91
+ | `previewPayFrom(address terminal, address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` | Simulates a payment without modifying state. Invokes data hooks if configured. Returns token count and hook specifications. |
92
+ | `previewCashOutFrom(address terminal, address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bool beneficiaryIsFeeless, bytes metadata)` | Simulates a cash out without modifying state. Invokes data hooks if configured. Returns reclaim amount, tax rate, and hook specifications. |
87
93
 
88
94
  ### JBRulesets
89
95
 
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 `JBTerminalStore.previewPayFrom(terminal, payer, amount, projectId, beneficiary, metadata)` to simulate the payment on-chain and see the exact token count and hook specifications that would be produced -- including data hook effects. This is a `view` function that does not modify state.
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 `JBTerminalStore.previewCashOutFrom(terminal, holder, projectId, cashOutCount, accountingContext, balanceAccountingContexts, beneficiaryIsFeeless, metadata)` to simulate the full cash out on-chain -- including data hook effects on tax rate, supply, and hook specifications. This is a `view` function that does not modify state. 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)
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.19",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -184,99 +184,25 @@ contract JBTerminalStore is IJBTerminalStore {
184
184
  JBCashOutHookSpecification[] memory hookSpecifications
185
185
  )
186
186
  {
187
- // Get a reference to the project's current ruleset.
188
- ruleset = RULESETS.currentOf(projectId);
189
-
190
- // Store the current surplus in `reclaimAmount` temporarily to avoid allocating a separate local variable
191
- // (saves one stack slot, which is needed to fit the 7th parameter without hitting stack-too-deep).
192
- reclaimAmount = ruleset.useTotalSurplusForCashOuts()
193
- ? JBSurplus.currentSurplusOf({
194
- projectId: projectId,
195
- terminals: DIRECTORY.terminalsOf(projectId),
196
- accountingContexts: new JBAccountingContext[](0),
197
- decimals: accountingContext.decimals,
198
- currency: accountingContext.currency
199
- })
200
- : _surplusFrom({
201
- terminal: msg.sender,
202
- projectId: projectId,
203
- accountingContexts: balanceAccountingContexts,
204
- ruleset: ruleset,
205
- targetDecimals: accountingContext.decimals,
206
- targetCurrency: accountingContext.currency
207
- });
208
-
209
- // Scoped to keep `totalSupply` and `context` off the outer stack.
210
- {
211
- // Get the total number of outstanding project tokens.
212
- uint256 totalSupply = IJBController(address(DIRECTORY.controllerOf(projectId)))
213
- .totalTokenSupplyWithReservedTokensOf(projectId);
214
-
215
- // Can't cash out more tokens than are in the supply.
216
- if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
217
-
218
- // SECURITY NOTE: The data hook has absolute control over cash-out economics.
219
- // It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
220
- // completely overriding the terminal's bonding curve math. For example, setting
221
- // totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
222
- // Project owners MUST audit their data hooks with the same rigor as the terminal.
223
-
224
- // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
225
- if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
226
- // Build the cash out context field-by-field to avoid stack-too-deep
227
- // (the struct has 11 fields — a struct literal would require all values on the stack at once).
228
- JBBeforeCashOutRecordedContext memory context;
229
- context.terminal = msg.sender;
230
- context.holder = holder;
231
- context.projectId = projectId;
232
- context.rulesetId = ruleset.id;
233
- context.cashOutCount = cashOutCount;
234
- context.totalSupply = totalSupply;
235
- context.surplus = JBTokenAmount({
236
- token: accountingContext.token,
237
- value: reclaimAmount, // reclaimAmount temporarily holds the current surplus.
238
- decimals: accountingContext.decimals,
239
- currency: accountingContext.currency
240
- });
241
- context.useTotalSurplus = ruleset.useTotalSurplusForCashOuts();
242
- context.cashOutTaxRate = ruleset.cashOutTaxRate();
243
- context.beneficiaryIsFeeless = beneficiaryIsFeeless;
244
- context.metadata = metadata;
245
-
246
- (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
247
- IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
248
- } else {
249
- cashOutTaxRate = ruleset.cashOutTaxRate();
250
- }
251
-
252
- // Calculate the reclaim amount. `reclaimAmount` currently holds the surplus — overwrite it with the
253
- // result.
254
- if (reclaimAmount != 0) {
255
- reclaimAmount = JBCashOuts.cashOutFrom({
256
- surplus: reclaimAmount,
257
- cashOutCount: cashOutCount,
258
- totalSupply: totalSupply,
259
- cashOutTaxRate: cashOutTaxRate
260
- });
261
- }
262
- }
187
+ (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = _computeCashOutFrom({
188
+ terminal: msg.sender,
189
+ holder: holder,
190
+ projectId: projectId,
191
+ cashOutCount: cashOutCount,
192
+ accountingContext: accountingContext,
193
+ balanceAccountingContexts: balanceAccountingContexts,
194
+ beneficiaryIsFeeless: beneficiaryIsFeeless,
195
+ metadata: metadata
196
+ });
263
197
 
264
- // Keep a reference to the amount that should be added to the project's balance.
198
+ // Compute the total amount to subtract from the project's balance.
265
199
  uint256 balanceDiff = reclaimAmount;
266
200
 
267
- // Ensure that the specifications have valid amounts.
268
201
  if (hookSpecifications.length != 0) {
269
- // Keep a reference to the number of cash out hooks specified.
270
202
  uint256 numberOfSpecifications = hookSpecifications.length;
271
-
272
- // Loop through each specification.
273
203
  for (uint256 i; i < numberOfSpecifications; i++) {
274
- // Get a reference to the specification's amount.
275
204
  uint256 specificationAmount = hookSpecifications[i].amount;
276
-
277
- // Ensure the amount is non-zero.
278
205
  if (specificationAmount != 0) {
279
- // Increment the total amount being subtracted from the balance.
280
206
  balanceDiff += specificationAmount;
281
207
  }
282
208
  }
@@ -323,98 +249,21 @@ contract JBTerminalStore is IJBTerminalStore {
323
249
  override
324
250
  returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
325
251
  {
326
- // Get a reference to the project's current ruleset.
327
- ruleset = RULESETS.currentOf(projectId);
328
-
329
- // The project must have a ruleset.
330
- if (ruleset.cycleNumber == 0) revert JBTerminalStore_RulesetNotFound(projectId);
331
-
332
- // The ruleset must not have payments paused.
333
- if (ruleset.pausePay()) revert JBTerminalStore_RulesetPaymentPaused();
334
-
335
- // The weight according to which new tokens are to be minted, as a fixed point number with 18 decimals.
336
- uint256 weight;
337
-
338
- // SECURITY NOTE: The data hook has absolute control over payment token minting.
339
- // It can return an arbitrary weight (overriding the ruleset's weight) and hook specifications
340
- // that divert payment funds to external hooks before they reach the project's balance.
341
- // Project owners MUST audit their data hooks with the same rigor as the terminal.
342
-
343
- // If the ruleset has a data hook enabled for payments, use it to derive a weight and memo.
344
- if (ruleset.useDataHookForPay() && ruleset.dataHook() != address(0)) {
345
- // Create the pay context that'll be sent to the data hook.
346
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
347
- terminal: msg.sender,
348
- payer: payer,
349
- amount: amount,
350
- projectId: projectId,
351
- rulesetId: ruleset.id,
352
- beneficiary: beneficiary,
353
- weight: ruleset.weight,
354
- reservedPercent: ruleset.reservedPercent(),
355
- metadata: metadata
356
- });
357
-
358
- (weight, hookSpecifications) = IJBRulesetDataHook(ruleset.dataHook()).beforePayRecordedWith(context);
359
- }
360
- // Otherwise use the ruleset's weight
361
- else {
362
- weight = ruleset.weight;
363
- }
364
-
365
- // Keep a reference to the amount that should be added to the project's balance.
366
- uint256 balanceDiff = amount.value;
367
-
368
- // Scoped section preventing stack too deep.
369
- {
370
- // Keep a reference to the number of hook specifications.
371
- uint256 numberOfSpecifications = hookSpecifications.length;
372
-
373
- // Ensure that the specifications have valid amounts.
374
- for (uint256 i; i < numberOfSpecifications; i++) {
375
- // Get a reference to the specification's amount.
376
- uint256 specifiedAmount = hookSpecifications[i].amount;
377
-
378
- // Ensure the amount is non-zero.
379
- if (specifiedAmount != 0) {
380
- // Can't send more to hook than was paid.
381
- if (specifiedAmount > balanceDiff) {
382
- revert JBTerminalStore_InvalidAmountToForwardHook(specifiedAmount, balanceDiff);
383
- }
384
-
385
- // Decrement the total amount being added to the local balance.
386
- balanceDiff -= specifiedAmount;
387
- }
388
- }
389
- }
390
-
391
- // If there's no amount being recorded, there's nothing left to do.
392
- if (amount.value == 0) return (ruleset, 0, hookSpecifications);
252
+ uint256 balanceDiff;
253
+ (ruleset, tokenCount, hookSpecifications, balanceDiff) = _computePayFrom({
254
+ terminal: msg.sender,
255
+ payer: payer,
256
+ amount: amount,
257
+ projectId: projectId,
258
+ beneficiary: beneficiary,
259
+ metadata: metadata
260
+ });
393
261
 
394
262
  // Add the correct balance difference to the token balance of the project.
395
263
  if (balanceDiff != 0) {
396
264
  balanceOf[msg.sender][projectId][amount.token] =
397
265
  balanceOf[msg.sender][projectId][amount.token] + balanceDiff;
398
266
  }
399
-
400
- // If there's no weight, the token count must be 0, so there's nothing left to do.
401
- if (weight == 0) return (ruleset, 0, hookSpecifications);
402
-
403
- // If the terminal should base its weight on a currency other than the terminal's currency, determine the
404
- // factor. The weight is always a fixed point mumber with 18 decimals. To ensure this, the ratio should use the
405
- // same
406
- // number of decimals as the `amount`.
407
- uint256 weightRatio = amount.currency == ruleset.baseCurrency()
408
- ? 10 ** amount.decimals
409
- : PRICES.pricePerUnitOf({
410
- projectId: projectId,
411
- pricingCurrency: amount.currency,
412
- unitCurrency: ruleset.baseCurrency(),
413
- decimals: amount.decimals
414
- });
415
-
416
- // Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has.
417
- tokenCount = mulDiv(amount.value, weight, weightRatio);
418
267
  }
419
268
 
420
269
  /// @notice Records a payout from a project.
@@ -699,6 +548,34 @@ contract JBTerminalStore is IJBTerminalStore {
699
548
  });
700
549
  }
701
550
 
551
+ /// @notice Returns the number of surplus terminal tokens that would be reclaimed by cashing out a given number of
552
+ /// tokens across all of a project's terminals using all accounting contexts.
553
+ /// @param projectId The ID of the project whose tokens would be cashed out.
554
+ /// @param cashOutCount The number of tokens that would be cashed out, as a fixed point number with 18 decimals.
555
+ /// @param decimals The number of decimals to include in the resulting fixed point number.
556
+ /// @param currency The currency that the resulting number will be in terms of.
557
+ /// @return The amount of surplus terminal tokens that would be reclaimed by cashing out `cashOutCount` tokens.
558
+ function currentTotalReclaimableSurplusOf(
559
+ uint256 projectId,
560
+ uint256 cashOutCount,
561
+ uint256 decimals,
562
+ uint256 currency
563
+ )
564
+ external
565
+ view
566
+ override
567
+ returns (uint256)
568
+ {
569
+ return this.currentReclaimableSurplusOf({
570
+ projectId: projectId,
571
+ cashOutCount: cashOutCount,
572
+ terminals: new IJBTerminal[](0),
573
+ accountingContexts: new JBAccountingContext[](0),
574
+ decimals: decimals,
575
+ currency: currency
576
+ });
577
+ }
578
+
702
579
  /// @notice Gets the current surplus amount in a terminal for a specified project.
703
580
  /// @dev The surplus is the amount of funds a project has in a terminal in excess of its payout limit.
704
581
  /// @dev The surplus is represented as a fixed point number with the same amount of decimals as the specified
@@ -757,10 +634,319 @@ contract JBTerminalStore is IJBTerminalStore {
757
634
  });
758
635
  }
759
636
 
637
+ /// @notice Simulates a cash out without modifying state.
638
+ /// @dev Invokes data hooks if configured, but skips the balance sufficiency check (balance may change between
639
+ /// preview and execution).
640
+ /// @param terminal The terminal address to simulate the cash out from.
641
+ /// @param holder The address cashing out.
642
+ /// @param projectId The ID of the project being cashed out from.
643
+ /// @param cashOutCount The number of project tokens being cashed out.
644
+ /// @param accountingContext The accounting context of the token being reclaimed.
645
+ /// @param balanceAccountingContexts The accounting contexts to include in the balance calculation.
646
+ /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
647
+ /// @param metadata Extra data to pass along to the data hook.
648
+ /// @return ruleset The project's current ruleset.
649
+ /// @return reclaimAmount The amount that would be reclaimed.
650
+ /// @return cashOutTaxRate The cash out tax rate that would be applied.
651
+ /// @return hookSpecifications Any cash out hook specifications from the data hook.
652
+ function previewCashOutFrom(
653
+ address terminal,
654
+ address holder,
655
+ uint256 projectId,
656
+ uint256 cashOutCount,
657
+ JBAccountingContext calldata accountingContext,
658
+ JBAccountingContext[] calldata balanceAccountingContexts,
659
+ bool beneficiaryIsFeeless,
660
+ bytes calldata metadata
661
+ )
662
+ external
663
+ view
664
+ override
665
+ returns (
666
+ JBRuleset memory ruleset,
667
+ uint256 reclaimAmount,
668
+ uint256 cashOutTaxRate,
669
+ JBCashOutHookSpecification[] memory hookSpecifications
670
+ )
671
+ {
672
+ (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = _computeCashOutFrom({
673
+ terminal: terminal,
674
+ holder: holder,
675
+ projectId: projectId,
676
+ cashOutCount: cashOutCount,
677
+ accountingContext: accountingContext,
678
+ balanceAccountingContexts: balanceAccountingContexts,
679
+ beneficiaryIsFeeless: beneficiaryIsFeeless,
680
+ metadata: metadata
681
+ });
682
+ }
683
+
684
+ /// @notice Simulates a payment without modifying state.
685
+ /// @dev Invokes data hooks if configured. Returns the same token count and hook specifications that
686
+ /// `recordPaymentFrom` would produce.
687
+ /// @param terminal The terminal address to simulate the payment from.
688
+ /// @param payer The address of the payer.
689
+ /// @param amount The amount being paid.
690
+ /// @param projectId The ID of the project being paid.
691
+ /// @param beneficiary The address to mint project tokens to.
692
+ /// @param metadata Extra data to pass along to the data hook.
693
+ /// @return ruleset The project's current ruleset.
694
+ /// @return tokenCount The number of project tokens that would be minted.
695
+ /// @return hookSpecifications Any pay hook specifications from the data hook.
696
+ function previewPayFrom(
697
+ address terminal,
698
+ address payer,
699
+ JBTokenAmount memory amount,
700
+ uint256 projectId,
701
+ address beneficiary,
702
+ bytes calldata metadata
703
+ )
704
+ external
705
+ view
706
+ override
707
+ returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
708
+ {
709
+ (ruleset, tokenCount, hookSpecifications,) = _computePayFrom({
710
+ terminal: terminal,
711
+ payer: payer,
712
+ amount: amount,
713
+ projectId: projectId,
714
+ beneficiary: beneficiary,
715
+ metadata: metadata
716
+ });
717
+ }
718
+
760
719
  //*********************************************************************//
761
720
  // -------------------------- internal views ------------------------- //
762
721
  //*********************************************************************//
763
722
 
723
+ /// @notice Computes payment results without writing state.
724
+ /// @param terminal The terminal recording the payment.
725
+ /// @param payer The address that made the payment.
726
+ /// @param amount The amount of tokens being paid.
727
+ /// @param projectId The ID of the project being paid.
728
+ /// @param beneficiary The beneficiary of the payment.
729
+ /// @param metadata Bytes to send to the data hook.
730
+ /// @return ruleset The ruleset the payment would be made during.
731
+ /// @return tokenCount The number of project tokens that would be minted.
732
+ /// @return hookSpecifications Pay hook specifications from the data hook.
733
+ /// @return balanceDiff The amount that would be added to the project's balance.
734
+ function _computePayFrom(
735
+ address terminal,
736
+ address payer,
737
+ JBTokenAmount memory amount,
738
+ uint256 projectId,
739
+ address beneficiary,
740
+ bytes memory metadata
741
+ )
742
+ internal
743
+ view
744
+ returns (
745
+ JBRuleset memory ruleset,
746
+ uint256 tokenCount,
747
+ JBPayHookSpecification[] memory hookSpecifications,
748
+ uint256 balanceDiff
749
+ )
750
+ {
751
+ // Get a reference to the project's current ruleset.
752
+ ruleset = RULESETS.currentOf(projectId);
753
+
754
+ // The project must have a ruleset.
755
+ if (ruleset.cycleNumber == 0) revert JBTerminalStore_RulesetNotFound(projectId);
756
+
757
+ // The ruleset must not have payments paused.
758
+ if (ruleset.pausePay()) revert JBTerminalStore_RulesetPaymentPaused();
759
+
760
+ // The weight according to which new tokens are to be minted, as a fixed point number with 18 decimals.
761
+ uint256 weight;
762
+
763
+ // SECURITY NOTE: The data hook has absolute control over payment token minting.
764
+ // It can return an arbitrary weight (overriding the ruleset's weight) and hook specifications
765
+ // that divert payment funds to external hooks before they reach the project's balance.
766
+ // Project owners MUST audit their data hooks with the same rigor as the terminal.
767
+
768
+ // If the ruleset has a data hook enabled for payments, use it to derive a weight and memo.
769
+ if (ruleset.useDataHookForPay() && ruleset.dataHook() != address(0)) {
770
+ // Create the pay context that'll be sent to the data hook.
771
+ JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
772
+ terminal: terminal,
773
+ payer: payer,
774
+ amount: amount,
775
+ projectId: projectId,
776
+ rulesetId: ruleset.id,
777
+ beneficiary: beneficiary,
778
+ weight: ruleset.weight,
779
+ reservedPercent: ruleset.reservedPercent(),
780
+ metadata: metadata
781
+ });
782
+
783
+ (weight, hookSpecifications) = IJBRulesetDataHook(ruleset.dataHook()).beforePayRecordedWith(context);
784
+ }
785
+ // Otherwise use the ruleset's weight
786
+ else {
787
+ weight = ruleset.weight;
788
+ }
789
+
790
+ // Keep a reference to the amount that should be added to the project's balance.
791
+ balanceDiff = amount.value;
792
+
793
+ // Scoped section preventing stack too deep.
794
+ {
795
+ // Keep a reference to the number of hook specifications.
796
+ uint256 numberOfSpecifications = hookSpecifications.length;
797
+
798
+ // Ensure that the specifications have valid amounts.
799
+ for (uint256 i; i < numberOfSpecifications; i++) {
800
+ // Get a reference to the specification's amount.
801
+ uint256 specifiedAmount = hookSpecifications[i].amount;
802
+
803
+ // Ensure the amount is non-zero.
804
+ if (specifiedAmount != 0) {
805
+ // Can't send more to hook than was paid.
806
+ if (specifiedAmount > balanceDiff) {
807
+ revert JBTerminalStore_InvalidAmountToForwardHook(specifiedAmount, balanceDiff);
808
+ }
809
+
810
+ // Decrement the total amount being added to the local balance.
811
+ balanceDiff -= specifiedAmount;
812
+ }
813
+ }
814
+ }
815
+
816
+ // If there's no amount being recorded, there's nothing left to do.
817
+ if (amount.value == 0) return (ruleset, 0, hookSpecifications, 0);
818
+
819
+ // If there's no weight, the token count must be 0, so there's nothing left to do.
820
+ if (weight == 0) return (ruleset, 0, hookSpecifications, balanceDiff);
821
+
822
+ // If the terminal should base its weight on a currency other than the terminal's currency, determine the
823
+ // factor. The weight is always a fixed point mumber with 18 decimals. To ensure this, the ratio should use the
824
+ // same
825
+ // number of decimals as the `amount`.
826
+ uint256 weightRatio = amount.currency == ruleset.baseCurrency()
827
+ ? 10 ** amount.decimals
828
+ : PRICES.pricePerUnitOf({
829
+ projectId: projectId,
830
+ pricingCurrency: amount.currency,
831
+ unitCurrency: ruleset.baseCurrency(),
832
+ decimals: amount.decimals
833
+ });
834
+
835
+ // Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has.
836
+ tokenCount = mulDiv(amount.value, weight, weightRatio);
837
+ }
838
+
839
+ /// @notice Computes cash out results without writing state.
840
+ /// @param terminal The terminal recording the cash out.
841
+ /// @param holder The account that is cashing out tokens.
842
+ /// @param projectId The ID of the project being cashed out from.
843
+ /// @param cashOutCount The number of project tokens to cash out.
844
+ /// @param accountingContext The accounting context of the token being reclaimed.
845
+ /// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
846
+ /// surplus being reclaimed from.
847
+ /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
848
+ /// @param metadata Bytes to send to the data hook.
849
+ /// @return ruleset The ruleset during the cash out.
850
+ /// @return reclaimAmount The amount of tokens reclaimed.
851
+ /// @return cashOutTaxRate The cash out tax rate applied.
852
+ /// @return hookSpecifications Cash out hook specifications from the data hook.
853
+ function _computeCashOutFrom(
854
+ address terminal,
855
+ address holder,
856
+ uint256 projectId,
857
+ uint256 cashOutCount,
858
+ JBAccountingContext calldata accountingContext,
859
+ JBAccountingContext[] calldata balanceAccountingContexts,
860
+ bool beneficiaryIsFeeless,
861
+ bytes memory metadata
862
+ )
863
+ internal
864
+ view
865
+ returns (
866
+ JBRuleset memory ruleset,
867
+ uint256 reclaimAmount,
868
+ uint256 cashOutTaxRate,
869
+ JBCashOutHookSpecification[] memory hookSpecifications
870
+ )
871
+ {
872
+ // Get a reference to the project's current ruleset.
873
+ ruleset = RULESETS.currentOf(projectId);
874
+
875
+ // Store the current surplus in `reclaimAmount` temporarily to avoid allocating a separate local variable
876
+ // (saves one stack slot, which is needed to fit the 7th parameter without hitting stack-too-deep).
877
+ reclaimAmount = ruleset.useTotalSurplusForCashOuts()
878
+ ? JBSurplus.currentSurplusOf({
879
+ projectId: projectId,
880
+ terminals: DIRECTORY.terminalsOf(projectId),
881
+ accountingContexts: new JBAccountingContext[](0),
882
+ decimals: accountingContext.decimals,
883
+ currency: accountingContext.currency
884
+ })
885
+ : _surplusFrom({
886
+ terminal: terminal,
887
+ projectId: projectId,
888
+ accountingContexts: balanceAccountingContexts,
889
+ ruleset: ruleset,
890
+ targetDecimals: accountingContext.decimals,
891
+ targetCurrency: accountingContext.currency
892
+ });
893
+
894
+ // Scoped to keep `totalSupply` and `context` off the outer stack.
895
+ {
896
+ // Get the total number of outstanding project tokens.
897
+ uint256 totalSupply = IJBController(address(DIRECTORY.controllerOf(projectId)))
898
+ .totalTokenSupplyWithReservedTokensOf(projectId);
899
+
900
+ // Can't cash out more tokens than are in the supply.
901
+ if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
902
+
903
+ // SECURITY NOTE: The data hook has absolute control over cash-out economics.
904
+ // It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
905
+ // completely overriding the terminal's bonding curve math. For example, setting
906
+ // totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
907
+ // Project owners MUST audit their data hooks with the same rigor as the terminal.
908
+
909
+ // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
910
+ if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
911
+ // Build the cash out context field-by-field to avoid stack-too-deep
912
+ // (the struct has 11 fields — a struct literal would require all values on the stack at once).
913
+ JBBeforeCashOutRecordedContext memory context;
914
+ context.terminal = terminal;
915
+ context.holder = holder;
916
+ context.projectId = projectId;
917
+ context.rulesetId = ruleset.id;
918
+ context.cashOutCount = cashOutCount;
919
+ context.totalSupply = totalSupply;
920
+ context.surplus = JBTokenAmount({
921
+ token: accountingContext.token,
922
+ value: reclaimAmount, // reclaimAmount temporarily holds the current surplus.
923
+ decimals: accountingContext.decimals,
924
+ currency: accountingContext.currency
925
+ });
926
+ context.useTotalSurplus = ruleset.useTotalSurplusForCashOuts();
927
+ context.cashOutTaxRate = ruleset.cashOutTaxRate();
928
+ context.beneficiaryIsFeeless = beneficiaryIsFeeless;
929
+ context.metadata = metadata;
930
+
931
+ (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
932
+ IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
933
+ } else {
934
+ cashOutTaxRate = ruleset.cashOutTaxRate();
935
+ }
936
+
937
+ // Calculate the reclaim amount. `reclaimAmount` currently holds the surplus — overwrite it with the
938
+ // result.
939
+ if (reclaimAmount != 0) {
940
+ reclaimAmount = JBCashOuts.cashOutFrom({
941
+ surplus: reclaimAmount,
942
+ cashOutCount: cashOutCount,
943
+ totalSupply: totalSupply,
944
+ cashOutTaxRate: cashOutTaxRate
945
+ });
946
+ }
947
+ }
948
+ }
949
+
764
950
  /// @notice Gets a project's surplus amount in a terminal as measured by a given ruleset, across multiple accounting
765
951
  /// contexts.
766
952
  /// @dev This amount changes as the value of the balance changes in relation to the currency being used to measure