@bananapus/core-v6 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/ADMINISTRATION.md +3 -0
  2. package/ARCHITECTURE.md +24 -0
  3. package/AUDIT_INSTRUCTIONS.md +4 -2
  4. package/CHANGE_LOG.md +17 -4
  5. package/README.md +12 -2
  6. package/RISKS.md +10 -2
  7. package/SKILLS.md +5 -2
  8. package/USER_JOURNEYS.md +4 -2
  9. package/foundry.toml +1 -0
  10. package/package.json +1 -1
  11. package/src/JBController.sol +52 -5
  12. package/src/JBMultiTerminal.sol +197 -179
  13. package/src/JBTerminalStore.sol +17 -7
  14. package/src/interfaces/IJBCashOutTerminal.sol +30 -0
  15. package/src/interfaces/IJBController.sol +15 -0
  16. package/src/interfaces/IJBTerminal.sol +28 -0
  17. package/src/interfaces/IJBTerminalStore.sol +1 -5
  18. package/src/libraries/JBPayoutSplitGroupLib.sol +157 -0
  19. package/src/structs/JBCashOutHookSpecification.sol +2 -0
  20. package/src/structs/JBPayHookSpecification.sol +2 -0
  21. package/test/CoreExploitTests.t.sol +21 -10
  22. package/test/TestCashOutHooks.sol +6 -4
  23. package/test/TestDataHookFuzzing.sol +6 -2
  24. package/test/TestPayHooks.sol +1 -1
  25. package/test/TestRulesetQueueing.sol +4 -5
  26. package/test/TestRulesetQueuingStress.sol +5 -3
  27. package/test/TestTerminalPreviewParity.sol +208 -0
  28. package/test/fork/TestTerminalPreviewParityFork.sol +109 -0
  29. package/test/units/static/JBController/TestPreviewMintOf.sol +116 -0
  30. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +144 -25
  31. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +11 -1
  32. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +15 -2
  33. package/test/units/static/JBMultiTerminal/TestPay.sol +64 -2
  34. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +116 -0
  35. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +98 -0
  36. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +11 -2
  37. package/test/units/static/JBRulesets/TestCurrentOf.sol +8 -6
  38. package/test/units/static/JBRulesets/TestRulesets.sol +25 -24
  39. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +4 -17
  40. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +49 -2
  41. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +96 -4
  42. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +81 -32
  43. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +113 -2
  44. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +227 -5
