@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
@@ -10,7 +10,6 @@ import {IJBController} from "../../../../src/interfaces/IJBController.sol";
10
10
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
11
11
  import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
12
12
  import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
13
- import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
14
13
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
15
14
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
16
15
  import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
@@ -22,7 +21,6 @@ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
22
21
  import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
23
22
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
24
23
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
25
- import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
26
24
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
27
25
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
28
26
 
@@ -54,29 +52,28 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
54
52
  address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(this))
55
53
  );
56
54
 
57
- if (token != JBConstants.NATIVE_TOKEN) {
58
- mockExpect(token, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(decimals));
55
+ // Mock ERC20 transfer for non-contract token addresses (needed for SafeERC20 calls later).
56
+ if (token != JBConstants.NATIVE_TOKEN && token.code.length == 0) {
57
+ vm.mockCall(token, abi.encodeWithSelector(IERC20.transfer.selector), abi.encode(true));
59
58
  }
60
59
 
61
60
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
62
61
  _tokens[0] = JBAccountingContext({token: token, decimals: decimals, currency: currency});
63
62
 
64
- JBRuleset memory returnedRuleset = JBRuleset({
65
- cycleNumber: 1,
66
- id: 0,
67
- basedOnId: 0,
68
- start: 0,
69
- duration: 0,
70
- weight: 0,
71
- weightCutPercent: 0,
72
- approvalHook: IJBRulesetApprovalHook(address(0)),
73
- metadata: 0
74
- });
75
-
76
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
63
+ // Mock recordAccountingContextOf in the store (validation now happens there)
64
+ mockExpect(
65
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
66
+ );
77
67
 
78
68
  vm.prank(address(this));
79
69
  _terminal.addAccountingContextsFor(_projectId, _tokens);
70
+
71
+ // Mock accountingContextOf for subsequent reads (not all code paths call it, so use mockCall only)
72
+ vm.mockCall(
73
+ address(store),
74
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, token)),
75
+ abi.encode(_tokens[0])
76
+ );
80
77
  }
81
78
 
82
79
  function test_WhenCallerDNHavePermission() external {
@@ -125,7 +122,8 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
125
122
  uint256 reclaimAmount = 1e9;
126
123
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](0);
127
124
  JBAccountingContext memory mockTokenContext =
128
- JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
125
+ // forge-lint: disable-next-line(unsafe-typecast)
126
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
129
127
  JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
130
128
  mockBalanceContext[0] = mockTokenContext;
131
129
  JBRuleset memory returnedRuleset = JBRuleset({
@@ -148,7 +146,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
148
146
  address(store),
149
147
  abi.encodeCall(
150
148
  IJBTerminalStore.recordCashOutFor,
151
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
149
+ (_holder, _projectId, _defaultAmount, mockTokenContext.token, true, "")
152
150
  ),
153
151
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
154
152
  );
@@ -165,6 +163,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
165
163
 
166
164
  // put code at mockToken address to pass OZ Address check
167
165
  vm.etch(_mockToken, abi.encode(1));
166
+ // forge-lint: disable-next-line(unsafe-typecast)
168
167
  _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
169
168
 
170
169
  vm.prank(_bene);
@@ -177,7 +176,8 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
177
176
  uint256 reclaimAmount = 1e9;
178
177
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](0);
179
178
  JBAccountingContext memory mockTokenContext =
180
- JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
179
+ // forge-lint: disable-next-line(unsafe-typecast)
180
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
181
181
  JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
182
182
  mockBalanceContext[0] = mockTokenContext;
