@bananapus/core-v6 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/ADMINISTRATION.md +0 -1
  2. package/AUDIT_INSTRUCTIONS.md +1 -1
  3. package/CHANGE_LOG.md +3 -3
  4. package/RISKS.md +3 -3
  5. package/SKILLS.md +8 -8
  6. package/USER_JOURNEYS.md +1 -1
  7. package/foundry.toml +0 -1
  8. package/package.json +1 -1
  9. package/src/JBMultiTerminal.sol +92 -192
  10. package/src/JBTerminalStore.sol +405 -235
  11. package/src/interfaces/IJBMultiTerminal.sol +0 -4
  12. package/src/interfaces/IJBTerminal.sol +4 -4
  13. package/src/interfaces/IJBTerminalStore.sol +65 -33
  14. package/src/libraries/JBPayoutSplitGroupLib.sol +0 -1
  15. package/src/libraries/JBSurplus.sol +3 -4
  16. package/test/ComprehensiveInvariant.t.sol +5 -7
  17. package/test/CoreExploitTests.t.sol +18 -23
  18. package/test/TestCashOut.sol +6 -6
  19. package/test/TestMultiTerminalSurplus.sol +4 -4
  20. package/test/TestMultiTokenSurplus.sol +6 -23
  21. package/test/TestTerminalMigration.sol +2 -7
  22. package/test/fork/TestSequencerPriceFeedFork.sol +1 -1
  23. package/test/fork/TestTerminalPreviewParityFork.sol +0 -1
  24. package/test/invariants/TerminalStoreInvariant.t.sol +5 -7
  25. package/test/units/static/JBMultiTerminal/JBMultiTerminalSetup.sol +1 -2
  26. package/test/units/static/JBMultiTerminal/TestAccountingContextsOf.sol +23 -24
  27. package/test/units/static/JBMultiTerminal/TestAddAccountingContextsFor.sol +79 -119
  28. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +33 -26
  29. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +32 -27
  30. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +22 -4
  31. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +8 -5
  32. package/test/units/static/JBMultiTerminal/TestPay.sol +41 -33
  33. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +19 -18
  34. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +38 -22
  35. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +9 -6
  36. package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +4 -4
  37. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +37 -32
  38. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +5 -20
  39. package/test/units/static/JBTerminalStore/JBTerminalStoreSetup.sol +17 -0
  40. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +120 -246
  41. package/test/units/static/JBTerminalStore/TestCurrentSurplusOf.sol +29 -7
  42. package/test/units/static/JBTerminalStore/TestCurrentTotalSurplusOf.sol +88 -20
  43. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +30 -29
  44. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +46 -16
  45. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +24 -53
  46. package/test/units/static/JBTerminalStore/TestRecordPayoutFor.sol +24 -4
  47. package/test/units/static/JBTerminalStore/TestRecordUsedAllowanceOf.sol +14 -4
  48. package/test/units/static/JBTerminalStore/TestUint224Overflow.sol +21 -3
@@ -2,11 +2,8 @@
2
2
  pragma solidity 0.8.26;
3
3
 
4
4
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
5
- import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
6
- import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
5
+ import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
7
6
  import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
8
- import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
9
- import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
10
7
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
11
8
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
12
9
 
@@ -33,40 +30,42 @@ contract TestAccountingContextsOf_Local is JBMultiTerminalSetup {
33
30
  address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(this))
34
31
  );
35
32
 
36
- // mock call to tokens decimals()
37
- mockExpect(_usdc, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(6));
38
-
39
- // setup: return data
40
- JBRuleset memory ruleset = JBRuleset({
41
- cycleNumber: 1,
42
- id: 0,
43
- basedOnId: 0,
44
- start: 0,
45
- duration: 0,
46
- weight: 0,
47
- weightCutPercent: 0,
48
- approvalHook: IJBRulesetApprovalHook(address(0)),
49
- metadata: 0
50
- });
51
-
52
- // mock call to rulesets currentOf returning 0 to bypass ruleset checking
53
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
54
-
55
33
  // call params