@@ -0,0 +1,109 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.26;
3
+
4
+ import {TestTerminalPreviewParity_Local} from "../TestTerminalPreviewParity.sol";
5
+ import {IJBController} from "../../src/interfaces/IJBController.sol";
6
+ import {IJBTerminal} from "../../src/interfaces/IJBTerminal.sol";
7
+ import {IJBCashOutTerminal} from "../../src/interfaces/IJBCashOutTerminal.sol";
8
+ import {JBConstants} from "../../src/libraries/JBConstants.sol";
9
+ import {JBCashOutHookSpecification} from "../../src/structs/JBCashOutHookSpecification.sol";
10
+ import {JBPayHookSpecification} from "../../src/structs/JBPayHookSpecification.sol";
11
+ import {JBRuleset} from "../../src/structs/JBRuleset.sol";
12
+
13
+ contract TestTerminalPreviewParityFork is TestTerminalPreviewParity_Local {
14
+ uint256 internal constant FORK_BLOCK = 22_000_000;
15
+
16
+ function setUp() public override {
17
+ vm.createSelectFork("ethereum", FORK_BLOCK);
18
+ super.setUp();
19
+ }
20
+
21
+ function testForkPreviewPayForMatchesPay() external {
22
+ _launchFeeProject();
23
+ uint256 projectId = _launchProject(2500, 10_000);
24
+
25
+ (
26
+ JBRuleset memory ruleset,
27
+ uint256 previewBeneficiaryTokenCount,
28
+ uint256 previewReservedTokenCount,
29
+ JBPayHookSpecification[] memory hookSpecifications
30
+ ) = _terminal.previewPayFor(projectId, JBConstants.NATIVE_TOKEN, 1 ether, _beneficiary, "");
31
+
32
+ assertEq(hookSpecifications.length, 0);
33
+
34
+ uint256 balanceBefore = jbTokens().totalBalanceOf(_beneficiary, projectId);
35
+ uint256 reservedBefore = _controller.pendingReservedTokenBalanceOf(projectId);
36
+
37
+ address payer = makeAddr("forkPayer");
38
+ vm.deal(payer, 1 ether);
39
+
40
+ vm.expectEmit();
41
+ emit IJBTerminal.Pay(
42
+ ruleset.id,
43
+ ruleset.cycleNumber,
44
+ projectId,
45
+ payer,
46
+ _beneficiary,
47
+ 1 ether,
48
+ previewBeneficiaryTokenCount,
49
+ "",
50
+ "",
51
+ payer
52
+ );
53
+
54
+ vm.prank(payer);
55
+ uint256 beneficiaryTokenCount =
56
+ _terminal.pay{value: 1 ether}(projectId, JBConstants.NATIVE_TOKEN, 1 ether, _beneficiary, 0, "", "");
57
+
58
+ assertEq(beneficiaryTokenCount, previewBeneficiaryTokenCount);
59
+ assertEq(jbTokens().totalBalanceOf(_beneficiary, projectId) - balanceBefore, previewBeneficiaryTokenCount);
60
+ assertEq(_controller.pendingReservedTokenBalanceOf(projectId) - reservedBefore, previewReservedTokenCount);
61
+ }
62
+
63
+ function testForkPreviewCashOutFromMatchesCashOut() external {
64
+ _launchFeeProject();
65
+ uint256 projectId = _launchProject(0, 5000);
66
+
67
+ vm.prank(_projectOwner);
68
+ jbFeelessAddresses().setFeelessAddress(_beneficiary, true);
69
+
70
+ vm.deal(_beneficiary, 1 ether);
71
+ vm.prank(_beneficiary);
72
+ uint256 minted =
73
+ _terminal.pay{value: 1 ether}(projectId, JBConstants.NATIVE_TOKEN, 1 ether, _beneficiary, 0, "", "");
74
+
75
+ uint256 cashOutCount = minted / 2;
76
+
77
+ (
78
+ JBRuleset memory ruleset,
79
+ uint256 previewReclaimAmount,
80
+ uint256 previewCashOutTaxRate,
81
+ JBCashOutHookSpecification[] memory hookSpecifications
82
+ ) = _terminal.previewCashOutFrom(
83
+ _beneficiary, projectId, cashOutCount, JBConstants.NATIVE_TOKEN, payable(_beneficiary), ""
84
+ );
85
+
86
+ assertEq(hookSpecifications.length, 0);
87
+
88
+ vm.expectEmit();
89
+ emit IJBCashOutTerminal.CashOutTokens(
90
+ ruleset.id,
91
+ ruleset.cycleNumber,
92
+ projectId,
93
+ _beneficiary,
94
+ _beneficiary,
95
+ cashOutCount,
96
+ previewCashOutTaxRate,
97
+ previewReclaimAmount,
98
+ "",
99
+ _beneficiary
100
+ );
101
+
102
+ vm.prank(_beneficiary);
103
+ uint256 reclaimAmount = _terminal.cashOutTokensOf(
104
+ _beneficiary, projectId, cashOutCount, JBConstants.NATIVE_TOKEN, 0, payable(_beneficiary), ""
105
+ );
106
+
107
+ assertEq(reclaimAmount, previewReclaimAmount);
108
+ }
109
+ }
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {JBController} from "../../../../src/JBController.sol";
5
+ import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
6
+ import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
7
+ import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
8
+ import {JBRulesetMetadataResolver} from "../../../../src/libraries/JBRulesetMetadataResolver.sol";
9
+ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
10
+ import {JBRulesetMetadata} from "../../../../src/structs/JBRulesetMetadata.sol";
11
+ import {JBControllerSetup} from "./JBControllerSetup.sol";
12
+
13
+ contract TestPreviewMintOf_Local is JBControllerSetup {
14
+ uint256 _projectId = 1;
15
+
16
+ function setUp() public {
17
+ super.controllerSetup();
18
+ }
19
+
20
+ function test_RevertsWhenTokenCountIsZero() external {
21
+ vm.expectRevert(JBController.JBController_ZeroTokensToMint.selector);
22
+ _controller.previewMintOf(_projectId, 0, true);
23
+ }
24
+
25
+ function test_ReturnsSplitCountsWhenUsingReservedPercent() external {
26
+ uint256 tokenCount = 1000;
27
+
28
+ JBRulesetMetadata memory metadata = JBRulesetMetadata({
29
+ reservedPercent: 2500,
30
+ cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE,
31
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
32
+ pausePay: false,
33
+ pauseCreditTransfers: false,
34
+ allowOwnerMinting: false,
35
+ allowSetCustomToken: false,
36
+ allowTerminalMigration: false,
37
+ allowSetTerminals: false,
38
+ ownerMustSendPayouts: false,
39
+ allowSetController: false,
40
+ allowAddAccountingContext: true,
41
+ allowAddPriceFeed: false,
42
+ holdFees: false,
43
+ useTotalSurplusForCashOuts: false,
44
+ useDataHookForPay: false,
45
+ useDataHookForCashOut: false,
46
+ dataHook: address(0),
47
+ metadata: 0
48
+ });
49
+
50
+ JBRuleset memory ruleset = JBRuleset({
51
+ cycleNumber: 1,
52
+ id: 1,
53
+ basedOnId: 0,
54
+ start: uint48(block.timestamp),
55
+ duration: 0,
56
+ weight: 0,
57
+ weightCutPercent: 0,
58
+ approvalHook: IJBRulesetApprovalHook(address(0)),
59
+ metadata: JBRulesetMetadataResolver.packRulesetMetadata(metadata)
60
+ });
61
+
62
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
63
+
64
+ (uint256 beneficiaryTokenCount, uint256 reservedTokenCount) =
65
+ _controller.previewMintOf(_projectId, tokenCount, true);
66
+
67
+ assertEq(beneficiaryTokenCount, 750);
68
+ assertEq(reservedTokenCount, 250);
69
+ }
70
+
71
+ function test_IgnoresReservedPercentWhenFlagIsFalse() external {
72
+ uint256 tokenCount = 1000;
73
+
74
+ JBRulesetMetadata memory metadata = JBRulesetMetadata({
75
+ reservedPercent: 9000,
76
+ cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE,
77
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
78
+ pausePay: false,
79
+ pauseCreditTransfers: false,
80
+ allowOwnerMinting: false,
81
+ allowSetCustomToken: false,
82
+ allowTerminalMigration: false,
83
+ allowSetTerminals: false,
84
+ ownerMustSendPayouts: false,
85
+ allowSetController: false,
86
+ allowAddAccountingContext: true,
87
+ allowAddPriceFeed: false,
88
+ holdFees: false,
89
+ useTotalSurplusForCashOuts: false,
90
+ useDataHookForPay: false,
91
+ useDataHookForCashOut: false,
92
+ dataHook: address(0),
93
+ metadata: 0
94
+ });
95
+
96
+ JBRuleset memory ruleset = JBRuleset({
97
+ cycleNumber: 1,
98
+ id: 1,
99
+ basedOnId: 0,
100
+ start: uint48(block.timestamp),
101
+ duration: 0,
102
+ weight: 0,
103
+ weightCutPercent: 0,
104
+ approvalHook: IJBRulesetApprovalHook(address(0)),
105
+ metadata: JBRulesetMetadataResolver.packRulesetMetadata(metadata)
106
+ });
107
+
108
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
109
+
110
+ (uint256 beneficiaryTokenCount, uint256 reservedTokenCount) =
111
+ _controller.previewMintOf(_projectId, tokenCount, false);
112
+
113
+ assertEq(beneficiaryTokenCount, tokenCount);
114
+ assertEq(reservedTokenCount, 0);
115
+ }
116
+ }
@@ -10,6 +10,7 @@ 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";
13
14
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
14
15
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
15
16
  import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