183
183
  JBRuleset memory returnedRuleset = JBRuleset({
@@ -200,7 +200,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
200
200
  address(store),
201
201
  abi.encodeCall(
202
202
  IJBTerminalStore.recordCashOutFor,
203
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
203
+ (_holder, _projectId, _defaultAmount, mockTokenContext.token, true, "")
204
204
  ),
205
205
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
206
206
  );
@@ -217,6 +217,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
217
217
 
218
218
  // put code at mockToken address to pass OZ Address check
219
219
  vm.etch(_mockToken, abi.encode(1));
220
+ // forge-lint: disable-next-line(unsafe-typecast)
220
221
  _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
221
222
 
222
223
  vm.expectRevert(
@@ -238,7 +239,8 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
238
239
  uint256 reclaimAmount = 1e9;
239
240
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](0);
240
241
  JBAccountingContext memory mockTokenContext =
241
- JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
242
+ // forge-lint: disable-next-line(unsafe-typecast)
243
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
242
244
  JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
243
245
  mockBalanceContext[0] = mockTokenContext;
244
246
  JBRuleset memory returnedRuleset = JBRuleset({
@@ -261,7 +263,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
261
263
  address(store),
262
264
  abi.encodeCall(
263
265
  IJBTerminalStore.recordCashOutFor,
264
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, false, "")
266
+ (_holder, _projectId, _defaultAmount, mockTokenContext.token, false, "")
265
267
  ),
266
268
  abi.encode(returnedRuleset, reclaimAmount, _halfCashOutTaxRate, hookSpecifications)
267
269
  );
@@ -278,6 +280,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
278
280
 
279
281
  // put code at mockToken address to pass OZ Address check
280
282
  vm.etch(_mockToken, abi.encode(1));
283
+ // forge-lint: disable-next-line(unsafe-typecast)
281
284
  _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
282
285
 
283
286
  // get fee amount
@@ -378,7 +381,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
378
381
  address(store),
379
382
  abi.encodeCall(
380
383
  IJBTerminalStore.recordCashOutFor,
381
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
384
+ (_holder, _projectId, _defaultAmount, mockTokenContext.token, true, "")
382
385
  ),
383
386
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
384
387
  );
@@ -495,7 +498,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
495
498
  address(store),
496
499
  abi.encodeCall(
497
500
  IJBTerminalStore.recordCashOutFor,
498
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
501
+ (_holder, _projectId, _defaultAmount, mockTokenContext.token, true, "")
499
502
  ),
500
503
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
501
504
  );
@@ -583,7 +586,8 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
583
586
  hookSpecifications[0] =
584
587
  JBCashOutHookSpecification({hook: IJBCashOutHook(address(this)), noop: true, amount: 0, metadata: "info"});
585
588
  JBAccountingContext memory mockTokenContext =
586
- JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
589
+ // forge-lint: disable-next-line(unsafe-typecast)
590
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
587
591
  JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
588
592
  mockBalanceContext[0] = mockTokenContext;
589
593
  JBRuleset memory returnedRuleset = JBRuleset({
@@ -603,7 +607,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
603
607
  address(store),
604
608
  abi.encodeCall(
605
609
  IJBTerminalStore.recordCashOutFor,
606
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
610
+ (_holder, _projectId, _defaultAmount, mockTokenContext.token, true, "")
607
611
  ),
608
612
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
609
613
  );
@@ -614,6 +618,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
614
618
  address(this), abi.encodeCall(IJBController.burnTokensOf, (_holder, _projectId, _defaultAmount, "")), ""
615
619
  );
616
620
 
621
+ // forge-lint: disable-next-line(unsafe-typecast)
617
622
  _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
618
623
 
619
624
  vm.prank(_bene);
@@ -10,6 +10,7 @@ import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
10
10
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
11
11
  import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
12
12
  import {JBFees} from "../../../../src/libraries/JBFees.sol";
13
+ import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
13
14
  import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
14
15
  import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
15
16
  import {JBSplit} from "../../../../src/structs/JBSplit.sol";
@@ -32,6 +33,7 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
32
33
 
33
34
  address _native = JBConstants.NATIVE_TOKEN;
34
35
  address _usdc = makeAddr("USDC");
36
+ // forge-lint: disable-next-line(unsafe-typecast)
35
37
  uint32 _usdcCurrency = uint32(uint160(_usdc));
36
38
 
37
39
  JBSplit private _split;
@@ -42,10 +44,12 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
42
44
  }
43
45
 
