@bananapus/core-v6 0.0.20 → 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 +3 -4
  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
package/ADMINISTRATION.md CHANGED
@@ -258,7 +258,6 @@ The following values are set at deploy time and cannot be changed:
258
258
  - `PERMISSIONS` -- the permissions contract
259
259
  - `PERMIT2` -- the Uniswap Permit2 contract
260
260
  - `PROJECTS` -- the projects NFT contract
261
- - `RULESETS` -- derived from STORE.RULESETS()
262
261
  - `SPLITS` -- the splits contract
263
262
  - `STORE` -- the terminal store contract
264
263
  - `TOKENS` -- the tokens contract
@@ -263,7 +263,7 @@ These are the patterns that will trip you up if you are not aware of them:
263
263
  13. **`JBERC20` is cloned via `Clones.clone()`** -- constructor sets invalid name/symbol; real values set in `initialize()`.
264
264
  14. **Named returns auto-return** -- several functions use named return variables without explicit `return` statements.
265
265
  15. **Preview functions call data hooks** -- `previewPayFor`, `previewCashOutFrom`, and their store-level counterparts invoke data hooks during simulation. A reverting data hook will cause the preview to revert. Preview functions are `view` but still make external calls to hooks.
266
- 16. **Store preview functions use `msg.sender` as terminal** -- `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` use `msg.sender` (not an explicit parameter) as the terminal context. Only terminal contracts calling these will get correct balance/surplus lookups. Use the terminal-level `JBMultiTerminal.previewPayFor` / `previewCashOutFrom` instead for general-purpose previews.
266
+ 16. **Store preview functions take an explicit terminal parameter** -- `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` take an explicit `terminal` address for balance/surplus lookups. Callers must pass a registered terminal to get correct results. The terminal-level `JBMultiTerminal.previewPayFor` / `previewCashOutFrom` handle this automatically by passing `address(this)`.
267
267
 
268
268
  ## Priority Areas to Audit
269
269
 
package/CHANGE_LOG.md CHANGED
@@ -18,8 +18,8 @@ A new `_feeFreeSurplusOf` mapping (`projectId => token => uint256`) tracks cumul
18
18
 
19
19
  Two `view` functions on `JBTerminalStore` and `IJBTerminalStore`:
20
20
 
21
- - `previewPayFrom(address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` -- Simulates a payment and returns `(JBRuleset ruleset, uint256 tokenCount, JBPayHookSpecification[] hookSpecifications)`. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Does not modify state.
22
- - `previewCashOutFrom(address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bool beneficiaryIsFeeless, bytes metadata)` -- Simulates a cash out and returns `(JBRuleset ruleset, uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] hookSpecifications)`. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Does not modify state.
21
+ - `previewPayFrom(address terminal, address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` -- Simulates a payment and returns `(JBRuleset ruleset, uint256 tokenCount, JBPayHookSpecification[] hookSpecifications)`. Uses the explicit `terminal` parameter for balance/surplus lookups. Invokes data hooks if configured. Does not modify state.
22
+ - `previewCashOutFrom(address terminal, address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, bool beneficiaryIsFeeless, bytes metadata)` -- Simulates a cash out and returns `(JBRuleset ruleset, uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] hookSpecifications)`. Uses the explicit `terminal` parameter for balance/surplus lookups. Invokes data hooks if configured. Does not modify state.
23
23
 
24
24
  Internal computation logic was extracted into shared `_computePayFrom` and `_computeCashOutFrom` view helpers; the existing `recordPaymentFrom` and `recordCashOutFor` functions were refactored to call these helpers before writing state.
25
25
 
@@ -117,7 +117,7 @@ Parameters changed from `memory` to `calldata` for gas efficiency.
117
117
 
118
118
  | Function | Description |
119
119
  |----------|-------------|
120
- | `currentTotalReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 decimals, uint256 currency)` | Convenience view that returns the reclaimable surplus across all terminals using all accounting contexts. Delegates to `currentReclaimableSurplusOf` with empty `terminals` and `accountingContexts` arrays. Mirrors the `currentTotalSurplusOf` pattern. |
120
+ | `currentTotalReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 decimals, uint256 currency)` | Convenience view that returns the reclaimable surplus across all terminals using all tokens. Delegates to `currentReclaimableSurplusOf` with empty `terminals` and `tokens` arrays. Mirrors the `currentTotalSurplusOf` pattern. |
121
121
 
122
122
  #### IJBTokens / JBTokens
123
123
 
package/RISKS.md CHANGED
@@ -39,7 +39,7 @@ What must be true for the system to remain safe:
39
39
 