@@ -21,6 +22,8 @@ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
21
22
  import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
22
23
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
23
24
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
25
+ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
26
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
24
27
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
25
28
 
26
29
  contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
@@ -45,6 +48,37 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
45
48
  _mockToken2 = new MockERC20("testToken", "TT");
46
49
  }
47
50
 
51
+ function _acceptToken(address token, uint8 decimals, uint32 currency) internal {
52
+ mockExpect(address(projects), abi.encodeCall(IERC721.ownerOf, (_projectId)), abi.encode(address(0)));
53
+ mockExpect(
54
+ address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(this))
55
+ );
56
+
57
+ if (token != JBConstants.NATIVE_TOKEN) {
58
+ mockExpect(token, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(decimals));
59
+ }
60
+
61
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
62
+ _tokens[0] = JBAccountingContext({token: token, decimals: decimals, currency: currency});
63
+
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));
77
+
78
+ vm.prank(address(this));
79
+ _terminal.addAccountingContextsFor(_projectId, _tokens);
80
+ }
81
+
48
82
  function test_WhenCallerDNHavePermission() external {
49
83
  // it will revert UNAUTHORIZED
50
84
 
@@ -72,8 +106,6 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
72
106
  }
73
107
 
74
108
  modifier whenCallerHasPermission() {
75
- vm.prank(_bene);
76
-
77
109
  // mock call to JBPermissions hasPermission
78
110
  mockExpect(
79
111
  address(permissions),
@@ -92,8 +124,10 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
92
124
 
93
125
  uint256 reclaimAmount = 1e9;
94
126
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](0);
95
- JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](0);
96
- JBAccountingContext memory mockTokenContext = JBAccountingContext({token: address(0), decimals: 0, currency: 0});
127
+ JBAccountingContext memory mockTokenContext =
128
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
129
+ JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
130
+ mockBalanceContext[0] = mockTokenContext;
97
131
  JBRuleset memory returnedRuleset = JBRuleset({
98
132
  cycleNumber: 1,
99
133
  id: 1,
@@ -131,7 +165,9 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
131
165
 
132
166
  // put code at mockToken address to pass OZ Address check
133
167
  vm.etch(_mockToken, abi.encode(1));
168
+ _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
134
169
 
170
+ vm.prank(_bene);
135
171
  _terminal.cashOutTokensOf(_holder, _projectId, _defaultAmount, _mockToken, _minReclaimed, _bene, "");
136
172
  }
137
173
 
@@ -140,8 +176,10 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
140
176
 
141
177
  uint256 reclaimAmount = 1e9;
142
178
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](0);
143
- JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](0);
144
- JBAccountingContext memory mockTokenContext = JBAccountingContext({token: address(0), decimals: 0, currency: 0});
179
+ JBAccountingContext memory mockTokenContext =
180
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
181
+ JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
182
+ mockBalanceContext[0] = mockTokenContext;
145
183
  JBRuleset memory returnedRuleset = JBRuleset({
146
184
  cycleNumber: 1,
147
185
  id: 1,
@@ -179,10 +217,14 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
179
217
 
180
218
  // put code at mockToken address to pass OZ Address check
181
219
  vm.etch(_mockToken, abi.encode(1));
220
+ _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
182
221
 
183
222
  vm.expectRevert(
184
- abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_UnderMinTokensReclaimed.selector, 1e9, 1e18)
223
+ abi.encodeWithSelector(
224
+ JBMultiTerminal.JBMultiTerminal_UnderMinTokensReclaimed.selector, reclaimAmount, 1e18
225
+ )
185
226
  );
227
+ vm.prank(_bene);
186
228
  _terminal.cashOutTokensOf(_holder, _projectId, _defaultAmount, _mockToken, 1e18, _bene, ""); // minReclaimAmount
187
229
  // = 1e18 but only 1e9 reclaimed
188
230
  }