44
46
  function _setAccountingContext(uint256 projectId, address token, uint8 decimals, uint32 currency) internal {
45
- bytes32 contextSlot = keccak256(abi.encode(projectId, uint256(0)));
46
- bytes32 slot = keccak256(abi.encode(token, contextSlot));
47
- bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
48
- vm.store(address(_terminal), slot, packed);
47
+ // Mock the store to return this accounting context
48
+ mockExpect(
49
+ address(store),
50
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), projectId, token)),
51
+ abi.encode(JBAccountingContext({token: token, decimals: decimals, currency: currency}))
52
+ );
49
53
  }
50
54
 
51
55
  modifier whenASplitHookIsConfigured() {
@@ -66,6 +70,13 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
66
70
  function test_GivenTheSplitHookEQFeeless() external whenASplitHookIsConfigured {
67
71
  // it will not process a fee
68
72
 
73
+ // Mock accountingContextOf for the decimals lookup in executePayout
74
+ mockExpect(
75
+ address(store),
76
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _noProject, _native)),
77
+ abi.encode(JBAccountingContext({token: _native, decimals: 0, currency: 0}))
78
+ );
79
+
69
80
  // mock call to split hook supportsInterface
70
81
  mockExpect(
71
82
  address(_hook),
@@ -111,6 +122,13 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
111
122
  function test_GivenTheSplitHookDNEQFeeless() external whenASplitHookIsConfigured {
112
123
  // it will process a fee
113
124
 
125
+ // Mock accountingContextOf for the decimals lookup in executePayout
126
+ mockExpect(
127
+ address(store),
128
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _noProject, _native)),
129
+ abi.encode(JBAccountingContext({token: _native, decimals: 0, currency: 0}))
130
+ );
131
+
114
132
  // mock call to split hook supportsInterface
115
133
  mockExpect(
116
134
  address(_hook),
@@ -13,6 +13,8 @@ import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
13
13
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
14
14
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
15
15
 
16
+ // Accounting context is now read from the store
17
+
16
18
  contract TestExecuteProcessFee_Local is JBMultiTerminalSetup {
17
19
  uint256 _projectId = 1;
18
20
  uint256 _defaultAmount = 1e18;
@@ -28,11 +30,12 @@ contract TestExecuteProcessFee_Local is JBMultiTerminalSetup {
28
30
  }
29
31
 
30
32
  function _setAccountingContext(address token, uint8 decimals, uint32 currency) internal {
31
- bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
32
- bytes32 slot = keccak256(abi.encode(token, contextSlot));
33
-
34
- bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
35
- vm.store(address(_terminal), slot, packed);
33
+ // Mock the store to return this accounting context
34
+ mockExpect(
35
+ address(store),
36
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, token)),
37
+ abi.encode(JBAccountingContext({token: token, decimals: decimals, currency: currency}))
38
+ );
36
39
  }
37
40
 
38
41
  function test_WhenCallerIsNotItself() external {
@@ -7,7 +7,6 @@ import {IJBController} from "../../../../src/interfaces/IJBController.sol";
7
7
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
8
8
  import {IJBPayHook} from "../../../../src/interfaces/IJBPayHook.sol";
9
9
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
10
- import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
11
10
  import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
12
11
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
13
12
  import {IJBTokens} from "../../../../src/interfaces/IJBTokens.sol";
@@ -17,7 +16,6 @@ import {JBAfterPayRecordedContext} from "../../../../src/structs/JBAfterPayRecor
17
16
  import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
18
17
  import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
19
18
  import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
20
- import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
21
19
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
22
20
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
23
21
 
@@ -60,23 +58,22 @@ contract TestPay_Local is JBMultiTerminalSetup {
60
58
  token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
61
59
  });
62
60
 
63
- // setup: return data
64
- JBRuleset memory returnedRuleset = JBRuleset({
65
- cycleNumber: 1,
66
- id: 0,
67
- basedOnId: 0,
68
- start: 0,
69
- duration: 0,
70
- weight: 0,
71
- weightCutPercent: 0,
72
- approvalHook: IJBRulesetApprovalHook(address(0)),
73
- metadata: 0
74
- });
75
-
76
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
61
+ // Mock recordAccountingContextOf in the store (validation now happens there)
62
+ mockExpect(
63
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
64
+ );
77
65
 
78
66
  _terminal.addAccountingContextsFor(_projectId, _tokens);
79
67
 
