@bananapus/core-v6 0.0.19 → 0.0.21

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 (44) 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 +17 -4
  5. package/README.md +12 -2
  6. package/RISKS.md +10 -2
  7. package/SKILLS.md +5 -2
  8. package/USER_JOURNEYS.md +4 -2
  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 +17 -7
  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 +1 -5
  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/TestTerminalPreviewParityFork.sol +109 -0
  29. package/test/units/static/JBController/TestPreviewMintOf.sol +116 -0
  30. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +144 -25
  31. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +11 -1
  32. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +15 -2
  33. package/test/units/static/JBMultiTerminal/TestPay.sol +64 -2
  34. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +116 -0
  35. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +98 -0
  36. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +11 -2
  37. package/test/units/static/JBRulesets/TestCurrentOf.sol +8 -6
  38. package/test/units/static/JBRulesets/TestRulesets.sol +25 -24
  39. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +4 -17
  40. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +49 -2
  41. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +96 -4
  42. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +81 -32
  43. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +113 -2
  44. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +227 -5
@@ -34,6 +34,7 @@ import {IJBTokens} from "./interfaces/IJBTokens.sol";
34
34
  import {JBConstants} from "./libraries/JBConstants.sol";
35
35
  import {JBFees} from "./libraries/JBFees.sol";
36
36
  import {JBMetadataResolver} from "./libraries/JBMetadataResolver.sol";
37
+ import {JBPayoutSplitGroupLib} from "./libraries/JBPayoutSplitGroupLib.sol";
37
38
  import {JBRulesetMetadataResolver} from "./libraries/JBRulesetMetadataResolver.sol";
38
39
  import {JBAccountingContext} from "./structs/JBAccountingContext.sol";
39
40
  import {JBAfterPayRecordedContext} from "./structs/JBAfterPayRecordedContext.sol";
@@ -889,6 +890,97 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
889
890
  }
890
891
  }
891
892
 