@@ -195,8 +237,10 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
195
237
 
196
238
  uint256 reclaimAmount = 1e9;
197
239
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](0);
198
- JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](0);
199
- JBAccountingContext memory mockTokenContext = JBAccountingContext({token: address(0), decimals: 0, currency: 0});
240
+ JBAccountingContext memory mockTokenContext =
241
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
242
+ JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
243
+ mockBalanceContext[0] = mockTokenContext;
200
244
  JBRuleset memory returnedRuleset = JBRuleset({
201
245
  cycleNumber: 1,
202
246
  id: 1,
@@ -234,6 +278,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
234
278
 
235
279
  // put code at mockToken address to pass OZ Address check
236
280
  vm.etch(_mockToken, abi.encode(1));
281
+ _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
237
282
 
238
283
  // get fee amount
239
284
  uint256 tax = JBFees.feeAmountFrom(reclaimAmount, 25); // 25 = default fee)
@@ -256,6 +301,7 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
256
301
  ""
257
302
  );
258
303
 
304
+ vm.prank(_bene);
259
305
  _terminal.cashOutTokensOf(_holder, _projectId, _defaultAmount, _mockToken, _minReclaimed, _bene, "");
260
306
  }
261
307
 
@@ -306,9 +352,13 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
306
352
 
307
353
  uint256 reclaimAmount = 1e9;
