@bananapus/core-v6 0.0.18 → 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 (46) 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 +29 -1
  5. package/README.md +12 -2
  6. package/RISKS.md +10 -2
  7. package/SKILLS.md +9 -0
  8. package/USER_JOURNEYS.md +6 -0
  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 +367 -171
  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 +66 -0
  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/TestSequencerPriceFeedFork.sol +168 -0
  29. package/test/fork/TestTerminalPreviewParityFork.sol +109 -0
  30. package/test/units/static/JBController/TestPreviewMintOf.sol +116 -0
  31. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +144 -25
  32. package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +11 -1
  33. package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +15 -2
  34. package/test/units/static/JBMultiTerminal/TestPay.sol +64 -2
  35. package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +116 -0
  36. package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +98 -0
  37. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +11 -2
  38. package/test/units/static/JBRulesets/TestCurrentOf.sol +8 -6
  39. package/test/units/static/JBRulesets/TestRulesets.sol +25 -24
  40. package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +4 -17
  41. package/test/units/static/JBSurplus/TestSurplusFuzz.sol +49 -2
  42. package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +215 -0
  43. package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +475 -0
  44. package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +464 -0
  45. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +113 -2
  46. package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +227 -5
@@ -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,
@@ -258,7 +258,8 @@ contract TestPay_Local is JBMultiTerminalSetup {
258
258
  value: _defaultAmount
259
259
  });
260
260
  JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](1);
261
- hookSpecifications[0] = JBPayHookSpecification({hook: _mockHook, amount: _defaultAmount, metadata: ""});
261
+ hookSpecifications[0] =
262
+ JBPayHookSpecification({hook: _mockHook, noop: false, amount: _defaultAmount, metadata: ""});
262
263
 