893
+ /// @notice Simulates cashing out project tokens from this terminal without modifying state.
894
+ /// @param holder The address whose tokens are being cashed out.
895
+ /// @param projectId The ID of the project whose tokens are being cashed out.
896
+ /// @param cashOutCount The number of project tokens to cash out.
897
+ /// @param tokenToReclaim The token to reclaim from the project's surplus.
898
+ /// @param beneficiary The address that would receive the reclaimed tokens.
899
+ /// @param metadata Extra data to send to the data hook and cash out hooks.
900
+ /// @return ruleset The project's current ruleset.
901
+ /// @return reclaimAmount The amount of tokens that would be reclaimed from the project's surplus.
902
+ /// @return cashOutTaxRate The cash out tax rate that would be applied.
903
+ /// @return hookSpecifications Any cash out hook specifications from the data hook.
904
+ function previewCashOutFrom(
905
+ address holder,
906
+ uint256 projectId,
907
+ uint256 cashOutCount,
908
+ address tokenToReclaim,
909
+ address payable beneficiary,
910
+ bytes calldata metadata
911
+ )
912
+ external
913
+ view
914
+ override
915
+ returns (
916
+ JBRuleset memory ruleset,
917
+ uint256 reclaimAmount,
918
+ uint256 cashOutTaxRate,
919
+ JBCashOutHookSpecification[] memory hookSpecifications
920
+ )
921
+ {
922
+ // Keep a reference to the accounting context for the reclaimed token.
923
+ JBAccountingContext memory accountingContext =
924
+ _accountingContextOf({projectId: projectId, token: tokenToReclaim});
925
+
926
+ // Keep a reference to the accounting contexts to include in the balance calculation.
927
+ JBAccountingContext[] memory balanceAccountingContexts = _accountingContextsOf[projectId];
928
+
929
+ (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.previewCashOutFrom({
930
+ holder: holder,
931
+ projectId: projectId,
932
+ cashOutCount: cashOutCount,
933
+ accountingContext: accountingContext,
934
+ balanceAccountingContexts: balanceAccountingContexts,
935
+ beneficiaryIsFeeless: _isFeeless(beneficiary),
936
+ metadata: metadata
937
+ });
938
+ }
939
+
940
+ /// @notice Simulates paying a project through this terminal without modifying state.
941
+ /// @param projectId The ID of the project being paid.
942
+ /// @param token The token being paid in.
943
+ /// @param amount The amount of tokens being paid.
944
+ /// @param beneficiary The address to mint project tokens to.
945
+ /// @param metadata Extra data to pass along to the data hook and pay hooks.
946
+ /// @return ruleset The project's current ruleset.
947
+ /// @return beneficiaryTokenCount The number of project tokens that would be minted for the beneficiary.
948
+ /// @return reservedTokenCount The number of project tokens that would be reserved.
949
+ /// @return hookSpecifications Any pay hook specifications from the data hook.
950
+ function previewPayFor(
951
+ uint256 projectId,
952
+ address token,
953
+ uint256 amount,
954
+ address beneficiary,
955
+ bytes calldata metadata
956
+ )
957
+ external
958
+ view
959
+ override
960
+ returns (
961
+ JBRuleset memory ruleset,
962
+ uint256 beneficiaryTokenCount,
963
+ uint256 reservedTokenCount,
964
+ JBPayHookSpecification[] memory hookSpecifications
965
+ )
966
+ {
967
+ // Keep a reference to the token count returned by the store preview.
968
+ uint256 tokenCount;
969
+
970
+ // Preview the payment through the store.
971
+ (ruleset, tokenCount, hookSpecifications) = STORE.previewPayFrom({
972
+ payer: _msgSender(),
973
+ amount: _tokenAmountOf({projectId: projectId, token: token, value: amount}),
974
+ projectId: projectId,
975
+ beneficiary: beneficiary,
976
+ metadata: metadata
977
+ });
978
+
979
+ // Split the token count into beneficiary and reserved portions using the controller preview.
980
+ (beneficiaryTokenCount, reservedTokenCount) = _controllerOf(projectId)
981
+ .previewMintOf({projectId: projectId, tokenCount: tokenCount, useReservedPercent: true});
982
+ }
983
+
892
984
  //*********************************************************************//
893
985
  // -------------------------- public views --------------------------- //
894
986
  //*********************************************************************//
@@ -924,9 +1016,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
924
1016
  returns (uint256)
925
1017
  {
926
1018
  // Make sure the project has an accounting context for the token being paid.
927
- if (_accountingContextForTokenOf[projectId][token].token == address(0)) {
928
- revert JBMultiTerminal_TokenNotAccepted(token);
929
- }
1019
+ _accountingContextOf({projectId: projectId, token: token});
930
1020
 
931
1021
  // If the terminal's token is the native token, override `amount` with `msg.value`.
932
1022
  if (token == JBConstants.NATIVE_TOKEN) return msg.value;
@@ -1061,7 +1151,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1061
1151
  uint256 cashOutTaxRate;
1062
1152
 
1063
1153
  // Keep a reference to the accounting context of the token being reclaimed.
1064
- JBAccountingContext memory accountingContext = _accountingContextForTokenOf[projectId][tokenToReclaim];
1154
+ JBAccountingContext memory accountingContext =
1155
+ _accountingContextOf({projectId: projectId, token: tokenToReclaim});
1065
1156
 
1066
1157
  // Scoped section prevents stack too deep.
1067
1158
  {
@@ -1160,53 +1251,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1160
1251
  });
1161
1252
  }
1162
1253
 
1163
- /// @notice Fund a project either by calling this terminal's internal `addToBalance` function or by calling the
1164
- /// recipient
1165
- /// terminal's `addToBalance` function.
1166
- /// @param terminal The terminal on which the project is expecting to receive funds.
1167
- /// @param projectId The ID of the project being funded.
1168
- /// @param token The token being used.
1169
- /// @param amount The amount being funded, as a fixed point number with the amount of decimals that the terminal's
1170
- /// accounting context specifies.
1171
- /// @param metadata Additional metadata to include with the payment.
1172
- function _efficientAddToBalance(
1173
- IJBTerminal terminal,
1174
- uint256 projectId,
1175
- address token,
1176
- uint256 amount,
1177
- bytes memory metadata
1178
- )
1179
- internal
1180
- {
1181
- // Call the internal method if this terminal is being used.
1182
- if (terminal == IJBTerminal(address(this))) {
1183
- _addToBalanceOf({
1184
- projectId: projectId,
1185
- token: token,
1186
- amount: amount,
1187
- shouldReturnHeldFees: false,
1188
- memo: "",
1189
- metadata: metadata
1190
- });
1191
- } else {
1192
- // Trigger any inherited pre-transfer logic.
1193
- // Keep a reference to the amount that'll be paid as a `msg.value`.
1194
- // slither-disable-next-line reentrancy-events
1195
- uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
1196
-
1197
- // Add to balance.
1198
- // If this terminal's token is the native token, send it in `msg.value`.
1199
- terminal.addToBalanceOf{value: payValue}({
1200
- projectId: projectId,
1201
- token: token,
1202
- amount: amount,
1203
- shouldReturnHeldFees: false,
1204
- memo: "",
1205
- metadata: metadata
1206
- });
1207
- }
1208
- }
1209
-
1210
1254
  /// @notice Pay a project either by calling this terminal's internal `pay` function or by calling the recipient