40
40
  ### Surplus Manipulation
41
41
 
42
- - **Cross-terminal surplus aggregation.** When `useTotalSurplusForCashOuts` is enabled, `recordCashOutFor` aggregates surplus across all terminals via `JBSurplus.currentSurplusOf()`, which calls `terminal.currentSurplusOf()` on each. If a malicious terminal is added to the project's directory, it could report inflated surplus. Defense: `InadequateTerminalStoreBalance` revert prevents extracting more than the actual terminal balance.
42
+ - **Cross-terminal surplus aggregation.** When `useTotalSurplusForCashOuts` is enabled, `recordCashOutFor` aggregates surplus across all terminals via `JBSurplus.currentSurplusOf()`, which calls `terminal.currentSurplusOf()` on each. If a malicious terminal is added to the project's directory, it could report inflated surplus. Defense: `InadequateTerminalStoreBalance` revert prevents extracting more than the actual terminal balance. When `useTotalSurplusForCashOuts` is false, only the single token being reclaimed contributes to the surplus calculation.
43
43
  - **Price feed inconsistency across terminals.** Different tokens in different terminals are converted to a common currency via `JBPrices`. If price feeds between terminals are stale or inconsistent, aggregated surplus can be inflated/deflated, affecting cash out reclaim amounts.
44
44
 
45
45
  ## 3. Reentrancy Surface
@@ -129,7 +129,7 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
129
129
  `JBMultiTerminal.previewPayFor`, `JBMultiTerminal.previewCashOutFrom`, and `JBController.previewMintOf` are `view` functions that simulate operations without modifying state. They compose the same computation paths as the real operations.
130
130
 
131
131
  - **Data hooks are called during previews.** `previewPayFor` and `previewCashOutFrom` invoke `beforePayRecordedWith` and `beforeCashOutRecordedWith` on data hooks. A reverting data hook causes the preview to revert. A gas-consuming hook can cause the preview to run out of gas.
132
- - **Store previews use `msg.sender` as terminal.** `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` use `msg.sender` for balance/surplus lookups. Only a registered terminal calling these will get correct results. External callers should use the terminal-level functions (`JBMultiTerminal.previewPayFor` / `previewCashOutFrom`) which handle this automatically.
132
+ - **Store previews take an explicit terminal parameter.** `JBTerminalStore.previewPayFrom` and `previewCashOutFrom` take an explicit `terminal` address for balance/surplus lookups. Callers must pass a registered terminal to get correct results. The terminal-level functions (`JBMultiTerminal.previewPayFor` / `previewCashOutFrom`) handle this automatically by passing `address(this)`.
133
133
  - **No state modification risk.** Preview functions cannot change balances, mint/burn tokens, or consume limits. They are safe to call from any context.
134
134
 
135
135
  ## 7. Integration Risks
