@bananapus/core-v6 0.0.21 → 0.0.22

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 (48) hide show
  1. package/ADMINISTRATION.md +0 -1
  2. package/AUDIT_INSTRUCTIONS.md +1 -1
  3. package/CHANGE_LOG.md +3 -3
  4. package/RISKS.md +3 -3
  5. package/SKILLS.md +8 -8
  6. package/USER_JOURNEYS.md +1 -1
  7. package/foundry.toml +0 -1
  8. package/package.json +1 -1
  9. package/src/JBMultiTerminal.sol +92 -192
  10. package/src/JBTerminalStore.sol +405 -235
  11. package/src/interfaces/IJBMultiTerminal.sol +0 -4
  12. package/src/interfaces/IJBTerminal.sol +4 -4
  13. package/src/interfaces/IJBTerminalStore.sol +65 -33
  14. package/src/libraries/JBPayoutSplitGroupLib.sol +0 -1
  15. package/src/libraries/JBSurplus.sol +3 -4
  16. package/test/ComprehensiveInvariant.t.sol +5 -7
  17. package/test/CoreExploitTests.t.sol +18 -23
  18. package/test/TestCashOut.sol +6 -6
  19. package/test/TestMultiTerminalSurplus.sol +4 -4
  20. package/test/TestMultiTokenSurplus.sol +6 -23
  21. package/test/TestTerminalMigration.sol +2 -7
  22. package/test/fork/TestSequencerPriceFeedFork.sol +1 -1
  23. package/test/fork/TestTerminalPreviewParityFork.sol +0 -1
  24. package/test/invariants/TerminalStoreInvariant.t.sol +5 -7
  25. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +1 -2
  26. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +23 -24
  27. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +79 -119
  28. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +33 -26
  29. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +32 -27
  30. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +22 -4
  31. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +8 -5
  32. package/test/units/static/JBMultiTerminal/TestPay.sol +41 -33
  33. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +19 -18
  34. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +38 -22
  35. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +9 -6
  36. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +4 -4
  37. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +37 -32
  38. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +5 -20
  39. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +17 -0
  40. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +120 -246
  41. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +29 -7
  42. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +88 -20
  43. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +30 -29
  44. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +46 -16
  45. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +24 -53
  46. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +24 -4
  47. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +14 -4
  48. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +21 -3
@@ -2,6 +2,7 @@
2
2
  pragma solidity 0.8.26;
3
3
 
4
4
  import {mulDiv} from "@prb/math/src/Common.sol";
5
+ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
5
6
 
6
7
  import {IJBController} from "./interfaces/IJBController.sol";
7
8
  import {IJBDirectory} from "./interfaces/IJBDirectory.sol";
@@ -10,9 +11,9 @@ import {IJBRulesetDataHook} from "./interfaces/IJBRulesetDataHook.sol";
10
11
  import {IJBRulesets} from "./interfaces/IJBRulesets.sol";
11
12
  import {IJBTerminal} from "./interfaces/IJBTerminal.sol";
12
13
  import {IJBTerminalStore} from "./interfaces/IJBTerminalStore.sol";
14
+ import {JBCashOuts} from "./libraries/JBCashOuts.sol";
13
15
  import {JBConstants} from "./libraries/JBConstants.sol";
14
16
  import {JBFixedPointNumber} from "./libraries/JBFixedPointNumber.sol";
15
- import {JBCashOuts} from "./libraries/JBCashOuts.sol";
16
17
  import {JBRulesetMetadataResolver} from "./libraries/JBRulesetMetadataResolver.sol";
17
18
  import {JBSurplus} from "./libraries/JBSurplus.sol";
18
19
  import {JBAccountingContext} from "./structs/JBAccountingContext.sol";