1211
1255
  /// terminal's `pay` function.
1212
1256
  /// @param terminal The terminal on which the project is expecting to receive payments.
@@ -1301,6 +1345,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1301
1345
  // Set the specification being iterated on.
1302
1346
  JBCashOutHookSpecification memory specification = specifications[i];
1303
1347
 
1348
+ // A noop specification is informational only and doesn't trigger the hook.
1349
+ if (specification.noop) continue;
1350
+
1304
1351
  // Get the fee for the specified amount.
1305
1352
  uint256 specificationAmountFee = _isFeeless(address(specification.hook))
1306
1353
  ? 0
@@ -1384,6 +1431,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1384
1431
  // Set the specification being iterated on.
1385
1432
  JBPayHookSpecification memory specification = specifications[i];
1386
1433
 
1434
+ // A noop specification is informational only and doesn't trigger the hook.
1435
+ if (specification.noop) continue;
1436
+
1387
1437
  // Pass the correct token `forwardedAmount` to the hook.
1388
1438
  context.forwardedAmount = JBTokenAmount({
1389
1439
  value: specification.amount,
@@ -1438,17 +1488,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1438
1488
  internal
1439
1489
  {
1440
1490
  // Keep a reference to the token amount to forward to the store.
1441
- JBTokenAmount memory tokenAmount;
1442
-
1443
- // Scoped section prevents stack too deep. `context` only used within scope.
1444
- {
1445
- // Get a reference to the token's accounting context.
1446
- JBAccountingContext memory context = _accountingContextForTokenOf[projectId][token];
1447
-
1448
- // Bundle the amount info into a `JBTokenAmount` struct.
1449
- tokenAmount =
1450
- JBTokenAmount({token: token, decimals: context.decimals, currency: context.currency, value: amount});
1451
- }
1491
+ JBTokenAmount memory tokenAmount = _tokenAmountOf({projectId: projectId, token: token, value: amount});
1452
1492
 
1453
1493
  // Record the payment.
1454
1494
  // Keep a reference to the ruleset the payment is being made during.
@@ -1548,6 +1588,52 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1548
1588
  }
1549
1589
  }
1550
1590
 
1591
+ /// @notice Fund a project either by calling this terminal's internal `addToBalance` function or by calling the
1592
+ /// recipient terminal's `addToBalance` function.
1593
+ /// @param terminal The terminal on which the project is expecting to receive funds.
1594
+ /// @param projectId The ID of the project being funded.
1595
+ /// @param token The token being used.
1596
+ /// @param amount The amount being funded, as a fixed point number with the amount of decimals that the terminal's
1597
+ /// accounting context specifies.
1598
+ /// @param metadata Additional metadata to include with the payment.
1599
+ function _efficientAddToBalance(
1600
+ IJBTerminal terminal,
1601
+ uint256 projectId,
1602
+ address token,
1603
+ uint256 amount,
1604
+ bytes memory metadata
1605
+ )
1606
+ internal
1607
+ {
1608
+ // Call the internal method if this terminal is being used.
1609
+ if (terminal == IJBTerminal(address(this))) {
1610
+ _addToBalanceOf({
1611
+ projectId: projectId,
1612
+ token: token,
1613
+ amount: amount,
1614
+ shouldReturnHeldFees: false,
1615
+ memo: "",
1616
+ metadata: metadata
1617
+ });
1618
+ } else {
1619
+ // Trigger any inherited pre-transfer logic.
1620
+ // Keep a reference to the amount that'll be paid as a `msg.value`.
1621
+ // slither-disable-next-line reentrancy-events
1622
+ uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
1623
+
1624
+ // Add to balance.
1625
+ // If this terminal's token is the native token, send it in `msg.value`.
1626
+ terminal.addToBalanceOf{value: payValue}({
1627
+ projectId: projectId,
1628
+ token: token,
1629
+ amount: amount,
1630
+ shouldReturnHeldFees: false,
1631
+ memo: "",
1632
+ metadata: metadata
1633
+ });
1634
+ }
1635
+ }
1636
+
1551
1637
  /// @notice Records an added balance for a project.
1552
1638
  /// @param projectId The ID of the project to record the added balance for.
1553
1639
  /// @param token The token to record the added balance for.
@@ -1635,54 +1721,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1635
1721
  });