56
34
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
57
35
  // forge-lint: disable-next-line(unsafe-typecast)
58
36
  _tokens[0] = JBAccountingContext({token: _usdc, decimals: 6, currency: uint32(uint160(_usdc))});
59
37
 
38
+ // Mock recordAccountingContextOf in the store (validation now happens there)
39
+ mockExpect(
40
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
41
+ );
42
+
60
43
  _terminal.addAccountingContextsFor(_projectId, _tokens);
61
44
 
45
+ // Mock the store to return all contexts when queried
46
+ mockExpect(
47
+ address(store),
48
+ abi.encodeCall(IJBTerminalStore.accountingContextsOf, (address(_terminal), _projectId)),
49
+ abi.encode(_tokens)
50
+ );
51
+
62
52
  JBAccountingContext[] memory _storedContexts = _terminal.accountingContextsOf(_projectId);
63
53
  assertEq(_storedContexts[0].currency, _usdcCurrency);
64
54
  assertEq(_storedContexts[0].token, _usdc);
65
55
  assertEq(_storedContexts[0].decimals, 6);
66
56
  }
67
57
 
68
- function test_WhenAccountingContextsAreNotSet() external view {
58
+ function test_WhenAccountingContextsAreNotSet() external {
69
59
  // it will return an empty array
60
+
61
+ // Mock the store to return empty array
62
+ JBAccountingContext[] memory _empty = new JBAccountingContext[](0);
63
+ mockExpect(
64
+ address(store),
65
+ abi.encodeCall(IJBTerminalStore.accountingContextsOf, (address(_terminal), _projectId)),
66
+ abi.encode(_empty)
67
+ );
68
+
70
69
  JBAccountingContext[] memory _storedContexts = _terminal.accountingContextsOf(_projectId);
71
70
  assertEq(_storedContexts.length, 0);
72
71
  }
@@ -1,14 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
4
+ import {JBTerminalStore} from "../../../../src/JBTerminalStore.sol";
5
5
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
6
- import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
7
- import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
6
+ import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
8
7
  import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
9
8
  import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
10
- import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
11
- import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
12
9
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
13
10
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
14
11
 
@@ -43,44 +40,20 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
43
40
  function test_GivenTheContextIsAlreadySet() external whenCallerIsPermissioned {
44
41
  // it will revert ACCOUNTING_CONTEXT_ALREADY_SET
45
42
 
46
- // Accounting Context to set
47
- // forge-lint: disable-next-line(unsafe-typecast)
48
- JBAccountingContext memory _context =
49
- // forge-lint: disable-next-line(unsafe-typecast)
50
- JBAccountingContext({token: _usdc, decimals: 18, currency: uint32(_usdcCurrency)});
51
-
52
- // Find the storage slot
53
- bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
54
- bytes32 slot = keccak256(abi.encode(_usdc, contextSlot));
55
-
56
- // Set storage
57
- vm.store(address(_terminal), slot, bytes32(abi.encode(_context)));
58
-
59
- // setup: return data
60
- JBRuleset memory ruleset = JBRuleset({
61
- cycleNumber: 1,
62
- id: 0,
63
- basedOnId: 0,
64
- start: 0,
65
- duration: 0,
66
- weight: 0,
67
- weightCutPercent: 0,
68
- approvalHook: IJBRulesetApprovalHook(address(0)),
69
- metadata: 0
70
- });
71
-
72
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
73
-
74
- JBAccountingContext memory _storedContext = _terminal.accountingContextForTokenOf(_projectId, _usdc);
75
- assertEq(_storedContext.token, _usdc);
76
-
77
43
  // call params
78
44
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
79
45
  // forge-lint: disable-next-line(unsafe-typecast)
80
46
  _tokens[0] = JBAccountingContext({token: _usdc, decimals: 6, currency: uint32(uint160(_usdc))});
81
47
 
48
+ // Mock recordAccountingContextOf to revert with AccountingContextAlreadySet
49
+ vm.mockCallRevert(
50
+ address(store),
51
+ abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)),
52
+ abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_AccountingContextAlreadySet.selector, _usdc)
53
+ );
54
+
82
55
  vm.expectRevert(
83
- abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_AccountingContextAlreadySet.selector, _usdc)
56
+ abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_AccountingContextAlreadySet.selector, _usdc)
84
57
  );