@@ -34,6 +35,9 @@ contract JBTerminalStore is IJBTerminalStore {
34
35
  // --------------------------- custom errors ------------------------- //
35
36
  //*********************************************************************//
36
37
 
38
+ error JBTerminalStore_AccountingContextAlreadySet(address token);
39
+ error JBTerminalStore_AccountingContextDecimalsMismatch();
40
+ error JBTerminalStore_AddingAccountingContextNotAllowed();
37
41
  error JBTerminalStore_InadequateControllerAllowance(uint256 amount, uint256 allowance);
38
42
  error JBTerminalStore_InadequateControllerPayoutLimit(uint256 amount, uint256 limit);
39
43
  error JBTerminalStore_InadequateTerminalStoreBalance(uint256 amount, uint256 balance);
@@ -44,6 +48,7 @@ contract JBTerminalStore is IJBTerminalStore {
44
48
  error JBTerminalStore_RulesetPaymentPaused();
45
49
  error JBTerminalStore_TerminalMigrationNotAllowed();
46
50
  error JBTerminalStore_Uint224Overflow(uint256 value);
51
+ error JBTerminalStore_ZeroAccountingContextCurrency();
47
52
 
48
53
  //*********************************************************************//
49
54
  // -------------------------- internal constants --------------------- //
@@ -120,6 +125,22 @@ contract JBTerminalStore is IJBTerminalStore {
120
125
  public
121
126
  override usedSurplusAllowanceOf;
122
127
 
128
+ //*********************************************************************//
129
+ // --------------------- internal stored properties ------------------ //
130
+ //*********************************************************************//
131
+
132
+ /// @notice The accounting context for a terminal's project token.
133
+ /// @custom:param terminal The terminal the accounting context applies to.
134
+ /// @custom:param projectId The ID of the project.
135
+ /// @custom:param token The token to get the accounting context for.
136
+ mapping(address terminal => mapping(uint256 projectId => mapping(address token => JBAccountingContext))) internal
137
+ _accountingContextForTokenOf;
138
+
139
+ /// @notice A list of accounting contexts for each terminal's project.
140
+ /// @custom:param terminal The terminal the accounting contexts apply to.
141
+ /// @custom:param projectId The ID of the project.
142
+ mapping(address terminal => mapping(uint256 projectId => JBAccountingContext[])) internal _accountingContextsOf;
143
+
123
144
  //*********************************************************************//
124
145
  // -------------------------- constructor ---------------------------- //
125
146
  //*********************************************************************//
@@ -137,6 +158,64 @@ contract JBTerminalStore is IJBTerminalStore {
137
158
  // ---------------------- external transactions ---------------------- //
138
159
  //*********************************************************************//
139
160
 
161
+ /// @notice Records accounting contexts for a terminal's project tokens.
162
+ /// @dev Uses msg.sender as the terminal.
163
+ /// @param projectId The ID of the project.
164
+ /// @param contexts The accounting contexts to record.
165
+ function recordAccountingContextOf(uint256 projectId, JBAccountingContext[] calldata contexts) external override {
166
+ // Get a reference to the project's current ruleset.
167
+ JBRuleset memory ruleset = RULESETS.currentOf(projectId);
168
+
169
+ // Make sure that if there's a ruleset, it allows adding accounting contexts.
170
+ if (ruleset.id != 0 && !ruleset.allowAddAccountingContext()) {
171
+ revert JBTerminalStore_AddingAccountingContextNotAllowed();
172
+ }
173
+
174
+ // Record each accounting context.
175
+ for (uint256 i; i < contexts.length; i++) {
176
+ JBAccountingContext calldata context = contexts[i];
177
+
178
+ // Make sure the token accounting context isn't already set.
179
+ if (_accountingContextForTokenOf[msg.sender][projectId][context.token].token != address(0)) {
180
+ revert JBTerminalStore_AccountingContextAlreadySet(context.token);
181
+ }
182
+
183
+ // Keep track of a flag indicating if we know the provided decimals are incorrect.
184
+ bool knownInvalidDecimals;
185
+
186
+ // Check if the token is the native token and has the correct decimals.
187
+ if (context.token == JBConstants.NATIVE_TOKEN && context.decimals != 18) {
188
+ knownInvalidDecimals = true;
189
+ } else if (context.token != JBConstants.NATIVE_TOKEN && context.token.code.length > 0) {
190
+ // slither-disable-next-line calls-loop
191
+ try IERC20Metadata(context.token).decimals() returns (uint8 decimals) {
192
+ if (context.decimals != decimals) {
193
+ knownInvalidDecimals = true;
194
+ }
195
+ } catch {
196
+ // The token didn't support `decimals`.
197
+ // @dev Non-standard ERC20s that revert on `decimals()` will bypass decimal validation.
198
+ // The caller is responsible for providing the correct decimals for such tokens.
199
+ knownInvalidDecimals = false;
200
+ }
201
+ }
202
+
203
+ // Make sure the decimals are correct.
204
+ if (knownInvalidDecimals) {
205
+ revert JBTerminalStore_AccountingContextDecimalsMismatch();
206
+ }
207
+
208
+ // Make sure the currency is non-zero.
209
+ if (context.currency == 0) revert JBTerminalStore_ZeroAccountingContextCurrency();
210
+
211
+ // Store the accounting context.
212
+ _accountingContextForTokenOf[msg.sender][projectId][context.token] = context;
213
+
214
+ // Add the context to the list.
215
+ _accountingContextsOf[msg.sender][projectId].push(context);
216
+ }
217
+ }
218
+
140
219
  /// @notice Records funds being added to a project's balance.
141
220
  /// @param projectId The ID of the project which funds are being added to the balance of.
142
221
  /// @param token The token being added to the balance.
@@ -155,9 +234,7 @@ contract JBTerminalStore is IJBTerminalStore {
155
234
  /// @param holder The account that is cashing out tokens.
156
235
  /// @param projectId The ID of the project being cashing out from.
157
236
  /// @param cashOutCount The number of project tokens to cash out, as a fixed point number with 18 decimals.
158
- /// @param accountingContext The accounting context of the token being reclaimed by the cash out.
159
- /// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
160
- /// surplus being reclaimed from.
237
+ /// @param tokenToReclaim The token being reclaimed by the cash out.
161
238
  /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address. Passed through to data
162
239
  /// hooks so they can skip their own fees when value stays in the protocol (e.g. project-to-project routing).
163
240
  /// @param metadata Bytes to send to the data hook, if the project's current ruleset specifies one.
@@ -172,8 +249,7 @@ contract JBTerminalStore is IJBTerminalStore {
172
249
  address holder,
173
250
  uint256 projectId,
174
251
  uint256 cashOutCount,
175
- JBAccountingContext calldata accountingContext,
176
- JBAccountingContext[] calldata balanceAccountingContexts,
252
+ address tokenToReclaim,
177
253
  bool beneficiaryIsFeeless,
178
254
  bytes memory metadata
179
255
  )
@@ -191,8 +267,7 @@ contract JBTerminalStore is IJBTerminalStore {
191
267
  holder: holder,
192
268
  projectId: projectId,
193
269
  cashOutCount: cashOutCount,
194
- accountingContext: accountingContext,
195
- balanceAccountingContexts: balanceAccountingContexts,
270
+ tokenToReclaim: tokenToReclaim,
196
271
  beneficiaryIsFeeless: beneficiaryIsFeeless,
197
272
  metadata: metadata
198
273
  });
@@ -211,17 +286,17 @@ contract JBTerminalStore is IJBTerminalStore {
211
286
  }
212
287
 
213
288
  // The amount being reclaimed must be within the project's balance.
214
- if (balanceDiff > balanceOf[msg.sender][projectId][accountingContext.token]) {
289
+ if (balanceDiff > balanceOf[msg.sender][projectId][tokenToReclaim]) {
215
290
  revert JBTerminalStore_InadequateTerminalStoreBalance(
216
- balanceDiff, balanceOf[msg.sender][projectId][accountingContext.token]
291
+ balanceDiff, balanceOf[msg.sender][projectId][tokenToReclaim]
217
292
  );
218
293
  }
219
294
 
220
295
  // Remove the reclaimed funds from the project's balance.
221
296
  if (balanceDiff != 0) {
222
297
  unchecked {
223
- balanceOf[msg.sender][projectId][accountingContext.token] =
224
- balanceOf[msg.sender][projectId][accountingContext.token] - balanceDiff;
298
+ balanceOf[msg.sender][projectId][tokenToReclaim] =
299
+ balanceOf[msg.sender][projectId][tokenToReclaim] - balanceDiff;
225
300
  }
226
301
  }
227
302
  }
@@ -273,7 +348,7 @@ contract JBTerminalStore is IJBTerminalStore {
273
348
  /// This is safe because the entire transaction reverts atomically if the validation fails, but callers should
274
349
  /// be aware of this ordering.
275
350
  /// @param projectId The ID of the project that is paying out funds.
276
- /// @param accountingContext The context of the token being paid out.
351
+ /// @param token The token being paid out.
277
352
  /// @param amount The amount to pay out (use from the payout limit), as a fixed point number.
278
353
  /// @param currency The currency of the `amount`. This must match the project's current ruleset's currency.
279
354
  /// @return ruleset The ruleset the payout was made during, as a `JBRuleset` struct.
@@ -281,7 +356,7 @@ contract JBTerminalStore is IJBTerminalStore {
281
356
  /// decimals as its relative terminal.
282
357
  function recordPayoutFor(
283
358
  uint256 projectId,
284
- JBAccountingContext calldata accountingContext,
359
+ address token,
285
360
  uint256 amount,
286
361
  uint256 currency
287
362
  )
@@ -289,53 +364,50 @@ contract JBTerminalStore is IJBTerminalStore {
289
364
  override
290
365
  returns (JBRuleset memory ruleset, uint256 amountPaidOut)
291
366
  {
367
+ // Look up the accounting context from storage.
368
+ JBAccountingContext memory accountingContext = _accountingContextForTokenOf[msg.sender][projectId][token];
369
+
292
370
  // Get a reference to the project's current ruleset.
293
371
  ruleset = RULESETS.currentOf(projectId);
294
372
 
295
373
  // Convert the amount to the balance's currency.
296
374
  amountPaidOut = (currency == accountingContext.currency)
297
375
  ? amount
298
- : mulDiv(
299
- amount,
300
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the `_amount`'s
301
- // fidelity as possible when converting.
302
- PRICES.pricePerUnitOf({
376
+ : mulDiv({
377
+ x: amount,
378
+ y: 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
379
+ // `_amount`'s fidelity as possible when converting.
380
+ denominator: PRICES.pricePerUnitOf({
303
381
  projectId: projectId,
304
382
  pricingCurrency: currency,
305
383
  unitCurrency: accountingContext.currency,
306
384
  decimals: _MAX_FIXED_POINT_FIDELITY
307
385
  })
308
- );
386
+ });
309
387
 
310
388
  // The amount being paid out must be available.
311
- if (amountPaidOut > balanceOf[msg.sender][projectId][accountingContext.token]) {
389
+ if (amountPaidOut > balanceOf[msg.sender][projectId][token]) {
312
390
  revert JBTerminalStore_InadequateTerminalStoreBalance(
313
- amountPaidOut, balanceOf[msg.sender][projectId][accountingContext.token]
391
+ amountPaidOut, balanceOf[msg.sender][projectId][token]
314
392
  );
315
393
  }
316
394
 
317
395
  // Removed the paid out funds from the project's token balance.
318
396
  unchecked {
319
- balanceOf[msg.sender][projectId][accountingContext.token] =
320
- balanceOf[msg.sender][projectId][accountingContext.token] - amountPaidOut;
397
+ balanceOf[msg.sender][projectId][token] = balanceOf[msg.sender][projectId][token] - amountPaidOut;
321
398
  }
322
399
 
323
400
  // The new total amount which has been paid out during this ruleset.
324
401
  uint256 newUsedPayoutLimitOf =
325
- usedPayoutLimitOf[msg.sender][projectId][accountingContext.token][ruleset.cycleNumber][currency] + amount;
402
+ usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] + amount;
326
403
 
327
404
  // Store the new amount.
328
- usedPayoutLimitOf[msg.sender][projectId][accountingContext.token][ruleset.cycleNumber][currency] =
329
- newUsedPayoutLimitOf;
405
+ usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] = newUsedPayoutLimitOf;
330
406
 
331
407
  // Amount must be within what is still available to pay out.
332
408
  uint256 payoutLimit = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
333
409
  .payoutLimitOf({
334
- projectId: projectId,
335
- rulesetId: ruleset.id,
336
- terminal: msg.sender,
337
- token: accountingContext.token,
338
- currency: currency
410
+ projectId: projectId, rulesetId: ruleset.id, terminal: msg.sender, token: token, currency: currency
339
411
  });
340
412
 
341
413
  // Make sure the new used amount is within the payout limit.
@@ -368,8 +440,7 @@ contract JBTerminalStore is IJBTerminalStore {
368
440
  /// @notice Records a use of a project's surplus allowance.
369
441
  /// @dev When surplus allowance is "used", it is taken out of the project's surplus within a terminal.
370
442
  /// @param projectId The ID of the project to use the surplus allowance of.
371
- /// @param accountingContext The accounting context of the token whose balances should contribute to the surplus
372
- /// allowance being reclaimed from.
443
+ /// @param token The token whose balances should contribute to the surplus allowance being reclaimed from.
373
444
  /// @param amount The amount to use from the surplus allowance, as a fixed point number.
374
445
  /// @param currency The currency of the `amount`. Must match the currency of the surplus allowance.
375
446
  /// @return ruleset The ruleset during the surplus allowance is being used during, as a `JBRuleset` struct.
@@ -377,7 +448,7 @@ contract JBTerminalStore is IJBTerminalStore {
377
448
  /// as its relative terminal.
378
449
  function recordUsedAllowanceOf(
379
450
  uint256 projectId,
380
- JBAccountingContext calldata accountingContext,
451
+ address token,
381
452
  uint256 amount,
382
453
  uint256 currency
383
454
  )
@@ -385,23 +456,26 @@ contract JBTerminalStore is IJBTerminalStore {
385
456
  override
386
457
  returns (JBRuleset memory ruleset, uint256 usedAmount)
387
458
  {
459
+ // Look up the accounting context from storage.
460
+ JBAccountingContext memory accountingContext = _accountingContextForTokenOf[msg.sender][projectId][token];
461
+
388
462
  // Get a reference to the project's current ruleset.
389
463
  ruleset = RULESETS.currentOf(projectId);
390
464
 
391
465
  // Convert the amount to this store's terminal's token.
392
466
  usedAmount = currency == accountingContext.currency
393
467
  ? amount
394
- : mulDiv(
395
- amount,
396
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the `amount`'s
397
- // fidelity as possible when converting.
398
- PRICES.pricePerUnitOf({
468
+ : mulDiv({
469
+ x: amount,
470
+ y: 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
471
+ // `amount`'s fidelity as possible when converting.
472
+ denominator: PRICES.pricePerUnitOf({
399
473
  projectId: projectId,
400
474
  pricingCurrency: currency,
401
475
  unitCurrency: accountingContext.currency,
402
476
  decimals: _MAX_FIXED_POINT_FIDELITY
403
477
  })
404
- );
478
+ });
405
479
 
406
480
  // Set the token being used as the only one to look for surplus within.
407
481
  JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
@@ -420,25 +494,19 @@ contract JBTerminalStore is IJBTerminalStore {
420
494
  if (usedAmount > surplus) revert JBTerminalStore_InadequateTerminalStoreBalance(usedAmount, surplus);
421
495
 
422
496
  // Update the project's balance.
423
- balanceOf[msg.sender][projectId][accountingContext.token] =
424
- balanceOf[msg.sender][projectId][accountingContext.token] - usedAmount;
497
+ balanceOf[msg.sender][projectId][token] = balanceOf[msg.sender][projectId][token] - usedAmount;
425
498
 
426
499
  // Get a reference to the new used surplus allowance for this ruleset ID.
427
500
  uint256 newUsedSurplusAllowanceOf =
428
- usedSurplusAllowanceOf[msg.sender][projectId][accountingContext.token][ruleset.id][currency] + amount;
501
+ usedSurplusAllowanceOf[msg.sender][projectId][token][ruleset.id][currency] + amount;
429
502
 
430
503
  // Store the incremented value.
431
- usedSurplusAllowanceOf[msg.sender][projectId][accountingContext.token][ruleset.id][currency] =
432
- newUsedSurplusAllowanceOf;
504
+ usedSurplusAllowanceOf[msg.sender][projectId][token][ruleset.id][currency] = newUsedSurplusAllowanceOf;
433
505
 
434
506
  // There must be sufficient surplus allowance available.
435
507
  uint256 surplusAllowance = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
436
508
  .surplusAllowanceOf({
437
- projectId: projectId,
438
- rulesetId: ruleset.id,
439
- terminal: msg.sender,
440
- token: accountingContext.token,
441
- currency: currency
509
+ projectId: projectId, rulesetId: ruleset.id, terminal: msg.sender, token: token, currency: currency
442
510
  });
443
511
 
444
512
  // Make sure the new used amount is within the allowance.
@@ -451,6 +519,40 @@ contract JBTerminalStore is IJBTerminalStore {
451
519
  // ------------------------- external views -------------------------- //
452
520
  //*********************************************************************//
453
521
 
522
+ /// @notice Returns the accounting context for a terminal's project token.
523
+ /// @param terminal The terminal the accounting context applies to.
524
+ /// @param projectId The ID of the project.
525
+ /// @param token The token to get the accounting context for.
526
+ /// @return The accounting context.
527
+ function accountingContextOf(
528
+ address terminal,
529
+ uint256 projectId,
530
+ address token
531
+ )
532
+ external
533
+ view
534
+ override
535
+ returns (JBAccountingContext memory)
536
+ {
537
+ return _accountingContextForTokenOf[terminal][projectId][token];
538
+ }
539
+
540
+ /// @notice Returns all accounting contexts for a terminal's project.
541
+ /// @param terminal The terminal the accounting contexts apply to.
542
+ /// @param projectId The ID of the project.
543
+ /// @return The accounting contexts.
544
+ function accountingContextsOf(
545
+ address terminal,
546
+ uint256 projectId
547
+ )
548
+ external
549
+ view
550
+ override
551
+ returns (JBAccountingContext[] memory)
552
+ {
553
+ return _accountingContextsOf[terminal][projectId];
554
+ }
555
+
454
556
  /// @notice Returns the number of surplus terminal tokens that would be reclaimed by cashing out a given project's
455
557
  /// tokens based on its current ruleset and the given total project token supply and total terminal token surplus.
456
558
  /// @param projectId The ID of the project whose project tokens would be cashed out.
@@ -489,17 +591,13 @@ contract JBTerminalStore is IJBTerminalStore {
489
591
  });
490
592
  }
491
593
 
492
- /// @notice Returns the number of surplus terminal tokens that would be reclaimed from a terminal by cashing out a
493
- /// given number of tokens, based on the total token supply and total surplus.
494
- /// @dev The returned amount in terms of the specified `terminal`'s base currency.
495
- /// @dev The returned amount is represented as a fixed point number with the same amount of decimals as the
496
- /// specified terminal.
594
+ /// @notice Returns the number of surplus terminal tokens that would be reclaimed from terminals by cashing out a
595
+ /// given number of tokens, considering only specific tokens.
497
596
  /// @param projectId The ID of the project whose tokens would be cashed out.
498
597
  /// @param cashOutCount The number of tokens that would be cashed out, as a fixed point number with 18 decimals.
499
598
  /// @param terminals The terminals that would be cashed out from. If this is an empty array, surplus within all
500
599
  /// the project's terminals are considered.
501
- /// @param accountingContexts The accounting contexts of the surplus terminal tokens that would be reclaimed. Pass
502
- /// an empty array to use all of the project's accounting contexts.
600
+ /// @param tokens The tokens to include in the surplus calculation.
503
601
  /// @param decimals The number of decimals to include in the resulting fixed point number.
504
602
  /// @param currency The currency that the resulting number will be in terms of.
505
603
  /// @return The amount of surplus terminal tokens that would be reclaimed by cashing out `cashOutCount`
@@ -508,7 +606,7 @@ contract JBTerminalStore is IJBTerminalStore {
508
606
  uint256 projectId,
509
607
  uint256 cashOutCount,
510
608
  IJBTerminal[] calldata terminals,
511
- JBAccountingContext[] calldata accountingContexts,
609
+ address[] calldata tokens,
512
610
  uint256 decimals,
513
611
  uint256 currency
514
612
  )
@@ -517,18 +615,9 @@ contract JBTerminalStore is IJBTerminalStore {
517
615
  override
518
616
  returns (uint256)
519
617
  {
520
- // Get a reference to the project's current ruleset.
521
- JBRuleset memory ruleset = RULESETS.currentOf(projectId);
522
-
523
- // Get the current surplus amount.
524
- // If a terminal wasn't provided, use the total surplus across all terminals. Otherwise,
525
- // get the `terminal`'s surplus.
526
- uint256 currentSurplus = JBSurplus.currentSurplusOf({
527
- projectId: projectId,
528
- terminals: terminals.length != 0 ? terminals : DIRECTORY.terminalsOf(projectId),
529
- accountingContexts: accountingContexts,
530
- decimals: decimals,
531
- currency: currency
618
+ // Aggregate surplus across the terminals, optionally filtered by the specified tokens.
619
+ uint256 currentSurplus = _currentSurplusOf({
620
+ projectId: projectId, terminals: terminals, tokens: tokens, decimals: decimals, currency: currency
532
621
  });
533
622
 
534
623
  // If there's no surplus, nothing can be reclaimed.
@@ -541,12 +630,39 @@ contract JBTerminalStore is IJBTerminalStore {
541
630
  // Can't cash out more tokens than are in the total supply.
542
631
  if (cashOutCount > totalSupply) return 0;
543
632
 
633
+ // Get the cash out tax rate from the current ruleset.
634
+ uint256 cashOutTaxRate = RULESETS.currentOf(projectId).cashOutTaxRate();
635
+
544
636
  // Return the amount of surplus terminal tokens that would be reclaimed.
545
637
  return JBCashOuts.cashOutFrom({
546
638
  surplus: currentSurplus,
547
639
  cashOutCount: cashOutCount,
548
640
  totalSupply: totalSupply,
549
- cashOutTaxRate: ruleset.cashOutTaxRate()
641
+ cashOutTaxRate: cashOutTaxRate
642
+ });
643
+ }
644
+
645
+ /// @notice Gets the current surplus amount for a project across specified terminals and tokens.
646
+ /// @param projectId The ID of the project to get surplus for.
647
+ /// @param terminals The terminals to include. If empty, all project terminals are used.
648
+ /// @param tokens The tokens to include. If empty, all tokens per terminal are used.
649
+ /// @param decimals The number of decimals to expect in the resulting fixed point number.
650
+ /// @param currency The currency the resulting amount should be in terms of.
651
+ /// @return surplus The current surplus amount.
652
+ function currentSurplusOf(
653
+ uint256 projectId,
654
+ IJBTerminal[] calldata terminals,
655
+ address[] calldata tokens,
656
+ uint256 decimals,
657
+ uint256 currency
658
+ )
659
+ external
660
+ view
661
+ override
662
+ returns (uint256)
663
+ {
664
+ return _currentSurplusOf({
665
+ projectId: projectId, terminals: terminals, tokens: tokens, decimals: decimals, currency: currency
550
666
  });
551
667
  }
552
668
 
@@ -572,46 +688,12 @@ contract JBTerminalStore is IJBTerminalStore {
572
688
  projectId: projectId,
573
689
  cashOutCount: cashOutCount,
574
690
  terminals: new IJBTerminal[](0),
575
- accountingContexts: new JBAccountingContext[](0),
691
+ tokens: new address[](0),
576
692
  decimals: decimals,
577
693
  currency: currency
578
694
  });
579
695
  }
580
696
 
581
- /// @notice Gets the current surplus amount in a terminal for a specified project.
582
- /// @dev The surplus is the amount of funds a project has in a terminal in excess of its payout limit.
583
- /// @dev The surplus is represented as a fixed point number with the same amount of decimals as the specified
584
- /// terminal.
585
- /// @param terminal The terminal the surplus is being calculated for.
586
- /// @param projectId The ID of the project to get surplus for.
587
- /// @param accountingContexts The accounting contexts of tokens whose balances should contribute to the surplus
588
- /// being calculated.
589
- /// @param currency The currency the resulting amount should be in terms of.
590
- /// @param decimals The number of decimals to expect in the resulting fixed point number.
591
- /// @return The current surplus amount the project has in the specified terminal.
592
- function currentSurplusOf(
593
- address terminal,
594
- uint256 projectId,
595
- JBAccountingContext[] calldata accountingContexts,
596
- uint256 decimals,
597
- uint256 currency
598
- )
599
- external
600
- view
601
- override
602
- returns (uint256)
603
- {
604
- // Return the surplus during the project's current ruleset.
605
- return _surplusFrom({
606
- terminal: terminal,
607
- projectId: projectId,
608
- accountingContexts: accountingContexts,
609
- ruleset: RULESETS.currentOf(projectId),
610
- targetDecimals: decimals,
611
- targetCurrency: currency
612
- });
613
- }
614
-
615
697
  /// @notice Gets the current surplus amount for a specified project across all terminals.
616
698
  /// @param projectId The ID of the project to get the total surplus for.
617
699
  /// @param decimals The number of decimals that the fixed point surplus should include.
@@ -627,10 +709,10 @@ contract JBTerminalStore is IJBTerminalStore {
627
709
  override
628
710
  returns (uint256)
629
711
  {
630
- return JBSurplus.currentSurplusOf({
712
+ return _currentSurplusOf({
631
713
  projectId: projectId,
632
- terminals: DIRECTORY.terminalsOf(projectId),
633
- accountingContexts: new JBAccountingContext[](0),
714
+ terminals: new IJBTerminal[](0),
715
+ tokens: new address[](0),
634
716
  decimals: decimals,
635
717
  currency: currency
636
718
  });
@@ -639,11 +721,11 @@ contract JBTerminalStore is IJBTerminalStore {
639
721
  /// @notice Simulates a cash out without modifying state.
640
722
  /// @dev Invokes data hooks if configured, but skips the balance sufficiency check (balance may change between
641
723
  /// preview and execution).
724
+ /// @param terminal The terminal to simulate the cash out from.
642
725
  /// @param holder The address cashing out.
643
726
  /// @param projectId The ID of the project being cashed out from.
644
727
  /// @param cashOutCount The number of project tokens being cashed out.
645
- /// @param accountingContext The accounting context of the token being reclaimed.
646
- /// @param balanceAccountingContexts The accounting contexts to include in the balance calculation.
728
+ /// @param tokenToReclaim The token being reclaimed.
647
729
  /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
648
730
  /// @param metadata Extra data to pass along to the data hook.
649
731
  /// @return ruleset The project's current ruleset.
@@ -651,11 +733,11 @@ contract JBTerminalStore is IJBTerminalStore {
651
733
  /// @return cashOutTaxRate The cash out tax rate that would be applied.
652
734
  /// @return hookSpecifications Any cash out hook specifications from the data hook.
653
735
  function previewCashOutFrom(
736
+ address terminal,
654
737
  address holder,
655
738
  uint256 projectId,
656
739
  uint256 cashOutCount,
657
- JBAccountingContext calldata accountingContext,
658
- JBAccountingContext[] calldata balanceAccountingContexts,
740
+ address tokenToReclaim,
659
741
  bool beneficiaryIsFeeless,
660
742
  bytes calldata metadata
661
743
  )
@@ -670,12 +752,11 @@ contract JBTerminalStore is IJBTerminalStore {
670
752
  )
671
753
  {
672
754
  (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = _computeCashOutFrom({
673
- terminal: msg.sender,
755
+ terminal: terminal,
674
756
  holder: holder,
675
757
  projectId: projectId,
676
758
  cashOutCount: cashOutCount,
677
- accountingContext: accountingContext,
678
- balanceAccountingContexts: balanceAccountingContexts,
759
+ tokenToReclaim: tokenToReclaim,
679
760
  beneficiaryIsFeeless: beneficiaryIsFeeless,
680
761
  metadata: metadata
681
762
  });
@@ -684,6 +765,7 @@ contract JBTerminalStore is IJBTerminalStore {
684
765
  /// @notice Simulates a payment without modifying state.
685
766
  /// @dev Invokes data hooks if configured. Returns the same token count and hook specifications that
686
767
  /// `recordPaymentFrom` would produce.
768
+ /// @param terminal The terminal to simulate the payment from.
687
769
  /// @param payer The address of the payer.
688
770
  /// @param amount The amount being paid.
689
771
  /// @param projectId The ID of the project being paid.
@@ -693,6 +775,7 @@ contract JBTerminalStore is IJBTerminalStore {
693
775
  /// @return tokenCount The number of project tokens that would be minted, including reserved tokens.
694
776
  /// @return hookSpecifications Any pay hook specifications from the data hook.
695
777
  function previewPayFrom(
778
+ address terminal,
696
779
  address payer,
697
780
  JBTokenAmount memory amount,
698
781
  uint256 projectId,
@@ -704,9 +787,8 @@ contract JBTerminalStore is IJBTerminalStore {
704
787
  override
705
788
  returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
706
789
  {
707
- // Use the caller as the terminal context for the preview.
708
790
  (ruleset, tokenCount, hookSpecifications,) = _computePayFrom({
709
- terminal: msg.sender,
791
+ terminal: terminal,
710
792
  payer: payer,
711
793
  amount: amount,
712
794
  projectId: projectId,
@@ -719,6 +801,159 @@ contract JBTerminalStore is IJBTerminalStore {
719
801
  // -------------------------- internal views ------------------------- //
720
802
  //*********************************************************************//
721
803
 
804
+ /// @notice Computes the surplus relevant for a cash out (total or local, depending on ruleset flag).
805
+ /// @param terminal The terminal the cash out is being recorded from.
806
+ /// @param projectId The ID of the project being cashed out from.
807
+ /// @param tokenToReclaim The token being reclaimed.
808
+ /// @param ruleset The ruleset during the cash out.
809
+ /// @return The surplus amount in the token's native decimals and currency.
810
+ function _cashOutSurplusOf(
811
+ address terminal,
812
+ uint256 projectId,
813
+ address tokenToReclaim,
814
+ JBRuleset memory ruleset
815
+ )
816
+ internal
817
+ view
818
+ returns (uint256)
819
+ {
820
+ // Look up the accounting context (decimals, currency) for the token being reclaimed at this terminal.
821
+ JBAccountingContext memory accountingContext = _accountingContextForTokenOf[terminal][projectId][tokenToReclaim];
822
+
823
+ // If the ruleset uses total surplus, aggregate across ALL terminals and ALL tokens.
824
+ if (ruleset.useTotalSurplusForCashOuts()) {
825
+ return JBSurplus.currentSurplusOf({
826
+ projectId: projectId,
827
+ // Get every terminal the project has registered.
828
+ terminals: DIRECTORY.terminalsOf(projectId),
829
+ // Empty tokens array = include all tokens at each terminal.
830
+ tokens: new address[](0),
831
+ // Express the result in the reclaimed token's decimals and currency.
832
+ decimals: accountingContext.decimals,
833
+ currency: accountingContext.currency
834
+ });
835
+ }
836
+
837
+ // Otherwise, only account for the specific token's surplus at this terminal.
838
+ JBAccountingContext[] memory singleContext = new JBAccountingContext[](1);
839
+ singleContext[0] = accountingContext;
840
+
841
+ // Compute surplus from only this terminal using only the reclaimed token's balance.
842
+ return _surplusFrom({
843
+ terminal: terminal,
844
+ projectId: projectId,
845
+ accountingContexts: singleContext,
846
+ ruleset: ruleset,
847
+ targetDecimals: accountingContext.decimals,
848
+ targetCurrency: accountingContext.currency
849
+ });
850
+ }
851
+
852
+ /// @notice Computes cash out results without writing state.
853
+ /// @param terminal The terminal recording the cash out.
854
+ /// @param holder The account that is cashing out tokens.
855
+ /// @param projectId The ID of the project being cashed out from.
856
+ /// @param cashOutCount The number of project tokens to cash out.
857
+ /// @param tokenToReclaim The token being reclaimed.
858
+ /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
859
+ /// @param metadata Bytes to send to the data hook.
860
+ /// @return ruleset The ruleset during the cash out.
861
+ /// @return reclaimAmount The amount of tokens reclaimed.
862
+ /// @return cashOutTaxRate The cash out tax rate applied.
863
+ /// @return hookSpecifications Cash out hook specifications from the data hook.
864
+ function _computeCashOutFrom(
865
+ address terminal,
866
+ address holder,
867
+ uint256 projectId,
868
+ uint256 cashOutCount,
869
+ address tokenToReclaim,
870
+ bool beneficiaryIsFeeless,
871
+ bytes memory metadata
872
+ )
873
+ internal
874
+ view
875
+ returns (
876
+ JBRuleset memory ruleset,
877
+ uint256 reclaimAmount,
878
+ uint256 cashOutTaxRate,
879
+ JBCashOutHookSpecification[] memory hookSpecifications
880
+ )
881
+ {
882
+ // Get a reference to the project's current ruleset.
883
+ ruleset = RULESETS.currentOf(projectId);
884
+
885
+ // Compute surplus — delegated to keep stack shallow.
886
+ reclaimAmount = _cashOutSurplusOf({
887
+ terminal: terminal, projectId: projectId, tokenToReclaim: tokenToReclaim, ruleset: ruleset
888
+ });
889
+
890
+ // Scoped to keep `totalSupply` and `context` off the outer stack.
891
+ {
892
+ // Re-read accounting context for the data hook context (stack-safe in this scope).
893
+ JBAccountingContext memory accountingContext =
894
+ _accountingContextForTokenOf[terminal][projectId][tokenToReclaim];
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
+
934
+ // Noop specifications are informational only, so they can't also request forwarded funds.
935
+ for (uint256 i; i < hookSpecifications.length; i++) {
936
+ if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
937
+ revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
938
+ }
939
+ }
940
+ } else {
941
+ cashOutTaxRate = ruleset.cashOutTaxRate();
942
+ }
943
+
944
+ // Calculate the reclaim amount. `reclaimAmount` currently holds the surplus — overwrite it with the
945
+ // result.
946
+ if (reclaimAmount != 0) {
947
+ reclaimAmount = JBCashOuts.cashOutFrom({
948
+ surplus: reclaimAmount,
949
+ cashOutCount: cashOutCount,
950
+ totalSupply: totalSupply,
951
+ cashOutTaxRate: cashOutTaxRate
952
+ });
953
+ }
954
+ }
955
+ }
956
+
722
957
  /// @notice Computes payment results without writing state.
723
958
  /// @param terminal The terminal recording the payment.
724
959
  /// @param payer The address that made the payment.
@@ -836,124 +1071,59 @@ contract JBTerminalStore is IJBTerminalStore {
836
1071
  });
837
1072
 
838
1073
  // Find the number of tokens to mint, as a fixed point number with as many decimals as `weight` has.
839
- tokenCount = mulDiv(amount.value, weight, weightRatio);
1074
+ tokenCount = mulDiv({x: amount.value, y: weight, denominator: weightRatio});
840
1075
  }
841
1076
 
842
- /// @notice Computes cash out results without writing state.
843
- /// @param terminal The terminal recording the cash out.
844
- /// @param holder The account that is cashing out tokens.
845
- /// @param projectId The ID of the project being cashed out from.
846
- /// @param cashOutCount The number of project tokens to cash out.
847
- /// @param accountingContext The accounting context of the token being reclaimed.
848
- /// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
849
- /// surplus being reclaimed from.
850
- /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
851
- /// @param metadata Bytes to send to the data hook.
852
- /// @return ruleset The ruleset during the cash out.
853
- /// @return reclaimAmount The amount of tokens reclaimed.
854
- /// @return cashOutTaxRate The cash out tax rate applied.
855
- /// @return hookSpecifications Cash out hook specifications from the data hook.
856
- function _computeCashOutFrom(
857
- address terminal,
858
- address holder,
1077
+ /// @notice Gets the current surplus amount for a project across specified terminals and tokens.
1078
+ /// @param projectId The ID of the project to get surplus for.
1079
+ /// @param terminals The terminals to include. If empty, all project terminals are used.
1080
+ /// @param tokens The tokens to include. If empty, all tokens per terminal are used.
1081
+ /// @param decimals The number of decimals to expect in the resulting fixed point number.
1082
+ /// @param currency The currency the resulting amount should be in terms of.
1083
+ /// @return surplus The current surplus amount.
1084
+ function _currentSurplusOf(
859
1085
  uint256 projectId,
860
- uint256 cashOutCount,
861
- JBAccountingContext calldata accountingContext,
862
- JBAccountingContext[] calldata balanceAccountingContexts,
863
- bool beneficiaryIsFeeless,
864
- bytes memory metadata
1086
+ IJBTerminal[] memory terminals,
1087
+ address[] memory tokens,
1088
+ uint256 decimals,
1089
+ uint256 currency
865
1090
  )
866
1091
  internal
867
1092
  view
868
- returns (
869
- JBRuleset memory ruleset,
870
- uint256 reclaimAmount,
871
- uint256 cashOutTaxRate,
872
- JBCashOutHookSpecification[] memory hookSpecifications
873
- )
1093
+ returns (uint256 surplus)
874
1094
  {
875
- // Get a reference to the project's current ruleset.
876
- ruleset = RULESETS.currentOf(projectId);
877
-
878
- // Store the current surplus in `reclaimAmount` temporarily to avoid allocating a separate local variable
879
- // (saves one stack slot, which is needed to fit the 7th parameter without hitting stack-too-deep).
880
- reclaimAmount = ruleset.useTotalSurplusForCashOuts()
881
- ? JBSurplus.currentSurplusOf({
882
- projectId: projectId,
883
- terminals: DIRECTORY.terminalsOf(projectId),
884
- accountingContexts: new JBAccountingContext[](0),
885
- decimals: accountingContext.decimals,
886
- currency: accountingContext.currency
887
- })
888
- : _surplusFrom({
889
- terminal: terminal,
890
- projectId: projectId,
891
- accountingContexts: balanceAccountingContexts,
892
- ruleset: ruleset,
893
- targetDecimals: accountingContext.decimals,
894
- targetCurrency: accountingContext.currency
895
- });
896
-
897
- // Scoped to keep `totalSupply` and `context` off the outer stack.
898
- {
899
- // Get the total number of outstanding project tokens.
900
- uint256 totalSupply = IJBController(address(DIRECTORY.controllerOf(projectId)))
901
- .totalTokenSupplyWithReservedTokensOf(projectId);
902
-
903
- // Can't cash out more tokens than are in the supply.
904
- if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
905
-
906
- // SECURITY NOTE: The data hook has absolute control over cash-out economics.
907
- // It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
908
- // completely overriding the terminal's bonding curve math. For example, setting
909
- // totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
910
- // Project owners MUST audit their data hooks with the same rigor as the terminal.
911
-
912
- // If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
913
- if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
914
- // Build the cash out context field-by-field to avoid stack-too-deep
915
- // (the struct has 11 fields — a struct literal would require all values on the stack at once).
916
- JBBeforeCashOutRecordedContext memory context;
917
- context.terminal = terminal;
918
- context.holder = holder;
919
- context.projectId = projectId;
920
- context.rulesetId = ruleset.id;
921
- context.cashOutCount = cashOutCount;
922
- context.totalSupply = totalSupply;
923
- context.surplus = JBTokenAmount({
924
- token: accountingContext.token,
925
- value: reclaimAmount, // reclaimAmount temporarily holds the current surplus.
926
- decimals: accountingContext.decimals,
927
- currency: accountingContext.currency
928
- });
929
- context.useTotalSurplus = ruleset.useTotalSurplusForCashOuts();
930
- context.cashOutTaxRate = ruleset.cashOutTaxRate();
931
- context.beneficiaryIsFeeless = beneficiaryIsFeeless;
932
- context.metadata = metadata;
1095
+ // If specific terminals were provided, use them. Otherwise, get all terminals from the directory.
1096
+ IJBTerminal[] memory resolvedTerminals = terminals.length != 0 ? terminals : DIRECTORY.terminalsOf(projectId);
933
1097
 
934
- (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
935
- IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
1098
+ // The ruleset determines payout limits, which affect surplus. Fetch it once for all terminals.
1099
+ JBRuleset memory ruleset = RULESETS.currentOf(projectId);
936
1100
 
937
- // Noop specifications are informational only, so they can't also request forwarded funds.
938
- for (uint256 i; i < hookSpecifications.length; i++) {
939
- if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
940
- revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
941
- }
1101
+ // Sum surplus across each terminal.
1102
+ for (uint256 i; i < resolvedTerminals.length; i++) {
1103
+ address terminal = address(resolvedTerminals[i]);
1104
+
1105
+ // Build the list of accounting contexts to include in this terminal's surplus calculation.
1106
+ JBAccountingContext[] memory accountingContexts;
1107
+ if (tokens.length != 0) {
1108
+ // Specific tokens requested: look up each token's accounting context at this terminal.
1109
+ accountingContexts = new JBAccountingContext[](tokens.length);
1110
+ for (uint256 j; j < tokens.length; j++) {
1111
+ accountingContexts[j] = _accountingContextForTokenOf[terminal][projectId][tokens[j]];
942
1112
  }
943
1113
  } else {
944
- cashOutTaxRate = ruleset.cashOutTaxRate();
1114
+ // No token filter: use all accounting contexts registered at this terminal.
1115
+ accountingContexts = _accountingContextsOf[terminal][projectId];
945
1116
  }
946
1117
 
947
- // Calculate the reclaim amount. `reclaimAmount` currently holds the surplus overwrite it with the
948
- // result.
949
- if (reclaimAmount != 0) {
950
- reclaimAmount = JBCashOuts.cashOutFrom({
951
- surplus: reclaimAmount,
952
- cashOutCount: cashOutCount,
953
- totalSupply: totalSupply,
954
- cashOutTaxRate: cashOutTaxRate
955
- });
956
- }
1118
+ // Add this terminal's surplus (balance minus payout limits) converted to the target decimals/currency.
1119
+ surplus += _surplusFrom({
1120
+ terminal: terminal,
1121
+ projectId: projectId,
1122
+ accountingContexts: accountingContexts,
1123
+ ruleset: ruleset,
1124
+ targetDecimals: decimals,
1125
+ targetCurrency: currency
1126
+ });
957
1127
  }
958
1128
  }
959
1129
 
@@ -965,7 +1135,7 @@ contract JBTerminalStore is IJBTerminalStore {
965
1135
  /// @param projectId The ID of the project to get the surplus for.
966
1136
  /// @param accountingContexts The accounting contexts of tokens whose balances should contribute to the surplus
967
1137
  /// being calculated.
968
- /// @param ruleset The ID of the ruleset to base the surplus on.
1138
+ /// @param ruleset The ruleset to base the surplus on.
969
1139
  /// @param targetDecimals The number of decimals to include in the resulting fixed point number.
970
1140
  /// @param targetCurrency The currency that the reported surplus is expected to be in terms of.
971
1141
  /// @return surplus The surplus of funds in terms of `targetCurrency`, as a fixed point number with
@@ -1038,17 +1208,17 @@ contract JBTerminalStore is IJBTerminalStore {
1038
1208
  // Add up all the balances.
1039
1209
  surplus = (surplus == 0 || accountingContext.currency == targetCurrency)
1040
1210
  ? surplus
1041
- : mulDiv(
1042
- surplus,
1043
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
1211
+ : mulDiv({
1212
+ x: surplus,
1213
+ y: 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
1044
1214
  // `_payoutLimitRemaining`'s fidelity as possible when converting.
1045
- PRICES.pricePerUnitOf({
1215
+ denominator: PRICES.pricePerUnitOf({
1046
1216
  projectId: projectId,
1047
1217
  pricingCurrency: accountingContext.currency,
1048
1218
  unitCurrency: targetCurrency,
1049
1219
  decimals: _MAX_FIXED_POINT_FIDELITY
1050
1220
  })
1051
- );
1221
+ });
1052
1222
 
1053
1223
  // Get a reference to the payout limit during the ruleset for the token.
1054
1224
  JBCurrencyAmount[] memory payoutLimits = IJBController(address(DIRECTORY.controllerOf(projectId)))
@@ -1087,17 +1257,17 @@ contract JBTerminalStore is IJBTerminalStore {
1087
1257
 
1088
1258
  // Convert the `payoutLimit`'s amount to be in terms of the provided currency.
1089
1259
  if (payoutLimit.amount != 0 && payoutLimit.currency != targetCurrency) {
1090
- uint256 converted = mulDiv(
1091
- payoutLimit.amount,
1092
- 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
1260
+ uint256 converted = mulDiv({
1261
+ x: payoutLimit.amount,
1262
+ y: 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
1093
1263
  // `payoutLimitRemaining`'s fidelity as possible when converting.
1094
- PRICES.pricePerUnitOf({
1264
+ denominator: PRICES.pricePerUnitOf({
1095
1265
  projectId: projectId,
1096
1266
  pricingCurrency: payoutLimit.currency,
1097
1267
  unitCurrency: targetCurrency,
1098
1268
  decimals: _MAX_FIXED_POINT_FIDELITY
1099
1269
  })
1100
- );
1270
+ });
1101
1271
  if (converted > type(uint224).max) revert JBTerminalStore_Uint224Overflow(converted);
1102
1272
  // forge-lint: disable-next-line(unsafe-typecast)
1103
1273
  payoutLimit.amount = uint224(converted);