1636
1722
  }
1637
1723
 
1638
- /// @notice Sends payouts to a project's current payout split group, according to its ruleset, up to its current
1639
- /// payout limit.
1640
- /// @dev If the percentages of the splits in the project's payout split group do not add up to 100%, the remainder
1641
- /// is sent to the project's owner.
1642
- /// @dev Anyone can send payouts on a project's behalf. Projects can include a wildcard split (a split with no
1643
- /// `hook`, `projectId`, or `beneficiary`) to send funds to the `_msgSender()` which calls this function. This can
1644
- /// be used to incentivize calling this function.
1645
- /// @dev Payouts sent to addresses which aren't feeless incur the protocol fee.
1646
- /// @notice Sends a payout to a split.
1647
- /// @param split The split to pay.
1648
- /// @param projectId The ID of the project the split was specified by.
1649
- /// @param token The address of the token being paid out.
1650
- /// @param amount The total amount that the split is being paid, as a fixed point number with the same number of
1651
- /// decimals as this terminal.
1652
- /// @return netPayoutAmount The amount sent to the split after subtracting fees.
1653
- function _sendPayoutToSplit(
1654
- JBSplit memory split,
1655
- uint256 projectId,
1656
- address token,
1657
- uint256 amount
1658
- )
1659
- internal
1660
- returns (uint256)
1661
- {
1662
- // Failed split payouts consume the payout limit by design. The try-catch prevents a single
1663
- // split from DoS-ing the entire payout. Failed splits' amounts are returned to the project balance via
1664
- // `_recordAddedBalanceFor`. Payout limit consumption is correct because the project authorized the
1665
- // distribution.
1666
- // slither-disable-next-line reentrancy-events
1667
- try this.executePayout({
1668
- split: split, projectId: projectId, token: token, amount: amount, originalMessageSender: _msgSender()
1669
- }) returns (
1670
- uint256 netPayoutAmount
1671
- ) {
1672
- return netPayoutAmount;
1673
- } catch (bytes memory failureReason) {
1674
- emit PayoutReverted({
1675
- projectId: projectId, split: split, amount: amount, reason: failureReason, caller: _msgSender()
1676
- });
1677
-
1678
- // Add balance back to the project.
1679
- _recordAddedBalanceFor({projectId: projectId, token: token, amount: amount});
1680
-
1681
- // Since the payout failed the netPayoutAmount is zero.
1682
- return 0;
1683
- }
1684
- }
1685
-
1686
1724
  /// @param projectId The ID of the project to send the payouts of.
1687
1725
  /// @param token The token being paid out.
1688
1726
  /// @param amount The number of terminal tokens to pay out, as a fixed point number with same number of decimals as
@@ -1725,8 +1763,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1725
1763
 
1726
1764
  // Send payouts to the splits and get a reference to the amount left over after the splits have been paid.
1727
1765
  // Also get a reference to the amount which was paid out to splits that is eligible for fees.
1728
- (uint256 leftoverPayoutAmount, uint256 amountEligibleForFees) = _sendPayoutsToSplitGroupOf({
1729
- projectId: projectId, token: token, rulesetId: ruleset.id, amount: amountPaidOut
1766
+ (uint256 leftoverPayoutAmount, uint256 amountEligibleForFees) = JBPayoutSplitGroupLib.sendPayoutsToSplitGroupOf({
1767
+ splits: SPLITS,
1768
+ store: STORE,
1769
+ projectId: projectId,
1770
+ token: token,
1771
+ rulesetId: ruleset.id,
1772
+ amount: amountPaidOut,
1773
+ caller: _msgSender()
1730
1774
  });
1731
1775
 
1732
1776
  // Send any leftover funds to the project owner and update the fee tracking accordingly.
@@ -1782,73 +1826,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1782
1826
  });