85
58
  _terminal.addAccountingContextsFor(_projectId, _tokens);
86
59
  }
@@ -88,32 +61,25 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
88
61
  function test_GivenHappyPathERC20() external whenCallerIsPermissioned {
89
62
  // it will set the context and emit SetAccountingContext
90
63
 
91
- // mock call to tokens decimals()
92
- mockExpect(_usdc, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(6));
93
-
94
- // setup: return data
95
- JBRuleset memory ruleset = JBRuleset({
96
- cycleNumber: 1,
97
- id: 0,
98
- basedOnId: 0,
99
- start: 0,
100
- duration: 0,
101
- weight: 0,
102
- weightCutPercent: 0,
103
- approvalHook: IJBRulesetApprovalHook(address(0)),
104
- metadata: 0
105
- });
106
-
107
- // mock call to rulesets currentOf returning 0 to bypass ruleset checking
108
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
109
-
110
64
  // call params
111
65
  JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
112
66
  // forge-lint: disable-next-line(unsafe-typecast)
113
67
  _tokens[0] = JBAccountingContext({token: _usdc, decimals: 6, currency: uint32(uint160(_usdc))});
114
68
 
69
+ // Mock recordAccountingContextOf in the store (validation now happens there)
70
+ mockExpect(
71
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
72
+ );
73
+
115
74
  _terminal.addAccountingContextsFor(_projectId, _tokens);
116
75
 
76
+ // Mock the store to return the context when queried
77
+ mockExpect(
78
+ address(store),
79
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _usdc)),
80
+ abi.encode(_tokens[0])
81
+ );
82
+
117
83
  JBAccountingContext memory _storedContext = _terminal.accountingContextForTokenOf(_projectId, _usdc);
118
84
  assertEq(_storedContext.token, _usdc);
119
85
  assertEq(_storedContext.decimals, 6);
@@ -128,25 +94,22 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
128
94
  token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
129
95
  });
130
96
 
131
- // mock call to rulesets currentOf returning 0 to bypass ruleset checking
132
-
133
- // setup: return data
134
- JBRuleset memory ruleset = JBRuleset({
135
- cycleNumber: 1,
136
- id: 0,
137
- basedOnId: 0,
138
- start: 0,
139
- duration: 0,
140
- weight: 0,
141
- weightCutPercent: 0,
142
- approvalHook: IJBRulesetApprovalHook(address(0)),
143
- metadata: 0
144
- });
145
-
146
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
97
+ // Mock recordAccountingContextOf in the store (validation now happens there)
98
+ mockExpect(
99
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
100
+ );
147
101
 
148
102
  _terminal.addAccountingContextsFor(_projectId, _tokens);
149
103
 
104
+ // Mock the store to return the context when queried
105
+ mockExpect(
106
+ address(store),
107
+ abi.encodeCall(
108
+ IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, JBConstants.NATIVE_TOKEN)
109
+ ),
110
+ abi.encode(_tokens[0])
111
+ );
112
+
150
113
  JBAccountingContext memory _storedContext =
151
114
  _terminal.accountingContextForTokenOf(_projectId, JBConstants.NATIVE_TOKEN);
152
115
  assertEq(_storedContext.token, JBConstants.NATIVE_TOKEN);
@@ -173,23 +136,22 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
173
136
  token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
174
137
  });
175
138
 
