@bananapus/core-v6 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/ADMINISTRATION.md +0 -1
  2. package/AUDIT_INSTRUCTIONS.md +1 -1
  3. package/CHANGE_LOG.md +3 -3
  4. package/RISKS.md +3 -3
  5. package/SKILLS.md +8 -8
  6. package/USER_JOURNEYS.md +1 -1
  7. package/foundry.toml +0 -1
  8. package/package.json +1 -1
  9. package/src/JBMultiTerminal.sol +92 -192
  10. package/src/JBTerminalStore.sol +405 -235
  11. package/src/interfaces/IJBMultiTerminal.sol +0 -4
  12. package/src/interfaces/IJBTerminal.sol +4 -4
  13. package/src/interfaces/IJBTerminalStore.sol +65 -33
  14. package/src/libraries/JBPayoutSplitGroupLib.sol +0 -1
  15. package/src/libraries/JBSurplus.sol +3 -4
  16. package/test/ComprehensiveInvariant.t.sol +5 -7
  17. package/test/CoreExploitTests.t.sol +18 -23
  18. package/test/TestCashOut.sol +6 -6
  19. package/test/TestMultiTerminalSurplus.sol +4 -4
  20. package/test/TestMultiTokenSurplus.sol +6 -23
  21. package/test/TestTerminalMigration.sol +2 -7
  22. package/test/fork/TestSequencerPriceFeedFork.sol +1 -1
  23. package/test/fork/TestTerminalPreviewParityFork.sol +0 -1
  24. package/test/invariants/TerminalStoreInvariant.t.sol +5 -7
  25. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +1 -2
  26. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +23 -24
  27. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +79 -119
  28. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +33 -26
  29. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +32 -27
  30. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +22 -4
  31. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +8 -5
  32. package/test/units/static/JBMultiTerminal/TestPay.sol +41 -33
  33. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +19 -18
  34. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +38 -22
  35. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +9 -6
  36. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +4 -4
  37. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +37 -32
  38. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +5 -20
  39. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +17 -0
  40. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +120 -246
  41. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +29 -7
  42. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +88 -20
  43. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +30 -29
  44. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +46 -16
  45. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +24 -53
  46. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +24 -4
  47. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +14 -4
  48. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +21 -3
@@ -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
 
@@ -8,7 +8,6 @@ import {IJBFeeTerminal} from "../../../../src/interfaces/IJBFeeTerminal.sol";
8
8
  import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
9
9
  import {IJBPayoutTerminal} from "../../../../src/interfaces/IJBPayoutTerminal.sol";
10
10
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
11
- import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
12
11
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
13
12
  import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
14
13
  import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
@@ -18,7 +17,6 @@ import {JBRulesetMetadataResolver} from "../../../../src/libraries/JBRulesetMeta
18
17
  import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
19
18
  import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
20
19
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
21
- import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
22
20
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
23
21
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
24
22
 
@@ -55,7 +53,7 @@ contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
55
53
  // recordUsedAllowance
56
54
  mockExpect(
57
55
  address(store),
58
- abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, mockTokenContext, 0, 0)),
56
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, mockTokenContext.token, 0, 0)),
59
57
  abi.encode(returnedRuleset, 0)
60
58
  );
61
59
 
@@ -88,12 +86,10 @@ contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
88
86
  metadata: 0
89
87
  });
90
88
 
91
- JBAccountingContext memory mockTokenContext = JBAccountingContext({token: address(0), decimals: 0, currency: 0});
92
-
93
- // recordUsedAllowance
89
+ // recordUsedAllowance terminal now passes token address directly.
94
90
  mockExpect(
95
91
  address(store),
96
- abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, mockTokenContext, 100, 0)),
92
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, mockToken, 100, 0)),
97
93
  abi.encode(returnedRuleset, 100)
98
94
  );
99
95
 
@@ -140,12 +136,10 @@ contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
140
136
  metadata: 0
141
137
  });
142
138
 