68
+ // Mock accountingContextOf for subsequent reads
69
+ mockExpect(
70
+ address(store),
71
+ abi.encodeCall(
72
+ IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, JBConstants.NATIVE_TOKEN)
73
+ ),
74
+ abi.encode(_tokens[0])
75
+ );
76
+
80
77
  _;
81
78
  }
82
79
 
@@ -89,31 +86,25 @@ contract TestPay_Local is JBMultiTerminalSetup {
89
86
  address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(this))
90
87
  );
91
88
 
92
- // mock call to token decimals
93
- mockExpect(address(_mockToken), abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(6));
94
-
95
89
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
96
90
  _tokens[0] = JBAccountingContext({
97
91
  token: address(_mockToken), decimals: 6, currency: uint32(uint160(address(_mockToken)))
98
92
  });
99
93
 
100
- // setup: return data
101
- JBRuleset memory ruleset = JBRuleset({
102
- cycleNumber: 1,
103
- id: 0,
104
- basedOnId: 0,
105
- start: 0,
106
- duration: 0,
107
- weight: 0,
108
- weightCutPercent: 0,
109
- approvalHook: IJBRulesetApprovalHook(address(0)),
110
- metadata: 0
111
- });
112
-
113
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
94
+ // Mock recordAccountingContextOf in the store (validation now happens there)
95
+ mockExpect(
96
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
97
+ );
114
98
 
115
99
  _terminal.addAccountingContextsFor(_projectId, _tokens);
116
100
 
101
+ // Mock accountingContextOf for subsequent reads
102
+ mockExpect(
103
+ address(store),
104
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, address(_mockToken))),
105
+ abi.encode(_tokens[0])
106
+ );
107
+
117
108
  _;
118
109
  }
119
110
 
@@ -254,6 +245,7 @@ contract TestPay_Local is JBMultiTerminalSetup {
254
245
  // forge-lint: disable-next-line(unsafe-typecast)
255
246
  token: address(_mockToken),
256
247
  decimals: 6,
248
+ // forge-lint: disable-next-line(unsafe-typecast)
257
249
  currency: uint32(_mockTokenCurrency),
258
250
  value: _defaultAmount
259
251
  });
@@ -441,8 +433,16 @@ contract TestPay_Local is JBMultiTerminalSetup {
441
433
  function test_WhenTheProjectDNHAccountingContextForTheToken() external {
442
434
  // it will revert TOKEN_NOT_ACCEPTED
443
435
 
436
+ // Mock totalBalanceOf (called before _acceptFundsFor)
444
437
  mockExpect(address(tokens), abi.encodeCall(IJBTokens.totalBalanceOf, (_bene, _projectId)), abi.encode(0));
445
438
 
439
+ // Mock accountingContextOf to return empty context (token not accepted)
440
+ mockExpect(
441
+ address(store),
442
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _native)),
443
+ abi.encode(JBAccountingContext({token: address(0), decimals: 0, currency: 0}))
444
+ );
445
+
446
446
  vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _native));
447
447
  _terminal.pay{value: 1e18}({
448
448
  projectId: _projectId,
@@ -463,8 +463,16 @@ contract TestPay_Local is JBMultiTerminalSetup {
463
463
  function test_WhenTheTerminalsTokenEqNativeTokenAndMsgvalueEqZero() external {
464
464
  // it will revert NO_MSG_VALUE_ALLOWED
465
465
 
466
+ // Mock totalBalanceOf (called before _acceptFundsFor)
466
467
  mockExpect(address(tokens), abi.encodeCall(IJBTokens.totalBalanceOf, (_bene, _projectId)), abi.encode(0));
467
468
 
469
+ // Mock accountingContextOf to return empty context (token not accepted)
470
+ mockExpect(
471
+ address(store),
472
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _native)),
473
+ abi.encode(JBAccountingContext({token: address(0), decimals: 0, currency: 0}))
474
+ );
475
+
468
476
  vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _native));