176
- // setup: return data
177
- JBRuleset memory ruleset = JBRuleset({
178
- cycleNumber: 1,
179
- id: 0,
180
- basedOnId: 0,
181
- start: 0,
182
- duration: 0,
183
- weight: 0,
184
- weightCutPercent: 0,
185
- approvalHook: IJBRulesetApprovalHook(address(0)),
186
- metadata: 0
187
- });
188
-
189
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
139
+ // Mock recordAccountingContextOf in the store (validation now happens there)
140
+ mockExpect(
141
+ address(store), abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)), ""
142
+ );
190
143
 
191
144
  _terminal.addAccountingContextsFor(_projectId, _tokens);
192
145
 
146
+ // Mock the store to return the context when queried
147
+ mockExpect(
148
+ address(store),
149
+ abi.encodeCall(
150
+ IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, JBConstants.NATIVE_TOKEN)
151
+ ),
152
+ abi.encode(_tokens[0])
153
+ );
154
+
193
155
  JBAccountingContext memory _storedContext =
194
156
  _terminal.accountingContextForTokenOf(_projectId, JBConstants.NATIVE_TOKEN);
195
157
  assertEq(_storedContext.token, JBConstants.NATIVE_TOKEN);
@@ -216,18 +178,19 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
216
178
  token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
217
179
  });
218
180
 
219
- // setup: return data
220
- JBRuleset memory ruleset = generateUnfriendlyRuleset();
221
-
222
- // mock rulesets call
223
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
181
+ // Mock recordAccountingContextOf to revert with AddingAccountingContextNotAllowed (validation now in store)
182
+ vm.mockCallRevert(
183
+ address(store),
184
+ abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)),
185
+ abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_AddingAccountingContextNotAllowed.selector)
186
+ );
224
187
 
225
- vm.expectRevert(JBMultiTerminal.JBMultiTerminal_AddingAccountingContextNotAllowed.selector);
188
+ vm.expectRevert(JBTerminalStore.JBTerminalStore_AddingAccountingContextNotAllowed.selector);
226
189
  _terminal.addAccountingContextsFor(_projectId, _tokens);
227
190
  }
228
191
 
229
192
  function test_WhenCurrencyIsNativeButDecimalsDNEQ18() external {
230
- // it will revert JBMultiTerminal_AccountingContextDecimalsMismatch
193
+ // it will revert JBTerminalStore_AccountingContextDecimalsMismatch
231
194
 
232
195
  // mock call to JBProjects ownerOf(_projectId)
233
196
  bytes memory _projectsCall = abi.encodeCall(IERC721.ownerOf, (_projectId));
@@ -247,18 +210,19 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
247
210
  currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
248
211
  });
249
212
 
250
- // setup: return data
251
- JBRuleset memory ruleset = generateFriendlyRuleset();
252
-
253
- // mock rulesets call
254
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
213
+ // Mock recordAccountingContextOf to revert with DecimalsMismatch (validation now in store)
214
+ vm.mockCallRevert(
215
+ address(store),
216
+ abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)),
217
+ abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_AccountingContextDecimalsMismatch.selector)
218
+ );
255
219
 
256
- vm.expectRevert(JBMultiTerminal.JBMultiTerminal_AccountingContextDecimalsMismatch.selector);
220
+ vm.expectRevert(JBTerminalStore.JBTerminalStore_AccountingContextDecimalsMismatch.selector);
257
221
  _terminal.addAccountingContextsFor(_projectId, _tokens);
258
222
  }
259
223
 
260
224
  function test_WhenTokenDecimalsDoesNotMatchAccountingContext() external {
261
- // it will revert JBMultiTerminal_AccountingContextDecimalsMismatch
225
+ // it will revert JBTerminalStore_AccountingContextDecimalsMismatch
262
226
 
263
227
  // mock call to JBProjects ownerOf(_projectId)
264
228
  bytes memory _projectsCall = abi.encodeCall(IERC721.ownerOf, (_projectId));
@@ -281,21 +245,19 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
281
245
  currency: uint32(uint160(someToken))
282
246
  });