143
- JBAccountingContext memory mockTokenContext = JBAccountingContext({token: address(0), decimals: 0, currency: 0});
144
-
145
- // recordUsedAllowance
139
+ // recordUsedAllowance terminal now passes token address directly
146
140
  mockExpect(
147
141
  address(store),
148
- abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, mockTokenContext, 100, 0)),
142
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, mockToken, 100, 0)),
149
143
  abi.encode(returnedRuleset, 100)
150
144
  );
151
145
 
@@ -222,24 +216,30 @@ contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
222
216
  metadata: 0
223
217
  });
224
218
 
225
- // mock call to tokens decimals()
226
- mockExpect(mockToken, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(18));
227
-
228
- // mock call to rulesets currentOf returning 0 to bypass ruleset checking
229
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
230
-
231
219
  // call params
232
220
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
233
221
  _tokens[0] = JBAccountingContext({token: mockToken, decimals: 18, currency: currencyId});
234
222
 
223
+ // Mock recordAccountingContextOf in the store (validation now happens there)
224
+ mockExpect(
225
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
226
+ );
227
+
235
228
  _terminal.addAccountingContextsFor(_projectId, _tokens);
236
229
 
230
+ // Mock accountingContextOf for subsequent reads (used by _tokenAmountOf during fee processing)
231
+ mockExpect(
232
+ address(store),
233
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, mockToken)),
234
+ abi.encode(_tokens[0])
235
+ );
236
+
237
237
  _terminal.accountingContextForTokenOf(_projectId, mockToken);
238
238
 
239
239
  // recordUsedAllowance
240
240
  mockExpect(
241
241
  address(store),
242
- abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0], 100, 0)),
242
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0].token, 100, 0)),
243
243
  abi.encode(returnedRuleset, 100)
244
244
  );
245
245
 
@@ -370,22 +370,21 @@ contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
370
370
  metadata: packedMetadata
371
371
  });
372
372
 
373
- // mock call to tokens decimals()
374
- mockExpect(mockToken, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(18));
375
-
376
- // mock call to rulesets currentOf
377
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
378
-
379
373
  // Set up accounting context so the token is recognized
380
374
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
381
375
  _tokens[0] = JBAccountingContext({token: mockToken, decimals: 18, currency: currencyId});
382
376
 
377
+ // Mock recordAccountingContextOf in the store (validation now happens there)
378
+ mockExpect(
379
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
380
+ );
381
+
383
382
  _terminal.addAccountingContextsFor(_projectId, _tokens);
384
383
 
385
384
  // recordUsedAllowance
386
385
  mockExpect(
387
386
  address(store),
388
- abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0], 100, 0)),
387
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0].token, 100, 0)),
389
388
  abi.encode(returnedRuleset, 100)
390
389
  );
391
390
 
@@ -499,22 +498,28 @@ contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
499
498
  metadata: packedMetadata
500
499
  });
501
500
 
502
- // mock call to tokens decimals()
503
- mockExpect(mockToken, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(18));
504
-
505
- // mock call to rulesets currentOf
506
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
507
-
508
501
  // Set up accounting context
509
502
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
510
503
  _tokens[0] = JBAccountingContext({token: mockToken, decimals: 18, currency: currencyId});
511
504
 
505
+ // Mock recordAccountingContextOf in the store (validation now happens there)
506
+ mockExpect(
507
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
508
+ );
509
+
512
510
  _terminal.addAccountingContextsFor(_projectId, _tokens);
513
511
 
512
+ // Mock accountingContextOf for subsequent reads (used by _tokenAmountOf during fee processing)
513
+ mockExpect(
514
+ address(store),
515
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, mockToken)),
516
+ abi.encode(_tokens[0])
517
+ );
518
+
514
519
  // recordUsedAllowance
515
520
  mockExpect(
516
521
  address(store),
517
- abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0], 100, 0)),
522
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0].token, 100, 0)),
518
523
  abi.encode(returnedRuleset, 100)
519
524
  );
520
525