1783
1827
  }
1784
1828
 
1785
- /// @notice Sends payouts to the payout splits group specified in a project's ruleset.
1786
- /// @param projectId The ID of the project to send the payouts of.
1787
- /// @param token The address of the token being paid out.
1788
- /// @param rulesetId The ID of the ruleset of the split group being paid.
1789
- /// @param amount The total amount being paid out, as a fixed point number with the same number of decimals as this
1790
- /// terminal.
1791
- /// @return amount The leftover amount (zero if the splits add up to 100%).
1792
- /// @return amountEligibleForFees The total amount of funds which were paid out and are eligible for fees.
1793
- function _sendPayoutsToSplitGroupOf(
1794
- uint256 projectId,
1795
- address token,
1796
- uint256 rulesetId,
1797
- uint256 amount
1798
- )
1799
- internal
1800
- returns (uint256, uint256 amountEligibleForFees)
1801
- {
1802
- // The total percentage available to split
1803
- uint256 leftoverPercentage = JBConstants.SPLITS_TOTAL_PERCENT;
1804
-
1805
- // Get a reference to the project's payout splits.
1806
- JBSplit[] memory splits =
1807
- SPLITS.splitsOf({projectId: projectId, rulesetId: rulesetId, groupId: uint256(uint160(token))});
1808
-
1809
- // Transfer between all splits.
1810
- for (uint256 i; i < splits.length; i++) {
1811
- // Get a reference to the split being iterated on.
1812
- JBSplit memory split = splits[i];
1813
-
1814
- // The amount to send to the split.
1815
- uint256 payoutAmount = mulDiv(amount, split.percent, leftoverPercentage);
1816
-
1817
- // The final payout amount after taking out any fees.
1818
- uint256 netPayoutAmount =
1819
- _sendPayoutToSplit({split: split, projectId: projectId, token: token, amount: payoutAmount});
1820
-
1821
- // If the split hook is a feeless address, this payout doesn't incur a fee.
1822
- if (netPayoutAmount != 0 && netPayoutAmount != payoutAmount) {
1823
- amountEligibleForFees += payoutAmount;
1824
- }
1825
-
1826
- if (payoutAmount != 0) {
1827
- // Subtract from the amount to be sent to the beneficiary.
1828
- unchecked {
1829
- amount -= payoutAmount;
1830
- }
1831
- }
1832
-
1833
- unchecked {
1834
- // Decrement the leftover percentage.
1835
- leftoverPercentage -= split.percent;
1836
- }
1837
-
1838
- emit SendPayoutToSplit({
1839
- projectId: projectId,
1840
- rulesetId: rulesetId,
1841
- group: uint256(uint160(token)),
1842
- split: split,
1843
- amount: payoutAmount,
1844
- netAmount: netPayoutAmount,
1845
- caller: _msgSender()
1846
- });
1847
- }
1848
-
1849
- return (amount, amountEligibleForFees);
1850
- }
1851
-
1852
1829
  /// @notice Takes a fee into the platform's project (with the `_FEE_BENEFICIARY_PROJECT_ID`).
1853
1830
  /// @param projectId The ID of the project paying the fee.
1854
1831
  /// @param token The address of the token that the fee is being paid in.
@@ -2011,6 +1988,25 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
2011
1988
  // -------------------------- internal views ------------------------- //
2012
1989
  //*********************************************************************//
2013
1990
 
1991
+ /// @notice Returns a project's accounting context for a token, reverting if it is not accepted.
1992
+ /// @param projectId The ID of the project to get the accounting context for.
1993
+ /// @param token The token to get the accounting context for.
1994
+ /// @return context The project's accounting context for the token.
1995
+ function _accountingContextOf(
1996
+ uint256 projectId,
1997
+ address token
1998
+ )
1999
+ internal
2000
+ view
2001
+ returns (JBAccountingContext memory context)
2002
+ {
2003
+ // Keep a reference to the accounting context configured for the token.
2004
+ context = _accountingContextForTokenOf[projectId][token];
2005
+
2006
+ // Revert if the token is not accepted by the project.
2007
+ if (context.token == address(0)) revert JBMultiTerminal_TokenNotAccepted(token);
2008
+ }
2009
+
2014
2010
  /// @notice Checks this terminal's balance of a specific token.