@@ -149,7 +149,7 @@ No `ReentrancyGuard` is used. The system relies on state ordering and the `Inade
149
149
 
150
150
  ### Cross-Terminal Surplus Aggregation
151
151
 
152
- - `JBSurplus.currentSurplusOf` calls `terminal.currentSurplusOf()` on each terminal. These are external view calls with no gas limit. A malicious or gas-expensive terminal can cause this aggregation to revert, blocking cash outs for any project that has `useTotalSurplusForCashOuts` enabled and uses that terminal.
152
+ - `JBSurplus.currentSurplusOf` calls `terminal.currentSurplusOf()` on each terminal. These are external view calls with no gas limit. A malicious or gas-expensive terminal can cause this aggregation to revert, blocking cash outs for any project that has `useTotalSurplusForCashOuts` enabled and uses that terminal. When `useTotalSurplusForCashOuts` is false, surplus is computed internally by the store for only the reclaimed token, avoiding cross-terminal external calls.
153
153
  - The surplus calculation converts each terminal's balance to a common currency via price feeds. Rounding accumulates across terminals. With N terminals and M tokens each, there are N*M price conversions, each with up to 1 wei of rounding error.
154
154
 
155
155
  ### `recordAddedBalanceFor` Access Control
package/SKILLS.md CHANGED
@@ -66,7 +66,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
66
66
  | `migrateBalanceOf(uint256 projectId, address token, IJBTerminal to)` | Migrates a project's token balance to another terminal. Requires `allowTerminalMigration`. |
67
67
  | `processHeldFeesOf(uint256 projectId, address token, uint256 count)` | Processes up to `count` held fees for a project, sending them to the fee beneficiary project. |
68
68
  | `addAccountingContextsFor(uint256 projectId, JBAccountingContext[] accountingContexts)` | Adds new accounting contexts (token types) to a terminal for a project. |
69
- | `currentSurplusOf(uint256 projectId, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the project's current surplus in the specified currency. |
69
+ | `currentSurplusOf(uint256 projectId, address[] tokens, uint256 decimals, uint256 currency)` | Returns the project's current surplus in this terminal. Empty `tokens` = all tokens. |
70
70
  | `accountingContextForTokenOf(uint256 projectId, address token)` | Returns the accounting context for a specific token. |
71
71
  | `accountingContextsOf(uint256 projectId)` | Returns all accounting contexts for a project. |
72
72
  | `previewPayFor(uint256 projectId, address token, uint256 amount, address beneficiary, bytes metadata)` | Simulates a full payment including the reserved/beneficiary token split. Returns `(ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications)`. Composes `STORE.previewPayFrom` + `controller.previewMintOf`. |
@@ -87,12 +87,12 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
87
87
  | `usedPayoutLimitOf(address terminal, uint256 projectId, address token, uint256 rulesetCycleNumber, uint256 currency)` | Returns the used payout limit for a project in a given cycle. |
88
88
  | `usedSurplusAllowanceOf(address terminal, uint256 projectId, address token, uint256 rulesetId, uint256 currency)` | Returns the used surplus allowance for a project in a given ruleset. |
89
89
  | `currentReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 totalSupply, uint256 surplus)` | Returns the reclaimable surplus given raw total supply and surplus values. Applies bonding curve from current ruleset. |
90
- | `currentReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, IJBTerminal[] terminals, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the reclaimable surplus across specified terminals. Empty arrays default to all terminals/contexts. |
91
- | `currentTotalReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 decimals, uint256 currency)` | Convenience view: reclaimable surplus across all terminals using all accounting contexts. Mirrors `currentTotalSurplusOf`. |
92
- | `currentSurplusOf(address terminal, uint256 projectId, JBAccountingContext[] accountingContexts, uint256 decimals, uint256 currency)` | Returns the current surplus for a project at a terminal. |
93
- | `currentTotalSurplusOf(uint256 projectId, uint256 decimals, uint256 currency)` | Returns the total surplus across all terminals. |
94
- | `previewPayFrom(address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` | Simulates a payment without modifying state. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Returns ruleset, token count, and hook specifications. |
95
- | `previewCashOutFrom(address holder, uint256 projectId, uint256 cashOutCount, JBAccountingContext accountingContext, JBAccountingContext[] balanceAccountingContexts, bool beneficiaryIsFeeless, bytes metadata)` | Simulates a cash out without modifying state. Uses `msg.sender` as the terminal context. Invokes data hooks if configured. Returns ruleset, reclaim amount, tax rate, and hook specifications. |
90
+ | `currentReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, IJBTerminal[] terminals, address[] tokens, uint256 decimals, uint256 currency)` | Returns the reclaimable surplus across specified terminals and tokens. Empty arrays default to all. |
91
+ | `currentTotalReclaimableSurplusOf(uint256 projectId, uint256 cashOutCount, uint256 decimals, uint256 currency)` | Convenience view: reclaimable surplus across all terminals and all tokens. |
92
+ | `currentSurplusOf(uint256 projectId, IJBTerminal[] terminals, address[] tokens, uint256 decimals, uint256 currency)` | Returns the current surplus across specified terminals and tokens. Empty arrays default to all. |
93
+ | `currentTotalSurplusOf(uint256 projectId, uint256 decimals, uint256 currency)` | Convenience view: total surplus across all terminals and all tokens. |
94
+ | `previewPayFrom(address terminal, address payer, JBTokenAmount amount, uint256 projectId, address beneficiary, bytes metadata)` | Simulates a payment without modifying state. Uses the explicit `terminal` parameter for balance/surplus lookups. Invokes data hooks if configured. Returns ruleset, token count, and hook specifications. |
95
+ | `previewCashOutFrom(address terminal, address holder, uint256 projectId, uint256 cashOutCount, address tokenToReclaim, bool beneficiaryIsFeeless, bytes metadata)` | Simulates a cash out without modifying state. Uses the explicit `terminal` parameter for balance/surplus lookups. Invokes data hooks if configured. Returns ruleset, reclaim amount, tax rate, and hook specifications. |
96
96
 
97
97
  ### JBRulesets
98
98
 
@@ -246,7 +246,7 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
246
246
  - Project #1 is the fee beneficiary project (receives all protocol fees)
247
247
  - **Fee-free cashout exemption is scoped to fee-free intra-terminal payout amounts.** `_feeFreeSurplusOf[projectId][token]` accumulates the value of fee-free payouts. During cashout with `cashOutTaxRate=0`, the 2.5% fee applies only up to this surplus, then depletes. Once consumed, subsequent cashouts are fee-free again. This prevents a round-trip fee bypass (intra-terminal payout → zero-tax cashout) while scoping fees precisely to the fee-free inflow.
248
248
  - `JBProjects` constructor optionally mints project #1 to `feeProjectOwner` -- if `address(0)`, no fee project is created
249
- - `JBMultiTerminal` derives `DIRECTORY` and `RULESETS` from the provided `store` in its constructor -- not passed directly
249
+ - `JBMultiTerminal` derives `DIRECTORY` from the provided `store` in its constructor -- not passed directly
250
250
  - `JBPrices.pricePerUnitOf()` checks project-specific feed, then inverse, then falls back to `DEFAULT_PROJECT_ID = 0`
251
251
  - `useAllowanceOf()` takes 8 args including `address payable feeBeneficiary` -- do NOT omit it
252
252
  - Cash out tax rate of 0% = proportional (1:1) redemption; 100% = nothing reclaimable (all surplus locked). Do NOT confuse with a "cash out rate" where 100% means full redemption.
package/USER_JOURNEYS.md CHANGED
@@ -69,7 +69,7 @@ All user paths through the Juicebox V6 core protocol. For each journey: entry po
69
69
  - Data hook can return empty weight (0) to suppress minting while still recording payment
70
70
  - Fee-on-transfer tokens: actual amount received is `_balanceOf(token) - balanceBefore` (measured via balance diff)
71
71
 
72
- **Preview**: Call `JBMultiTerminal.previewPayFor(projectId, token, amount, beneficiary, metadata)` to simulate the full payment on-chain -- including data hook effects and the reserved/beneficiary token split. Returns `(ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications)`. This is a `view` function that does not modify state. For lower-level access without the mint split, the terminal delegates to `JBTerminalStore.previewPayFrom(payer, amount, projectId, beneficiary, metadata)` which returns the raw `(ruleset, tokenCount, hookSpecifications)`.
72
+ **Preview**: Call `JBMultiTerminal.previewPayFor(projectId, token, amount, beneficiary, metadata)` to simulate the full payment on-chain -- including data hook effects and the reserved/beneficiary token split. Returns `(ruleset, beneficiaryTokenCount, reservedTokenCount, hookSpecifications)`. This is a `view` function that does not modify state. For lower-level access without the mint split, the terminal delegates to `JBTerminalStore.previewPayFrom(terminal, payer, amount, projectId, beneficiary, metadata)` which returns the raw `(ruleset, tokenCount, hookSpecifications)`.
73
73
 
74
74
  ---
75
75
 
package/foundry.toml CHANGED
@@ -2,7 +2,6 @@
2
2
  solc = '0.8.26'
3
3
  evm_version = 'cancun'
4
4
  optimizer_runs = 200
5
- via_ir = true
6
5
  libs = ["node_modules", "lib"]
7
6
  fs_permissions = [{ access = "read-write", path = "./"}]
8
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,12 +4,10 @@ pragma solidity 0.8.26;
4
4
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
5
5
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
6
6
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
- import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
8
7
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9
8
  import {Address} from "@openzeppelin/contracts/utils/Address.sol";
10
9
  import {Context} from "@openzeppelin/contracts/utils/Context.sol";
11
10
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
12
- import {mulDiv} from "@prb/math/src/Common.sol";
13
11
  import {IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol";
14
12
  import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
15
13
 
@@ -25,7 +23,6 @@ import {IJBPermissioned} from "./interfaces/IJBPermissioned.sol";
25
23
  import {IJBPermissions} from "./interfaces/IJBPermissions.sol";
26
24
  import {IJBPermitTerminal} from "./interfaces/IJBPermitTerminal.sol";
27
25
  import {IJBProjects} from "./interfaces/IJBProjects.sol";
28
- import {IJBRulesets} from "./interfaces/IJBRulesets.sol";
29
26
  import {IJBSplitHook} from "./interfaces/IJBSplitHook.sol";
30
27
  import {IJBSplits} from "./interfaces/IJBSplits.sol";
31
28
  import {IJBTerminal} from "./interfaces/IJBTerminal.sol";
@@ -61,9 +58,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
61
58
  // --------------------------- custom errors ------------------------- //
62
59
  //*********************************************************************//
63
60
 
64
- error JBMultiTerminal_AccountingContextAlreadySet(address token);
65
- error JBMultiTerminal_AccountingContextDecimalsMismatch();
66
- error JBMultiTerminal_AddingAccountingContextNotAllowed();
67
61
  error JBMultiTerminal_FeeTerminalNotFound(address token);
68
62
  error JBMultiTerminal_NoMsgValueAllowed(uint256 value);
69
63
  error JBMultiTerminal_OverflowAlert(uint256 value, uint256 limit);
@@ -75,7 +69,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
75
69
  error JBMultiTerminal_UnderMinReturnedTokens(uint256 count, uint256 min);
76
70
  error JBMultiTerminal_UnderMinTokensPaidOut(uint256 amount, uint256 min);
77
71
  error JBMultiTerminal_UnderMinTokensReclaimed(uint256 amount, uint256 min);
78
- error JBMultiTerminal_ZeroAccountingContextCurrency();
79
72
 
80
73
  //*********************************************************************//
81
74
  // ------------------------- public constants ------------------------ //
@@ -112,9 +105,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
112
105
  /// @notice Mints ERC-721s that represent project ownership and transfers.
113
106
  IJBProjects public immutable override PROJECTS;
114
107
 
115
- /// @notice The contract storing and managing project rulesets.
116
- IJBRulesets public immutable override RULESETS;
117
-
118
108
  /// @notice The contract that stores splits for each project.
119
109
  IJBSplits public immutable override SPLITS;
120
110
 
@@ -128,15 +118,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
128
118
  // --------------------- internal stored properties ------------------ //
129
119
  //*********************************************************************//
130
120
 
131
- /// @notice Context describing how a token is accounted for by a project.
132
- /// @custom:param projectId The ID of the project that the token accounting context applies to.
133
- /// @custom:param token The address of the token being accounted for.
134
- mapping(uint256 projectId => mapping(address token => JBAccountingContext)) internal _accountingContextForTokenOf;
135
-
136
- /// @notice A list of tokens accepted by each project.
137
- /// @custom:param projectId The ID of the project to get a list of accepted tokens for.
138
- mapping(uint256 projectId => JBAccountingContext[]) internal _accountingContextsOf;
139
-
140
121
  /// @notice The cumulative amount of fee-free intra-terminal payouts a project has received for a given token.
141
122
  /// @dev Incremented each time a fee-free payout lands (same terminal, no fee charged). During cashout with
142
123
  /// `cashOutTaxRate == 0`, fees are applied only up to this amount, then decremented. This prevents a round-trip
@@ -189,7 +170,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
189
170
  DIRECTORY = store.DIRECTORY();
190
171
  FEELESS_ADDRESSES = feelessAddresses;
191
172
  PROJECTS = projects;
192
- RULESETS = store.RULESETS();
193
173
  SPLITS = splits;
194
174
  STORE = store;
195
175
  TOKENS = tokens;
@@ -221,66 +201,12 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
221
201
  alsoGrantAccessIf: _msgSender() == address(_controllerOf(projectId))
222
202
  });
223
203
 
224
- // Get a reference to the project's current ruleset.
225
- JBRuleset memory ruleset = RULESETS.currentOf(projectId);
226
-
227
- // Make sure that if there's a ruleset, it allows adding accounting contexts.
228
- if (ruleset.id != 0 && !ruleset.allowAddAccountingContext()) {
229
- revert JBMultiTerminal_AddingAccountingContextNotAllowed();
230
- }
204
+ // Record all accounting contexts in the store (validates each and reverts if invalid).
205
+ STORE.recordAccountingContextOf({projectId: projectId, contexts: accountingContexts});
231
206
 
232
- // Start accepting each token.
207
+ // Emit an event for each accounting context.
233
208
  for (uint256 i; i < accountingContexts.length; i++) {
234
- // Set the accounting context being iterated on.
235
- JBAccountingContext memory accountingContext = accountingContexts[i];
236
-
237
- // Get a storage reference to the currency accounting context for the token.
238
- JBAccountingContext storage storedAccountingContext =
239
- _accountingContextForTokenOf[projectId][accountingContext.token];
240
-
241
- // Make sure the token accounting context isn't already set.
242
- if (storedAccountingContext.token != address(0)) {
243
- revert JBMultiTerminal_AccountingContextAlreadySet(storedAccountingContext.token);
244
- }
245
-
246
- // Keep track of a flag indiciating if we know the provided decimals are incorrect.
247
- bool knownInvalidDecimals;
248
-
249
- // Check if the token is the native token and has the correct decimals
250
- if (accountingContext.token == JBConstants.NATIVE_TOKEN && accountingContext.decimals != 18) {
251
- knownInvalidDecimals = true;
252
- } else if (accountingContext.token != JBConstants.NATIVE_TOKEN) {
253
- // slither-disable-next-line calls-loop
254
- try IERC20Metadata(accountingContext.token).decimals() returns (uint8 decimals) {
255
- // slither-disable-next-line calls-loop
256
- if (accountingContext.decimals != decimals) {
257
- knownInvalidDecimals = true;
258
- }
259
- } catch {
260
- // The token didn't support `decimals`.
261
- // @dev Non-standard ERC20s that revert on `decimals()` will bypass decimal validation.
262
- // The caller is responsible for providing the correct decimals for such tokens.
263
- knownInvalidDecimals = false;
264
- }
265
- }
266
-
267
- // Make sure the decimals are correct.
268
- if (knownInvalidDecimals) {
269
- revert JBMultiTerminal_AccountingContextDecimalsMismatch();
270
- }
271
-
272
- // Make sure the currency is non-zero.
273
- if (accountingContext.currency == 0) revert JBMultiTerminal_ZeroAccountingContextCurrency();
274
-
275
- // Define the context from the config.
276
- storedAccountingContext.token = accountingContext.token;
277
- storedAccountingContext.decimals = accountingContext.decimals;
278
- storedAccountingContext.currency = accountingContext.currency;
279
-
280
- // Add the token to the list of accepted tokens of the project.
281
- _accountingContextsOf[projectId].push(storedAccountingContext);
282
-
283
- emit SetAccountingContext({projectId: projectId, context: storedAccountingContext, caller: _msgSender()});
209
+ emit SetAccountingContext({projectId: projectId, context: accountingContexts[i], caller: _msgSender()});
284
210
  }
285
211
  }
286
212
 
@@ -406,7 +332,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
406
332
  JBSplitHookContext memory context = JBSplitHookContext({
407
333
  token: token,
408
334
  amount: netPayoutAmount,
409
- decimals: _accountingContextForTokenOf[projectId][token].decimals,
335
+ decimals: STORE.accountingContextOf({terminal: address(this), projectId: projectId, token: token})
336
+ .decimals,
410
337
  projectId: projectId,
411
338
  groupId: uint256(uint160(token)),
412
339
  split: split
@@ -814,29 +741,27 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
814
741
  override
815
742
  returns (JBAccountingContext memory)
816
743
  {
817
- return _accountingContextForTokenOf[projectId][token];
744
+ return STORE.accountingContextOf({terminal: address(this), projectId: projectId, token: token});
818
745
  }
819
746
 
820
747
  /// @notice The tokens accepted by a project.
821
748
  /// @param projectId The ID of the project to get the accepted tokens of.
822
749
  /// @return tokenContexts The accounting contexts of the accepted tokens.
823
750
  function accountingContextsOf(uint256 projectId) external view override returns (JBAccountingContext[] memory) {
824
- return _accountingContextsOf[projectId];
751
+ return STORE.accountingContextsOf({terminal: address(this), projectId: projectId});
825
752
  }
826
753
 
827
- /// @notice Gets the total current surplus amount in this terminal for a project, in terms of a given currency.
828
- /// @dev This total surplus only includes tokens that the project accepts (as returned by
829
- /// `accountingContextsOf(...)`).
830
- /// @param projectId The ID of the project to get the current total surplus of.
831
- /// @param accountingContexts The accounting contexts to use to calculate the surplus. Pass an empty array to use
832
- /// all of the project's accounting contexts.
754
+ /// @notice Gets the current surplus amount in this terminal for a project, in terms of a given currency.
755
+ /// @dev If `tokens` is empty, includes all tokens the project accepts (as returned by `accountingContextsOf(...)`).
756
+ /// @param projectId The ID of the project to get the current surplus of.
757
+ /// @param tokens The tokens to include in the surplus calculation. If empty, all tokens are included.
833
758
  /// @param decimals The number of decimals to include in the fixed point returned value.
834
759
  /// @param currency The currency to express the returned value in terms of.
835
760
  /// @return The current surplus amount the project has in this terminal, in terms of `currency` and with the
836
761
  /// specified number of decimals.
837
762
  function currentSurplusOf(
838
763
  uint256 projectId,
839
- JBAccountingContext[] memory accountingContexts,
764
+ address[] calldata tokens,
840
765
  uint256 decimals,
841
766
  uint256 currency
842
767
  )
@@ -845,12 +770,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
845
770
  override
846
771
  returns (uint256)
847
772
  {
773
+ IJBTerminal[] memory self = new IJBTerminal[](1);
774
+ self[0] = IJBTerminal(address(this));
848
775
  return STORE.currentSurplusOf({
849
- terminal: address(this),
850
- projectId: projectId,
851
- accountingContexts: accountingContexts.length != 0 ? accountingContexts : _accountingContextsOf[projectId],
852
- decimals: decimals,
853
- currency: currency
776
+ projectId: projectId, terminals: self, tokens: tokens, decimals: decimals, currency: currency
854
777
  });
855
778
  }
856
779
 
@@ -859,6 +782,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
859
782
  /// @dev Held fees can be processed at any time by this terminal's owner.
860
783
  /// @param projectId The ID of the project that is holding fees.
861
784
  /// @param token The token that the fees are held in.
785
+ /// @param count The maximum number of held fees to return.
786
+ /// @return heldFees The held fees.
862
787
  function heldFeesOf(
863
788
  uint256 projectId,
864
789
  address token,
@@ -919,22 +844,16 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
919
844
  JBCashOutHookSpecification[] memory hookSpecifications
920
845
  )
921
846
  {
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
- });
847
+ (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) =
848
+ STORE.previewCashOutFrom({
849
+ terminal: address(this),
850
+ holder: holder,
851
+ projectId: projectId,
852
+ cashOutCount: cashOutCount,
853
+ tokenToReclaim: tokenToReclaim,
854
+ beneficiaryIsFeeless: _isFeeless(beneficiary),
855
+ metadata: metadata
856
+ });
938
857
  }
939
858
 
940
859
  /// @notice Simulates paying a project through this terminal without modifying state.
@@ -969,6 +888,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
969
888
 
970
889
  // Preview the payment through the store.
971
890
  (ruleset, tokenCount, hookSpecifications) = STORE.previewPayFrom({
891
+ terminal: address(this),
972
892
  payer: _msgSender(),
973
893
  amount: _tokenAmountOf({projectId: projectId, token: token, value: amount}),
974
894
  projectId: projectId,
@@ -1150,25 +1070,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1150
1070
  // Keep a reference to the cash out tax rate being used.
1151
1071
  uint256 cashOutTaxRate;
1152
1072
 
1153
- // Keep a reference to the accounting context of the token being reclaimed.
1154
- JBAccountingContext memory accountingContext =
1155
- _accountingContextOf({projectId: projectId, token: tokenToReclaim});
1156
-
1157
- // Scoped section prevents stack too deep.
1158
- {
1159
- JBAccountingContext[] memory balanceAccountingContexts = _accountingContextsOf[projectId];
1160
-
1161
- // Record the cash out.
1162
- (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
1163
- holder: holder,
1164
- projectId: projectId,
1165
- accountingContext: accountingContext,
1166
- balanceAccountingContexts: balanceAccountingContexts,
1167
- cashOutCount: cashOutCount,
1168
- beneficiaryIsFeeless: _isFeeless(beneficiary),
1169
- metadata: metadata
1170
- });
1171
- }
1073
+ // Record the cash out.
1074
+ (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
1075
+ holder: holder,
1076
+ projectId: projectId,
1077
+ cashOutCount: cashOutCount,
1078
+ tokenToReclaim: tokenToReclaim,
1079
+ beneficiaryIsFeeless: _isFeeless(beneficiary),
1080
+ metadata: metadata
1081
+ });
1172
1082
 
1173
1083
  // Burn the project tokens.
1174
1084
  if (cashOutCount != 0) {
@@ -1215,11 +1125,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1215
1125
  ruleset: ruleset,
1216
1126
  cashOutTaxRate: cashOutTaxRate,
1217
1127
  beneficiary: beneficiary,
1218
- beneficiaryReclaimAmount: JBTokenAmount({
1219
- token: tokenToReclaim,
1220
- decimals: accountingContext.decimals,
1221
- currency: accountingContext.currency,
1222
- value: reclaimAmount
1128
+ beneficiaryReclaimAmount: _tokenAmountOf({
1129
+ projectId: projectId, token: tokenToReclaim, value: reclaimAmount
1223
1130
  }),
1224
1131
  specifications: hookSpecifications,
1225
1132
  metadata: metadata
@@ -1251,6 +1158,52 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1251
1158
  });
1252
1159
  }
1253
1160
 
1161
+ /// @notice Fund a project either by calling this terminal's internal `addToBalance` function or by calling the
1162
+ /// recipient terminal's `addToBalance` function.
1163
+ /// @param terminal The terminal on which the project is expecting to receive funds.
1164
+ /// @param projectId The ID of the project being funded.
1165
+ /// @param token The token being used.
1166
+ /// @param amount The amount being funded, as a fixed point number with the amount of decimals that the terminal's
1167
+ /// accounting context specifies.
1168
+ /// @param metadata Additional metadata to include with the payment.
1169
+ function _efficientAddToBalance(
1170
+ IJBTerminal terminal,
1171
+ uint256 projectId,
1172
+ address token,
1173
+ uint256 amount,
1174
+ bytes memory metadata
1175
+ )
1176
+ internal
1177
+ {
1178
+ // Call the internal method if this terminal is being used.
1179
+ if (terminal == IJBTerminal(address(this))) {
1180
+ _addToBalanceOf({
1181
+ projectId: projectId,
1182
+ token: token,
1183
+ amount: amount,
1184
+ shouldReturnHeldFees: false,
1185
+ memo: "",
1186
+ metadata: metadata
1187
+ });
1188
+ } else {
1189
+ // Trigger any inherited pre-transfer logic.
1190
+ // Keep a reference to the amount that'll be paid as a `msg.value`.
1191
+ // slither-disable-next-line reentrancy-events
1192
+ uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
1193
+
1194
+ // Add to balance.
1195
+ // If this terminal's token is the native token, send it in `msg.value`.
1196
+ terminal.addToBalanceOf{value: payValue}({
1197
+ projectId: projectId,
1198
+ token: token,
1199
+ amount: amount,
1200
+ shouldReturnHeldFees: false,
1201
+ memo: "",
1202
+ metadata: metadata
1203
+ });
1204
+ }
1205
+ }
1206
+
1254
1207
  /// @notice Pay a project either by calling this terminal's internal `pay` function or by calling the recipient
1255
1208
  /// terminal's `pay` function.
1256
1209
  /// @param terminal The terminal on which the project is expecting to receive payments.
@@ -1588,52 +1541,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1588
1541
  }
1589
1542
  }
1590
1543
 
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
-
1637
1544
  /// @notice Records an added balance for a project.
1638
1545
  /// @param projectId The ID of the project to record the added balance for.
1639
1546
  /// @param token The token to record the added balance for.
@@ -1721,6 +1628,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1721
1628
  });
1722
1629
  }
1723
1630
 
1631
+ /// @notice Sends payouts to a project's payout split group using the specified ruleset.
1724
1632
  /// @param projectId The ID of the project to send the payouts of.
1725
1633
  /// @param token The token being paid out.
1726
1634
  /// @param amount The number of terminal tokens to pay out, as a fixed point number with same number of decimals as
@@ -1741,12 +1649,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1741
1649
  JBRuleset memory ruleset;
1742
1650
 
1743
1651
  // Record the payout.
1744
- (ruleset, amountPaidOut) = STORE.recordPayoutFor({
1745
- projectId: projectId,
1746
- accountingContext: _accountingContextForTokenOf[projectId][token],
1747
- amount: amount,
1748
- currency: currency
1749
- });
1652
+ (ruleset, amountPaidOut) =
1653
+ STORE.recordPayoutFor({projectId: projectId, token: token, amount: amount, currency: currency});
1750
1654
 
1751
1655
  // Get a reference to the project's owner.
1752
1656
  // The owner will receive tokens minted by paying the platform fee and receive any leftover funds not sent to
@@ -1943,12 +1847,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1943
1847
  uint256 amountPaidOut;
1944
1848
 
1945
1849
  // Record the use of the allowance.
1946
- (ruleset, amountPaidOut) = STORE.recordUsedAllowanceOf({
1947
- projectId: projectId,
1948
- accountingContext: _accountingContextForTokenOf[projectId][token],
1949
- amount: amount,
1950
- currency: currency
1951
- });
1850
+ (ruleset, amountPaidOut) =
1851
+ STORE.recordUsedAllowanceOf({projectId: projectId, token: token, amount: amount, currency: currency});
1952
1852
 
1953
1853
  // Take a fee from the `amountPaidOut`, if needed.
1954
1854
  // The net amount is the final amount withdrawn after the fee has been taken.
@@ -2001,7 +1901,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
2001
1901
  returns (JBAccountingContext memory context)
2002
1902
  {
2003
1903
  // Keep a reference to the accounting context configured for the token.
2004
- context = _accountingContextForTokenOf[projectId][token];
1904
+ context = STORE.accountingContextOf({terminal: address(this), projectId: projectId, token: token});
2005
1905
 
2006
1906
  // Revert if the token is not accepted by the project.
2007
1907
  if (context.token == address(0)) revert JBMultiTerminal_TokenNotAccepted(token);