308
354
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](1);
309
- hookSpecifications[0] = JBCashOutHookSpecification({hook: _mockHook, amount: _defaultAmount, metadata: ""});
310
- JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](0);
311
- JBAccountingContext memory mockTokenContext = JBAccountingContext({token: address(0), decimals: 0, currency: 0});
355
+ hookSpecifications[0] =
356
+ JBCashOutHookSpecification({hook: _mockHook, noop: false, amount: _defaultAmount, metadata: ""});
357
+ JBAccountingContext memory mockTokenContext = JBAccountingContext({
358
+ token: address(_mockToken2), decimals: 18, currency: uint32(uint160(address(_mockToken2)))
359
+ });
360
+ JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
361
+ mockBalanceContext[0] = mockTokenContext;
312
362
  JBRuleset memory returnedRuleset = JBRuleset({
313
363
  cycleNumber: 1,
314
364
  id: 1,
@@ -349,10 +399,18 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
349
399
  abi.encode(true)
350
400
  );
351
401
 
352
- JBTokenAmount memory reclaimedAmount =
353
- JBTokenAmount({token: address(_mockToken2), decimals: 0, currency: 0, value: reclaimAmount});
354
- JBTokenAmount memory forwardedAmount =
355
- JBTokenAmount({token: address(_mockToken2), decimals: 0, currency: 0, value: _defaultAmount});
402
+ JBTokenAmount memory reclaimedAmount = JBTokenAmount({
403
+ token: address(_mockToken2),
404
+ decimals: 18,
405
+ currency: uint32(uint160(address(_mockToken2))),
406
+ value: reclaimAmount
407
+ });
408
+ JBTokenAmount memory forwardedAmount = JBTokenAmount({
409
+ token: address(_mockToken2),
410
+ decimals: 18,
411
+ currency: uint32(uint160(address(_mockToken2))),
412
+ value: _defaultAmount
413
+ });
356
414
 
357
415
  // needed for hook call
358
416
  JBAfterCashOutRecordedContext memory context = JBAfterCashOutRecordedContext({
@@ -373,6 +431,8 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
373
431
  // ensure approval is increased
374
432
  vm.expectCall(address(_mockToken2), abi.encodeCall(IERC20.approve, (address(_mockHook), _defaultAmount * 2)));
375
433
 
434
+ _acceptToken(address(_mockToken2), 18, uint32(uint160(address(_mockToken2))));
435
+
376
436
  vm.expectEmit();
377
437
  emit IJBCashOutTerminal.HookAfterRecordCashOut(_mockHook, context, _defaultAmount, 0, address(_bene));
378
438
 
@@ -409,9 +469,13 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
409
469
  uint256 reclaimAmount = 1e9;
410
470
  JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](1);
411
471
  JBCashOutHookSpecification[] memory paySpecs = new JBCashOutHookSpecification[](0);
412
- hookSpecifications[0] = JBCashOutHookSpecification({hook: _mockHook, amount: _defaultAmount, metadata: ""});
413
- JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](0);
414
- JBAccountingContext memory mockTokenContext = JBAccountingContext({token: address(0), decimals: 0, currency: 0});
472
+ hookSpecifications[0] =
473
+ JBCashOutHookSpecification({hook: _mockHook, noop: false, amount: _defaultAmount, metadata: ""});
474
+ JBAccountingContext memory mockTokenContext = JBAccountingContext({
475
+ token: address(_mockToken2), decimals: 18, currency: uint32(uint160(address(_mockToken2)))
476
+ });
477
+ JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
478
+ mockBalanceContext[0] = mockTokenContext;
415
479
  JBRuleset memory returnedRuleset = JBRuleset({
416
480
  cycleNumber: 1,
417
481
  id: 1,
@@ -455,12 +519,21 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
455
519
  uint256 hookTax = JBFees.feeAmountFrom(_defaultAmount, 25);
456
520
  uint256 passedAfterTax = _defaultAmount - hookTax;
457
521
 
458
- JBTokenAmount memory reclaimedAmount =
459
- JBTokenAmount({token: address(_mockToken2), decimals: 0, currency: 0, value: reclaimAmount});
460
- JBTokenAmount memory forwardedAmount =
461
- JBTokenAmount({token: address(_mockToken2), decimals: 0, currency: 0, value: passedAfterTax});
462
- JBTokenAmount memory feeRepayAmount =
463
- JBTokenAmount({token: address(_mockToken2), decimals: 0, currency: 0, value: hookTax});
522
+ JBTokenAmount memory reclaimedAmount = JBTokenAmount({
523
+ token: address(_mockToken2),
524
+ decimals: 18,
525
+ currency: uint32(uint160(address(_mockToken2))),
526
+ value: reclaimAmount
527
+ });
528
+ JBTokenAmount memory forwardedAmount = JBTokenAmount({
529
+ token: address(_mockToken2),
530
+ decimals: 18,
531
+ currency: uint32(uint160(address(_mockToken2))),
532
+ value: passedAfterTax
533
+ });
534
+ JBTokenAmount memory feeRepayAmount = JBTokenAmount({
535
+ token: address(_mockToken2), decimals: 18, currency: uint32(uint160(address(_mockToken2))), value: hookTax
536
+ });
464
537
 
465
538
  // needed for hook call
466
539
  JBAfterCashOutRecordedContext memory context = JBAfterCashOutRecordedContext({
@@ -494,10 +567,56 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
494
567
  ),
495
568
  abi.encode(returnedRuleset, 0, paySpecs)
496
569
  );
570
+
571
+ _acceptToken(address(_mockToken2), 18, uint32(uint160(address(_mockToken2))));
572
+
497
573
  vm.expectEmit();
498
574
  emit IJBCashOutTerminal.HookAfterRecordCashOut(_mockHook, context, passedAfterTax, hookTax, address(_bene));
499
575
 
500
576
  vm.prank(_bene);
501
577
  _terminal.cashOutTokensOf(_holder, _projectId, _defaultAmount, address(_mockToken2), _minReclaimed, _bene, "");
502
578
  }
579
+
580
+ function test_GivenTheCashOutHookSpecIsNoop() external whenCallerHasPermission {
581
+ uint256 reclaimAmount = 1e9;
582
+ JBCashOutHookSpecification[] memory hookSpecifications = new JBCashOutHookSpecification[](1);
583
+ hookSpecifications[0] =
584
+ JBCashOutHookSpecification({hook: IJBCashOutHook(address(this)), noop: true, amount: 0, metadata: "info"});
585
+ JBAccountingContext memory mockTokenContext =
586
+ JBAccountingContext({token: _mockToken, decimals: 18, currency: uint32(uint160(_mockToken))});
587
+ JBAccountingContext[] memory mockBalanceContext = new JBAccountingContext[](1);
588
+ mockBalanceContext[0] = mockTokenContext;
589
+ JBRuleset memory returnedRuleset = JBRuleset({
590
+ cycleNumber: 1,
591
+ id: 1,
592
+ basedOnId: 0,
593
+ start: 0,
594
+ duration: 0,
595
+ weight: 0,
596
+ weightCutPercent: 0,
597
+ approvalHook: IJBRulesetApprovalHook(address(0)),
598
+ metadata: 0
599
+ });
600
+
601
+ mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
602
+ mockExpect(
603
+ address(store),
604
+ abi.encodeCall(
605
+ IJBTerminalStore.recordCashOutFor,
606
+ (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
607
+ ),
608
+ abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
609
+ );
610
+ mockExpect(
611
+ address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(this))
612
+ );
613
+ mockExpect(
614
+ address(this), abi.encodeCall(IJBController.burnTokensOf, (_holder, _projectId, _defaultAmount, "")), ""
615
+ );
616
+
617
+ _acceptToken(_mockToken, 18, uint32(uint160(_mockToken)));
618
+
619
+ vm.prank(_bene);
620
+ _terminal.cashOutTokensOf(_holder, _projectId, _defaultAmount, _mockToken, _minReclaimed, _bene, "");
621
+ }
503
622
  }
@@ -32,6 +32,7 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
32
32
 
33
33
  address _native = JBConstants.NATIVE_TOKEN;
34
34
  address _usdc = makeAddr("USDC");
35
+ uint32 _usdcCurrency = uint32(uint160(_usdc));
35
36
 
36
37
  JBSplit private _split;
37
38
  JBSplit private _emptySplit;
@@ -40,6 +41,13 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
40
41
  super.multiTerminalSetup();
41
42
  }