283
247
 
284
- // setup: return data
285
- JBRuleset memory ruleset = generateFriendlyRuleset();
286
-
287
- // mock rulesets call
288
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
289
-
290
- // mock token call
291
- mockExpect(address(someToken), abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(18));
248
+ // Mock recordAccountingContextOf to revert with DecimalsMismatch (validation now in store)
249
+ vm.mockCallRevert(
250
+ address(store),
251
+ abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)),
252
+ abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_AccountingContextDecimalsMismatch.selector)
253
+ );
292
254
 
293
- vm.expectRevert(JBMultiTerminal.JBMultiTerminal_AccountingContextDecimalsMismatch.selector);
255
+ vm.expectRevert(JBTerminalStore.JBTerminalStore_AccountingContextDecimalsMismatch.selector);
294
256
  _terminal.addAccountingContextsFor(_projectId, _tokens);
295
257
  }
296
258
 
297
259
  function test_WhenCurrencyEQZero() external {
298
- // it will revert JBMultiTerminal_ZeroAccountingContextCurrency
260
+ // it will revert JBTerminalStore_ZeroAccountingContextCurrency
299
261
 
300
262
  // mock call to JBProjects ownerOf(_projectId)
301
263
  bytes memory _projectsCall = abi.encodeCall(IERC721.ownerOf, (_projectId));
@@ -314,16 +276,14 @@ contract TestAddAccountingContextsFor_Local is JBMultiTerminalSetup {
314
276
  // forge-lint: disable-next-line(unsafe-typecast)
315
277
  _tokens[0] = JBAccountingContext({token: someToken, decimals: 18, currency: uint32(uint160(0))});
316
278
 
317
- // setup: return data
318
- JBRuleset memory ruleset = generateFriendlyRuleset();
319
-
320
- // mock rulesets call
321
- mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
322
-
323
- // mock token call
324
- mockExpect(address(someToken), abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(18));
279
+ // Mock recordAccountingContextOf to revert with ZeroAccountingContextCurrency (validation now in store)
280
+ vm.mockCallRevert(
281
+ address(store),
282
+ abi.encodeCall(IJBTerminalStore.recordAccountingContextOf, (_projectId, _tokens)),
283
+ abi.encodeWithSelector(JBTerminalStore.JBTerminalStore_ZeroAccountingContextCurrency.selector)
284
+ );
325
285
 
326
- vm.expectRevert(JBMultiTerminal.JBMultiTerminal_ZeroAccountingContextCurrency.selector);
286
+ vm.expectRevert(JBTerminalStore.JBTerminalStore_ZeroAccountingContextCurrency.selector);
327
287
  _terminal.addAccountingContextsFor(_projectId, _tokens);
328
288
  }
329
289
  }
@@ -55,12 +55,12 @@ contract TestAddToBalanceOf_Local is JBMultiTerminalSetup {
55
55
  // forge-lint: disable-next-line(unsafe-typecast)
56
56
  JBAccountingContext({token: _native, decimals: 18, currency: uint32(_nativeCurrency)});
57
57
 
58
- // Find the storage slot
59
- bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
60
- bytes32 slot = keccak256(abi.encode(_native, contextSlot));
61
-
62
- // Set storage
63
- vm.store(address(_terminal), slot, bytes32(abi.encode(_context)));
58
+ // Mock the store to return this accounting context
59
+ mockExpect(
60
+ address(store),
61
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _native)),
62
+ abi.encode(_context)
63
+ );
64
64
 
65
65
  JBAccountingContext memory _storedContext = _terminal.accountingContextForTokenOf(_projectId, _native);
66
66
  assertEq(_storedContext.token, _native);