469
477
  _terminal.pay{value: 0}({
470
478
  projectId: _projectId,
@@ -5,7 +5,6 @@ import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
5
5
  import {IJBCashOutHook} from "../../../../src/interfaces/IJBCashOutHook.sol";
6
6
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
7
7
  import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
8
- import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
9
8
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
10
9
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
11
10
  import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
@@ -32,34 +31,35 @@ contract TestPreviewCashOutFrom_Local is JBMultiTerminalSetup {
32
31
  address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(this))
33
32
  );
34
33
 
35
- JBRuleset memory returnedRuleset = JBRuleset({
36
- cycleNumber: 1,
37
- id: 0,
38
- basedOnId: 0,
39
- start: 0,
40
- duration: 0,
41
- weight: 0,
42
- weightCutPercent: 0,
43
- approvalHook: IJBRulesetApprovalHook(address(0)),
44
- metadata: 0
45
- });
46
-
47
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
48
-
49
34
  JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
50
35
  contexts[0] = JBAccountingContext({token: token, decimals: decimals, currency: currency});
51
36
 
37
+ // Mock recordAccountingContextOf in the store (validation now happens there)
38
+ mockExpect(
39
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, contexts)), ""
40
+ );
41
+
52
42
  vm.prank(address(this));
53
43
  _terminal.addAccountingContextsFor(_projectId, contexts);
44
+
45
+ // Mock accountingContextOf for subsequent reads (not all code paths call it, so use mockCall only)
46
+ vm.mockCall(
47
+ address(store),
48
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, token)),
49
+ abi.encode(contexts[0])
50
+ );
54
51
  }
55
52
 
56
53
  function test_RevertsWhenTokenIsNotAccepted() external {
57
- vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _token));
54
+ // previewCashOutFrom now delegates directly to the store without a token acceptance check,
55
+ // so it reverts during store computation (e.g. unmocked external call) rather than with TokenNotAccepted.
56
+ vm.expectRevert();
58
57
  JBMultiTerminal(address(_terminal))
59
58
  .previewCashOutFrom(_holder, _projectId, _cashOutCount, _token, _beneficiary, "");
60
59
  }
61
60
 
62
61
  function test_ReturnsRulesetAndCashOutPreviewValues() external {
62
+ // forge-lint: disable-next-line(unsafe-typecast)
63
63
  _acceptToken(_token, 18, uint32(uint160(_token)));
64
64
 
65
65
  JBRuleset memory ruleset = JBRuleset({
@@ -80,7 +80,8 @@ contract TestPreviewCashOutFrom_Local is JBMultiTerminalSetup {
80
80
  });
81
81
 
82
82
  JBAccountingContext memory accountingContext =
83
- JBAccountingContext({token: _token, decimals: 18, currency: uint32(uint160(_token))});
83
+ // forge-lint: disable-next-line(unsafe-typecast)
84
+ JBAccountingContext({token: _token, decimals: 18, currency: uint32(uint160(_token))});
84
85
  JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
85
86
  accountingContexts[0] = accountingContext;
86
87
 
@@ -92,7 +93,7 @@ contract TestPreviewCashOutFrom_Local is JBMultiTerminalSetup {
92
93
  address(store),
93
94
  abi.encodeCall(
94
95
  IJBTerminalStore.previewCashOutFrom,
95
- (_holder, _projectId, _cashOutCount, accountingContext, accountingContexts, true, bytes(""))
96
+ (address(_terminal), _holder, _projectId, _cashOutCount, accountingContext.token, true, bytes(""))
96
97
  ),
97
98
  abi.encode(ruleset, 999, 1234, specs)
98
99
  );
@@ -27,21 +27,50 @@ contract TestPreviewPayFor_Local is JBMultiTerminalSetup {
27
27
  }
28
28
 
29
29
  function _setAccountingContext(address token, uint8 decimals, uint32 currency) internal {
30
- bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
31
- bytes32 slot = keccak256(abi.encode(token, contextSlot));
32
- bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
33
- vm.store(address(_terminal), slot, packed);
30
+ // Mock the store to return this accounting context
31
+ mockExpect(
32
+ address(store),
33
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, token)),
34
+ abi.encode(JBAccountingContext({token: token, decimals: decimals, currency: currency}))
35
+ );
34
36
  }
35
37
 