42
43
 
44
+ 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);
49
+ }
50
+
43
51
  modifier whenASplitHookIsConfigured() {
44
52
  _split = JBSplit({
45
53
  preferAddToBalance: false,
@@ -342,6 +350,8 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
342
350
  function test_GivenPreferAddToBalanceDNEQTrueAndTerminalEQThisAddress() external {
343
351
  // it will call internal _pay
344
352
 
353
+ _setAccountingContext(_projectId, _usdc, 0, _usdcCurrency);
354
+
345
355
  // mock call to directory primaryTerminalOf
346
356
  mockExpect(
347
357
  address(directory),
@@ -360,7 +370,7 @@ contract TestExecutePayout_Local is JBMultiTerminalSetup {
360
370
 
361
371
  // needed for next mock call returns
362
372
  JBTokenAmount memory tokenAmount =
363
- JBTokenAmount({token: _usdc, decimals: 0, currency: 0, value: _defaultAmount});
373
+ JBTokenAmount({token: _usdc, decimals: 0, currency: _usdcCurrency, value: _defaultAmount});
364
374
  JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](0);
365
375
  JBRuleset memory returnedRuleset = JBRuleset({
366
376
  cycleNumber: 1,
@@ -6,6 +6,7 @@ import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetAppro
6
6
  import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
7
7
  import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
8
8
  import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
9
+ import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
9
10
  import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
10
11
  import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
11
12
  import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
@@ -26,6 +27,14 @@ contract TestExecuteProcessFee_Local is JBMultiTerminalSetup {
26
27
  super.multiTerminalSetup();
27
28
  }
28
29
 
30
+ 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);
36
+ }
37
+
29
38
  function test_WhenCallerIsNotItself() external {
30
39
  // it will revert
31
40
 
@@ -87,9 +96,11 @@ contract TestExecuteProcessFee_Local is JBMultiTerminalSetup {
87
96
  function test_WhenFeeTerminalEQItself() external {
88
97
  // it will call internal _pay
89
98
 
99
+ _setAccountingContext(_native, 0, 1);
100
+
90
101
  // needed for next mock call returns
91
102
  JBTokenAmount memory tokenAmount =
92
- JBTokenAmount({token: _native, decimals: 0, currency: 0, value: _defaultAmount});
103
+ JBTokenAmount({token: _native, decimals: 0, currency: 1, value: _defaultAmount});
93
104
  JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](0);
94
105
  JBRuleset memory returnedRuleset = JBRuleset({
95
106
  cycleNumber: 1,
@@ -151,9 +162,11 @@ contract TestExecuteProcessFee_Local is JBMultiTerminalSetup {
151
162
  function test_GivenTokenDNEQNATIVE_TOKENAndPayingItself() external {
152
163
  // it will call external pay with zero msgvalue
153
164
 
165
+ _setAccountingContext(_usdc, 0, 1);
166
+
154
167
  // needed for next mock call returns
155
168
  JBTokenAmount memory tokenAmount =
156
- JBTokenAmount({token: _usdc, decimals: 0, currency: 0, value: _defaultAmount});
169
+ JBTokenAmount({token: _usdc, decimals: 0, currency: 1, value: _defaultAmount});
157
170
  JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](0);
158
171
  JBRuleset memory returnedRuleset = JBRuleset({
159
172
  cycleNumber: 1,