263
264
  JBRuleset memory returnedRuleset = JBRuleset({
264
265
  cycleNumber: 1,
@@ -349,7 +350,8 @@ contract TestPay_Local is JBMultiTerminalSetup {
349
350
  JBTokenAmount memory tokenAmount =
350
351
  JBTokenAmount({token: _native, decimals: 18, currency: uint32(_nativeCurrency), value: _defaultAmount});
351
352
  JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](1);
352
- hookSpecifications[0] = JBPayHookSpecification({hook: _mockHook, amount: _defaultAmount, metadata: ""});
353
+ hookSpecifications[0] =
354
+ JBPayHookSpecification({hook: _mockHook, noop: false, amount: _defaultAmount, metadata: ""});
353
355
 
354
356
  JBRuleset memory returnedRuleset = JBRuleset({
355
357
  cycleNumber: 1,
@@ -527,6 +529,66 @@ contract TestPay_Local is JBMultiTerminalSetup {
527
529
  });
528
530
  }
529
531
 
532
+ function test_GivenThePayHookSpecIsNoop() external whenNativeTokenIsAccepted {
533
+ JBTokenAmount memory tokenAmount =
534
+ JBTokenAmount({token: _native, decimals: 18, currency: uint32(_nativeCurrency), value: _defaultAmount});
535
+ JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](1);
536
+ hookSpecifications[0] =
537
+ JBPayHookSpecification({hook: IJBPayHook(address(this)), noop: true, amount: 0, metadata: "info"});
538
+
539
+ JBRuleset memory returnedRuleset = JBRuleset({
540
+ cycleNumber: 1,
541
+ id: 1,
542
+ basedOnId: 0,
543
+ start: 0,
544
+ duration: 0,
545
+ weight: 0,
546
+ weightCutPercent: 0,
547
+ approvalHook: IJBRulesetApprovalHook(address(0)),
548
+ metadata: 0
549
+ });
550
+
551
+ mockExpect(
552
+ address(store),
553
+ abi.encodeCall(
554
+ IJBTerminalStore.recordPaymentFrom, (address(this), tokenAmount, _projectId, _bene, bytes(""))
555
+ ),
556
+ abi.encode(returnedRuleset, 0, hookSpecifications)
557
+ );
558
+
559
+ bytes[] memory subsequentReturns = new bytes[](2);
560
+ subsequentReturns[0] = abi.encode(0);
561
+ subsequentReturns[1] = abi.encode(0);
562
+
563
+ mockExpectSubsequent(
564
+ address(tokens), abi.encodeCall(IJBTokens.totalBalanceOf, (_bene, _projectId)), subsequentReturns
565
+ );
566
+
567
+ vm.expectEmit();
568
+ emit IJBTerminal.Pay(
569
+ returnedRuleset.id,
570
+ returnedRuleset.cycleNumber,
571
+ _projectId,
572
+ address(this),
573
+ _bene,
574
+ _defaultAmount,
575
+ 0,
576
+ "",
577
+ bytes(""),
578
+ address(this)
579
+ );
580
+
581
+ _terminal.pay{value: _defaultAmount}({
582
+ projectId: _projectId,
583
+ token: _native,
584
+ amount: _defaultAmount,
585
+ beneficiary: _bene,
586
+ minReturnedTokens: 0,
587
+ memo: "",
588
+ metadata: ""
589
+ });
590
+ }
591
+
530
592
  // accept funds with permit2 has been extensively tested in other units
531
593
  /* modifier whenPayMetadataContainsPermitData() {
532
594
  _;
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
5
+ import {IJBCashOutHook} from "../../../../src/interfaces/IJBCashOutHook.sol";
6
+ import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
7
+ import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
8
+ import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
9
+ import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
10
+ import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
11
+ import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
12
+ import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
13
+ import {JBCashOutHookSpecification} from "../../../../src/structs/JBCashOutHookSpecification.sol";
14
+ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
15
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
16
+ import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
17
+
18
+ contract TestPreviewCashOutFrom_Local is JBMultiTerminalSetup {
19
+ uint256 _projectId = 1;
20
+ uint256 _cashOutCount = 1e18;
21
+ address _holder = makeAddr("holder");
22
+ address payable _beneficiary = payable(makeAddr("beneficiary"));
23
+ address _token = JBConstants.NATIVE_TOKEN;
24
+
25
+ function setUp() public {
26
+ super.multiTerminalSetup();
27
+ }
28
+
29
+ function _acceptToken(address token, uint8 decimals, uint32 currency) internal {
30
+ mockExpect(address(projects), abi.encodeCall(IERC721.ownerOf, (_projectId)), abi.encode(address(0)));
31
+ mockExpect(
32
+ address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(this))
33
+ );
34
+
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
+ JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
50
+ contexts[0] = JBAccountingContext({token: token, decimals: decimals, currency: currency});
51
+
52
+ vm.prank(address(this));
53
+ _terminal.addAccountingContextsFor(_projectId, contexts);
54
+ }
55
+
56
+ function test_RevertsWhenTokenIsNotAccepted() external {
57
+ vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _token));
58
+ JBMultiTerminal(address(_terminal))
59
+ .previewCashOutFrom(_holder, _projectId, _cashOutCount, _token, _beneficiary, "");
60
+ }
61
+
62
+ function test_ReturnsRulesetAndCashOutPreviewValues() external {
63
+ _acceptToken(_token, 18, uint32(uint160(_token)));
64
+
65
+ JBRuleset memory ruleset = JBRuleset({
66
+ cycleNumber: 1,
67
+ id: 1,
68
+ basedOnId: 0,
69
+ start: uint48(block.timestamp),
70
+ duration: 0,
71
+ weight: 0,
72
+ weightCutPercent: 0,
73
+ approvalHook: IJBRulesetApprovalHook(address(0)),
74
+ metadata: 0
75
+ });
76
+
77
+ JBCashOutHookSpecification[] memory specs = new JBCashOutHookSpecification[](1);
78
+ specs[0] = JBCashOutHookSpecification({
79
+ hook: IJBCashOutHook(makeAddr("hook")), noop: false, amount: 321, metadata: hex"5678"
80
+ });
81
+
82
+ JBAccountingContext memory accountingContext =
83
+ JBAccountingContext({token: _token, decimals: 18, currency: uint32(uint160(_token))});
84
+ JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
85
+ accountingContexts[0] = accountingContext;
86
+
87
+ mockExpect(
88
+ address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_beneficiary)), abi.encode(true)
89
+ );
90
+
91
+ mockExpect(
92
+ address(store),
93
+ abi.encodeCall(
94
+ IJBTerminalStore.previewCashOutFrom,
95
+ (_holder, _projectId, _cashOutCount, accountingContext, accountingContexts, true, bytes(""))
96
+ ),
97
+ abi.encode(ruleset, 999, 1234, specs)
98
+ );
99
+
100
+ (
101
+ JBRuleset memory previewRuleset,
102
+ uint256 reclaimAmount,
103
+ uint256 cashOutTaxRate,
104
+ JBCashOutHookSpecification[] memory previewSpecs
105
+ ) = JBMultiTerminal(address(_terminal))
106
+ .previewCashOutFrom(_holder, _projectId, _cashOutCount, _token, _beneficiary, "");
107
+
108
+ assertEq(previewRuleset.id, ruleset.id);
109
+ assertEq(reclaimAmount, 999);
110
+ assertEq(cashOutTaxRate, 1234);
111
+ assertEq(previewSpecs.length, 1);
112
+ assertEq(address(previewSpecs[0].hook), address(specs[0].hook));
113
+ assertEq(previewSpecs[0].amount, specs[0].amount);
114
+ assertEq(previewSpecs[0].metadata, specs[0].metadata);
115
+ }
116
+ }