36
38
  function test_RevertsWhenTokenIsNotAccepted() external {
39
+ // Mock accountingContextOf to return empty context (token not accepted)
40
+ mockExpect(
41
+ address(store),
42
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _token)),
43
+ abi.encode(JBAccountingContext({token: address(0), decimals: 0, currency: 0}))
44
+ );
45
+
37
46
  vm.prank(_payer);
38
47
  vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _token));
39
48
  JBMultiTerminal(address(_terminal)).previewPayFor(_projectId, _token, _amount, _beneficiary, "");
40
49
  }
41
50
 
42
51
  function test_ReturnsRulesetMintSplitAndHookSpecifications() external {
52
+ // forge-lint: disable-next-line(unsafe-typecast)
43
53
  _setAccountingContext(_token, 18, uint32(uint160(_token)));
44
54
 
55
+ _mockPreviewPayFrom();
56
+
57
+ vm.prank(_payer);
58
+ (
59
+ JBRuleset memory previewRuleset,
60
+ uint256 beneficiaryTokenCount,
61
+ uint256 reservedTokenCount,
62
+ JBPayHookSpecification[] memory previewSpecs
63
+ ) = JBMultiTerminal(address(_terminal)).previewPayFor(_projectId, _token, _amount, _beneficiary, "");
64
+
65
+ assertEq(previewRuleset.id, 1);
66
+ assertEq(beneficiaryTokenCount, 750);
67
+ assertEq(reservedTokenCount, 250);
68
+ assertEq(previewSpecs.length, 1);
69
+ assertEq(previewSpecs[0].amount, 123);
70
+ assertEq(previewSpecs[0].metadata, hex"1234");
71
+ }
72
+
73
+ function _mockPreviewPayFrom() internal {
45
74
  JBRuleset memory ruleset = JBRuleset({
46
75
  cycleNumber: 1,
47
76
  id: 1,
@@ -59,11 +88,14 @@ contract TestPreviewPayFor_Local is JBMultiTerminalSetup {
59
88
  JBPayHookSpecification({hook: IJBPayHook(makeAddr("hook")), noop: false, amount: 123, metadata: hex"1234"});
60
89
 
61
90
  JBTokenAmount memory tokenAmount =
62
- JBTokenAmount({token: _token, decimals: 18, currency: uint32(uint160(_token)), value: _amount});
91
+ // forge-lint: disable-next-line(unsafe-typecast)
92
+ JBTokenAmount({token: _token, decimals: 18, currency: uint32(uint160(_token)), value: _amount});
63
93
 
64
94
  mockExpect(
65
95
  address(store),
66
- abi.encodeCall(IJBTerminalStore.previewPayFrom, (_payer, tokenAmount, _projectId, _beneficiary, bytes(""))),
96
+ abi.encodeWithSelector(
97
+ bytes4(0xdb6d7e03), address(_terminal), _payer, tokenAmount, _projectId, _beneficiary, bytes("")
98
+ ),
67
99
  abi.encode(ruleset, 1000, specs)
68
100
  );
69
101
 
@@ -78,21 +110,5 @@ contract TestPreviewPayFor_Local is JBMultiTerminalSetup {
78
110
  abi.encodeCall(IJBController.previewMintOf, (_projectId, 1000, true)),
79
111
  abi.encode(750, 250)
80
112
  );
81
-
82
- vm.prank(_payer);
83
- (
84
- JBRuleset memory previewRuleset,
85
- uint256 beneficiaryTokenCount,
86
- uint256 reservedTokenCount,
87
- JBPayHookSpecification[] memory previewSpecs
88
- ) = JBMultiTerminal(address(_terminal)).previewPayFor(_projectId, _token, _amount, _beneficiary, "");
89
-
90
- assertEq(previewRuleset.id, ruleset.id);
91
- assertEq(beneficiaryTokenCount, 750);
92
- assertEq(reservedTokenCount, 250);
93
- assertEq(previewSpecs.length, 1);
94
- assertEq(address(previewSpecs[0].hook), address(specs[0].hook));
95
- assertEq(previewSpecs[0].amount, specs[0].amount);
96
- assertEq(previewSpecs[0].metadata, specs[0].metadata);
97
113
  }
98
114
  }
@@ -13,6 +13,7 @@ import {IJBSplits} from "../../../../src/interfaces/IJBSplits.sol";
13
13
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
14
14
  import {IJBTokens} from "../../../../src/interfaces/IJBTokens.sol";
15
15
  import {JBFees} from "../../../../src/libraries/JBFees.sol";
16
+ import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
16
17
  import {JBFee} from "../../../../src/structs/JBFee.sol";
17
18
  import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
18
19
  import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
@@ -65,16 +66,17 @@ contract TestProcessHeldFeesOf_Local is JBTest {
65
66
  address _beneficiary = makeAddr("beneficiary");
66
67
 
67
68
  function _setAccountingContext(uint256 projectId, address token, uint8 decimals, uint32 currency) internal {
68
- bytes32 contextSlot = keccak256(abi.encode(projectId, uint256(0)));
69
- bytes32 slot = keccak256(abi.encode(token, contextSlot));
70
- bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
71
- vm.store(address(_terminal), slot, packed);
69
+ // Mock the store to return this accounting context
70
+ mockExpect(
71
+ address(store),
72
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), projectId, token)),
73
+ abi.encode(JBAccountingContext({token: token, decimals: decimals, currency: currency}))
74
+ );
72
75
  }