@@ -75,18 +75,18 @@ contract TestAddToBalanceOf_Local is JBMultiTerminalSetup {
75
75
  // forge-lint: disable-next-line(unsafe-typecast)
76
76
  JBAccountingContext({token: _native, decimals: 18, currency: uint32(_nativeCurrency)});
77
77
 
78
- // Find the storage slot
79
- bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
80
- bytes32 slot = keccak256(abi.encode(_native, contextSlot));
81
-
82
- // Set storage
83
- vm.store(address(_terminal), slot, bytes32(abi.encode(_context)));
78
+ // Mock the store to return this accounting context
79
+ mockExpect(
80
+ address(store),
81
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _native)),
82
+ abi.encode(_context)
83
+ );
84
84
 
85
85
  JBAccountingContext memory _storedContext = _terminal.accountingContextForTokenOf(_projectId, _native);
86
86
  assertEq(_storedContext.token, _native);
87
87
 
88
- // Find the storage slot for fees array (_heldFeesOf is at storage slot 3)
89
- bytes32 feeSlot = keccak256(abi.encode(_projectId, uint256(3)));
88
+ // Find the storage slot for fees array (_heldFeesOf is at storage slot 1)
89
+ bytes32 feeSlot = keccak256(abi.encode(_projectId, uint256(1)));
90
90
  bytes32 slotForArrayLength = keccak256(abi.encode(_native, feeSlot));
91
91
 
92
92
  // Set the length of the fees array in the storage slot
@@ -223,6 +223,13 @@ contract TestAddToBalanceOf_Local is JBMultiTerminalSetup {
223
223
  function test_WhenTheProjectDNHAccountingContextForTheToken() external {
224
224
  // it will revert TOKEN_NOT_ACCEPTED
225
225
 
226
+ // Mock accountingContextOf to return empty context (token not accepted)
227
+ mockExpect(
228
+ address(store),
229
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _native)),
230
+ abi.encode(JBAccountingContext({token: address(0), decimals: 0, currency: 0}))
231
+ );
232
+
226
233
  vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _native));
227
234
 
228
235
  _terminal.addToBalanceOf{value: payAmount}({
@@ -244,12 +251,12 @@ contract TestAddToBalanceOf_Local is JBMultiTerminalSetup {
244
251
  // forge-lint: disable-next-line(unsafe-typecast)
245
252
  JBAccountingContext({token: _usdc, decimals: 18, currency: uint32(_usdcCurrency)});
246
253
 
247
- // Find the storage slot
248
- bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
249
- bytes32 slot = keccak256(abi.encode(_usdc, contextSlot));
250
-
251
- // Set storage
252
- vm.store(address(_terminal), slot, bytes32(abi.encode(_context)));
254
+ // Mock the store to return this accounting context
255
+ mockExpect(
256
+ address(store),
257
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _usdc)),
258
+ abi.encode(_context)
259
+ );
253
260
 
254
261
  JBAccountingContext memory _storedContext = _terminal.accountingContextForTokenOf(_projectId, _usdc);
255
262
  assertEq(_storedContext.token, _usdc);
@@ -273,12 +280,12 @@ contract TestAddToBalanceOf_Local is JBMultiTerminalSetup {
273
280
  // forge-lint: disable-next-line(unsafe-typecast)
274
281
  JBAccountingContext({token: _usdc, decimals: 18, currency: uint32(_usdcCurrency)});
275
282
 
276
- // Find the storage slot
277
- bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
278
- bytes32 slot = keccak256(abi.encode(_usdc, contextSlot));
279
-
280
- // Set storage
281
- vm.store(address(_terminal), slot, bytes32(abi.encode(_context)));
283
+ // Mock the store to return this accounting context
284
+ mockExpect(
285
+ address(store),
286
+ abi.encodeCall(IJBTerminalStore.accountingContextOf, (address(_terminal), _projectId, _usdc)),
287
+ abi.encode(_context)
288
+ );
282
289
 
283
290
  JBAccountingContext memory _storedContext = _terminal.accountingContextForTokenOf(_projectId, _usdc);
284
291
  assertEq(_storedContext.token, _usdc);
@@ -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);