2015
2011
  /// @param token The address of the token to get this terminal's balance of.
2016
2012
  /// @return This terminal's balance.
@@ -2064,4 +2060,26 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
2064
2060
  function _primaryTerminalOf(uint256 projectId, address token) internal view returns (IJBTerminal) {
2065
2061
  return DIRECTORY.primaryTerminalOf({projectId: projectId, token: token});
2066
2062
  }
2063
+
2064
+ /// @notice Packages a payment amount with the token's accounting context.
2065
+ /// @param projectId The ID of the project the token amount belongs to.
2066
+ /// @param token The token being paid.
2067
+ /// @param value The token amount's value.
2068
+ /// @return tokenAmount The packaged token amount.
2069
+ function _tokenAmountOf(
2070
+ uint256 projectId,
2071
+ address token,
2072
+ uint256 value
2073
+ )
2074
+ internal
2075
+ view
2076
+ returns (JBTokenAmount memory tokenAmount)
2077
+ {
2078
+ // Keep a reference to the token's accounting context.
2079
+ JBAccountingContext memory context = _accountingContextOf({projectId: projectId, token: token});
2080
+
2081
+ // Bundle the amount info into a `JBTokenAmount` struct.
2082
+ tokenAmount =
2083
+ JBTokenAmount({token: token, decimals: context.decimals, currency: context.currency, value: value});
2084
+ }
2067
2085
  }
@@ -10,6 +10,7 @@ import {IJBRulesetDataHook} from "./interfaces/IJBRulesetDataHook.sol";
10
10
  import {IJBRulesets} from "./interfaces/IJBRulesets.sol";
11
11
  import {IJBTerminal} from "./interfaces/IJBTerminal.sol";
12
12
  import {IJBTerminalStore} from "./interfaces/IJBTerminalStore.sol";
13
+ import {JBConstants} from "./libraries/JBConstants.sol";
13
14
  import {JBFixedPointNumber} from "./libraries/JBFixedPointNumber.sol";
14
15
  import {JBCashOuts} from "./libraries/JBCashOuts.sol";
15
16
  import {JBRulesetMetadataResolver} from "./libraries/JBRulesetMetadataResolver.sol";