73
76
 
74
77
  function setUp() public {
75
- // Constructor will call to find directory and rulesets from the terminal store
78
+ // Constructor will call to find directory from the terminal store
76
79
  mockExpect(address(store), abi.encodeCall(IJBTerminalStore.DIRECTORY, ()), abi.encode(address(directory)));
77
- mockExpect(address(store), abi.encodeCall(IJBTerminalStore.RULESETS, ()), abi.encode(address(rulesets)));
78
80
 
79
81
  _terminal = new ForTest_JBMultiTerminal(
80
82
  feelessAddresses, permissions, projects, splits, store, tokens, permit2, trustedForwarder
@@ -127,6 +129,7 @@ contract TestProcessHeldFeesOf_Local is JBTest {
127
129
  uint256 expectedFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: heldAmount, feePercent: _terminal.FEE()});
128
130
 
129
131
  // Set up accounting context for the fee beneficiary project (project 1) so _pay can build the token amount.
132
+ // forge-lint: disable-next-line(unsafe-typecast)
130
133
  _setAccountingContext(_feeProjectId, _mockToken, 0, uint32(uint160(_mockToken)));
131
134
 
132
135
  // Mock the directory call to find the fee terminal - return _terminal itself so it uses internal _pay
@@ -47,7 +47,7 @@ contract TestSendPayoutsOf_Local is JBMultiTerminalSetup {
47
47
  // record payout mock call
48
48
  mockExpect(
49
49
  address(store),
50
- abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext, 0, 0)),
50
+ abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext.token, 0, 0)),
51
51
  abi.encode(returnedRuleset, 0)
52
52
  );
53
53
 
@@ -86,7 +86,7 @@ contract TestSendPayoutsOf_Local is JBMultiTerminalSetup {
86
86
  // record payout mock call
87
87
  mockExpect(
88
88
  address(store),
89
- abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext, 0, 0)),
89
+ abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext.token, 0, 0)),
90
90
  abi.encode(returnedRuleset, 0)
91
91
  );
92
92
 
@@ -134,7 +134,7 @@ contract TestSendPayoutsOf_Local is JBMultiTerminalSetup {
134
134
  // record payout mock call
135
135
  mockExpect(
136
136
  address(store),
137
- abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext, 0, 0)),
137
+ abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext.token, 0, 0)),
138
138
  abi.encode(returnedRuleset, 0)
139
139
  );
140
140
 
@@ -204,7 +204,7 @@ contract TestSendPayoutsOf_Local is JBMultiTerminalSetup {
204
204
  // record payout mock call
205
205
  mockExpect(
206
206
  address(store),
207
- abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext, 0, 100)),
207
+ abi.encodeCall(IJBTerminalStore.recordPayoutFor, (_projectId, mockTokenContext.token, 0, 100)),
208
208
  abi.encode(returnedRuleset, 100)
209
209
  );
210
210