@@ -38,6 +39,7 @@ contract JBTerminalStore is IJBTerminalStore {
38
39
  error JBTerminalStore_InadequateTerminalStoreBalance(uint256 amount, uint256 balance);
39
40
  error JBTerminalStore_InsufficientTokens(uint256 count, uint256 totalSupply);
40
41
  error JBTerminalStore_InvalidAmountToForwardHook(uint256 amount, uint256 paidAmount);
42
+ error JBTerminalStore_NoopHookSpecHasAmount(uint256 amount);
41
43
  error JBTerminalStore_RulesetNotFound(uint256 projectId);
42
44
  error JBTerminalStore_RulesetPaymentPaused();
43
45
  error JBTerminalStore_TerminalMigrationNotAllowed();
@@ -637,7 +639,6 @@ contract JBTerminalStore is IJBTerminalStore {
637
639
  /// @notice Simulates a cash out without modifying state.
638
640
  /// @dev Invokes data hooks if configured, but skips the balance sufficiency check (balance may change between
639
641
  /// preview and execution).
640
- /// @param terminal The terminal address to simulate the cash out from.
641
642
  /// @param holder The address cashing out.
642
643
  /// @param projectId The ID of the project being cashed out from.
643
644
  /// @param cashOutCount The number of project tokens being cashed out.
@@ -650,7 +651,6 @@ contract JBTerminalStore is IJBTerminalStore {
650
651
  /// @return cashOutTaxRate The cash out tax rate that would be applied.
651
652
  /// @return hookSpecifications Any cash out hook specifications from the data hook.
652
653
  function previewCashOutFrom(
653
- address terminal,
654
654
  address holder,
655
655
  uint256 projectId,
656
656
  uint256 cashOutCount,
@@ -670,7 +670,7 @@ contract JBTerminalStore is IJBTerminalStore {
670
670
  )
671
671
  {
672
672
  (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = _computeCashOutFrom({
673
- terminal: terminal,
673
+ terminal: msg.sender,
674
674
  holder: holder,
675
675
  projectId: projectId,
676
676
  cashOutCount: cashOutCount,
@@ -684,17 +684,15 @@ contract JBTerminalStore is IJBTerminalStore {
684
684
  /// @notice Simulates a payment without modifying state.
685
685
  /// @dev Invokes data hooks if configured. Returns the same token count and hook specifications that
686
686
  /// `recordPaymentFrom` would produce.
687
- /// @param terminal The terminal address to simulate the payment from.
688
687
  /// @param payer The address of the payer.
689
688
  /// @param amount The amount being paid.
690
689
  /// @param projectId The ID of the project being paid.
691
690
  /// @param beneficiary The address to mint project tokens to.
692
691
  /// @param metadata Extra data to pass along to the data hook.
693
692
  /// @return ruleset The project's current ruleset.
694
- /// @return tokenCount The number of project tokens that would be minted.
693
+ /// @return tokenCount The number of project tokens that would be minted, including reserved tokens.
695
694
  /// @return hookSpecifications Any pay hook specifications from the data hook.
696
695
  function previewPayFrom(
697
- address terminal,
698
696
  address payer,
699
697
  JBTokenAmount memory amount,
700
698
  uint256 projectId,
@@ -706,8 +704,9 @@ contract JBTerminalStore is IJBTerminalStore {
706
704
  override
707
705
  returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications)
708
706
  {
707
+ // Use the caller as the terminal context for the preview.
709
708
  (ruleset, tokenCount, hookSpecifications,) = _computePayFrom({
710
- terminal: terminal,
709
+ terminal: msg.sender,
711
710
  payer: payer,
712
711
  amount: amount,
713
712
  projectId: projectId,
@@ -798,6 +797,10 @@ contract JBTerminalStore is IJBTerminalStore {
798
797
  // Ensure that the specifications have valid amounts.
799
798
  for (uint256 i; i < numberOfSpecifications; i++) {
800
799
  // Get a reference to the specification's amount.
800
+ if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
801
+ revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
802
+ }
803
+
801
804
  uint256 specifiedAmount = hookSpecifications[i].amount;
802
805
 
803
806
  // Ensure the amount is non-zero.
@@ -930,6 +933,13 @@ contract JBTerminalStore is IJBTerminalStore {
930
933
 
931
934
  (cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
932
935
  IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
936
+
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
+ }
942
+ }
933
943
  } else {
934
944
  cashOutTaxRate = ruleset.cashOutTaxRate();
935
945
  }
@@ -4,6 +4,8 @@ pragma solidity ^0.8.0;
4
4
  import {IJBCashOutHook} from "./IJBCashOutHook.sol";
5
5
  import {IJBTerminal} from "./IJBTerminal.sol";
6
6
  import {JBAfterCashOutRecordedContext} from "../structs/JBAfterCashOutRecordedContext.sol";
7
+ import {JBCashOutHookSpecification} from "../structs/JBCashOutHookSpecification.sol";
8
+ import {JBRuleset} from "../structs/JBRuleset.sol";
7
9
 
8
10
  /// @notice A terminal that can be cashed out from.
9
11
  interface IJBCashOutTerminal is IJBTerminal {
@@ -45,6 +47,34 @@ interface IJBCashOutTerminal is IJBTerminal {
45
47
  address caller
46
48
  );
47
49
 
50
+ /// @notice Simulates cashing out project tokens from this terminal without modifying state.
51
+ /// @param holder The address whose tokens are being cashed out.
52
+ /// @param projectId The ID of the project whose tokens are being cashed out.
53
+ /// @param cashOutCount The number of project tokens to cash out.
54
+ /// @param tokenToReclaim The token to reclaim from the project's surplus.
55
+ /// @param beneficiary The address that would receive the reclaimed tokens.
56
+ /// @param metadata Extra data to send to the data hook and cash out hooks.
57
+ /// @return ruleset The project's current ruleset.
58
+ /// @return reclaimAmount The amount of tokens that would be reclaimed from the project's surplus.
59
+ /// @return cashOutTaxRate The cash out tax rate that would be applied.
60
+ /// @return hookSpecifications Any cash out hook specifications from the data hook.
61
+ function previewCashOutFrom(
62
+ address holder,
63
+ uint256 projectId,
64
+ uint256 cashOutCount,
65
+ address tokenToReclaim,
66
+ address payable beneficiary,
67
+ bytes calldata metadata
68
+ )
69
+ external
70
+ view
71
+ returns (
72
+ JBRuleset memory ruleset,
73
+ uint256 reclaimAmount,
74
+ uint256 cashOutTaxRate,
75
+ JBCashOutHookSpecification[] memory hookSpecifications
76
+ );
77
+
48
78
  /// @notice Cashes out a holder's tokens for a project, reclaiming the token's proportional share of the project's
49
79
  /// surplus.
50
80
  /// @param holder The address whose tokens are being cashed out.
@@ -217,6 +217,21 @@ interface IJBController is IERC165, IJBProjectUriRegistry, IJBDirectoryAccessCon
217
217
  /// @return The pending reserved token balance.
218
218
  function pendingReservedTokenBalanceOf(uint256 projectId) external view returns (uint256);
219
219
 
220
+ /// @notice Previews how many beneficiary and reserved tokens `mintTokensOf(...)` would produce.
221
+ /// @param projectId The ID of the project whose tokens are being minted.
222
+ /// @param tokenCount The number of tokens to mint, including any reserved tokens.
223
+ /// @param useReservedPercent Whether to apply the ruleset's reserved percent.
224
+ /// @return beneficiaryTokenCount The number of tokens that would be minted for the beneficiary.
225
+ /// @return reservedTokenCount The number of tokens that would be reserved.
226
+ function previewMintOf(
227
+ uint256 projectId,
228
+ uint256 tokenCount,
229
+ bool useReservedPercent
230
+ )
231
+ external
232
+ view
233
+ returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount);
234
+
220
235
  /// @notice Returns a project's total token supply including pending reserved tokens.
221
236
  /// @param projectId The ID of the project to get the total token supply of.
222
237
  /// @return The total supply including pending reserved tokens.
@@ -6,6 +6,8 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6
6
  import {IJBPayHook} from "./IJBPayHook.sol";
7
7
  import {JBAccountingContext} from "../structs/JBAccountingContext.sol";
8
8
  import {JBAfterPayRecordedContext} from "../structs/JBAfterPayRecordedContext.sol";
9
+ import {JBPayHookSpecification} from "../structs/JBPayHookSpecification.sol";
10
+ import {JBRuleset} from "../structs/JBRuleset.sol";
9
11
 
10
12
  /// @notice A terminal that accepts payments and can be migrated.
11
13
  interface IJBTerminal is IERC165 {
@@ -102,6 +104,32 @@ interface IJBTerminal is IERC165 {
102
104
  view
103
105
  returns (uint256);
104
106
 
107
+ /// @notice Simulates paying a project through this terminal without modifying state.
108
+ /// @param projectId The ID of the project being paid.
109
+ /// @param token The token being paid in.
110
+ /// @param amount The amount of tokens being paid.
111
+ /// @param beneficiary The address to mint project tokens to.
112
+ /// @param metadata Extra data to pass along to the data hook and pay hooks.
113
+ /// @return ruleset The project's current ruleset.
114
+ /// @return beneficiaryTokenCount The number of project tokens that would be minted for the beneficiary.
115
+ /// @return reservedTokenCount The number of project tokens that would be reserved.
116
+ /// @return hookSpecifications Any pay hook specifications from the data hook.
117
+ function previewPayFor(
118
+ uint256 projectId,
119
+ address token,
120
+ uint256 amount,
121
+ address beneficiary,
122
+ bytes calldata metadata
123
+ )
124
+ external
125
+ view
126
+ returns (
127
+ JBRuleset memory ruleset,
128
+ uint256 beneficiaryTokenCount,
129
+ uint256 reservedTokenCount,
130
+ JBPayHookSpecification[] memory hookSpecifications
131
+ );
132
+
105
133
  /// @notice Adds accounting contexts for a project's tokens.
106
134
  /// @param projectId The ID of the project to add accounting contexts for.
107
135
  /// @param accountingContexts The accounting contexts to add.