@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
@@ -0,0 +1,98 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
5
+ import {IJBController} from "../../../../src/interfaces/IJBController.sol";
6
+ import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
7
+ import {IJBPayHook} from "../../../../src/interfaces/IJBPayHook.sol";
8
+ import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
9
+ import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
10
+ import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
11
+ import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
12
+ import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
13
+ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
14
+ import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
15
+ import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
16
+
17
+ contract TestPreviewPayFor_Local is JBMultiTerminalSetup {
18
+ uint256 _projectId = 1;
19
+ uint256 _amount = 1e18;
20
+ address _token = JBConstants.NATIVE_TOKEN;
21
+ address _beneficiary = makeAddr("beneficiary");
22
+ address _payer = makeAddr("payer");
23
+ IJBController _controller = IJBController(makeAddr("controller"));
24
+
25
+ function setUp() public {
26
+ super.multiTerminalSetup();
27
+ }
28
+
29
+ function _setAccountingContext(address token, uint8 decimals, uint32 currency) internal {
30
+ bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
31
+ bytes32 slot = keccak256(abi.encode(token, contextSlot));
32
+ bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
33
+ vm.store(address(_terminal), slot, packed);
34
+ }
35
+
36
+ function test_RevertsWhenTokenIsNotAccepted() external {
37
+ vm.prank(_payer);
38
+ vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _token));
39
+ JBMultiTerminal(address(_terminal)).previewPayFor(_projectId, _token, _amount, _beneficiary, "");
40
+ }
41
+
42
+ function test_ReturnsRulesetMintSplitAndHookSpecifications() external {
43
+ _setAccountingContext(_token, 18, uint32(uint160(_token)));
44
+
45
+ JBRuleset memory ruleset = JBRuleset({
46
+ cycleNumber: 1,
47
+ id: 1,
48
+ basedOnId: 0,
49
+ start: uint48(block.timestamp),
50
+ duration: 0,
51
+ weight: 0,
52
+ weightCutPercent: 0,
53
+ approvalHook: IJBRulesetApprovalHook(address(0)),
54
+ metadata: 0
55
+ });
56
+
57
+ JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](1);
58
+ specs[0] =
59
+ JBPayHookSpecification({hook: IJBPayHook(makeAddr("hook")), noop: false, amount: 123, metadata: hex"1234"});
60
+
61
+ JBTokenAmount memory tokenAmount =
62
+ JBTokenAmount({token: _token, decimals: 18, currency: uint32(uint160(_token)), value: _amount});
63
+
64
+ mockExpect(
65
+ address(store),
66
+ abi.encodeCall(IJBTerminalStore.previewPayFrom, (_payer, tokenAmount, _projectId, _beneficiary, bytes(""))),
67
+ abi.encode(ruleset, 1000, specs)
68
+ );
69
+
70
+ mockExpect(
71
+ address(directory),
72
+ abi.encodeCall(IJBDirectory.controllerOf, (_projectId)),
73
+ abi.encode(address(_controller))
74
+ );
75
+
76
+ mockExpect(
77
+ address(_controller),
78
+ abi.encodeCall(IJBController.previewMintOf, (_projectId, 1000, true)),
79
+ abi.encode(750, 250)
80
+ );
81
+
82
+ vm.prank(_payer);
83
+ (
84
+ JBRuleset memory previewRuleset,
85
+ uint256 beneficiaryTokenCount,
86
+ uint256 reservedTokenCount,
87
+ JBPayHookSpecification[] memory previewSpecs
88
+ ) = JBMultiTerminal(address(_terminal)).previewPayFor(_projectId, _token, _amount, _beneficiary, "");
89
+
90
+ assertEq(previewRuleset.id, ruleset.id);
91
+ assertEq(beneficiaryTokenCount, 750);
92
+ assertEq(reservedTokenCount, 250);
93
+ assertEq(previewSpecs.length, 1);
94
+ assertEq(address(previewSpecs[0].hook), address(specs[0].hook));
95
+ assertEq(previewSpecs[0].amount, specs[0].amount);
96
+ assertEq(previewSpecs[0].metadata, specs[0].metadata);
97
+ }
98
+ }
@@ -59,10 +59,18 @@ contract TestProcessHeldFeesOf_Local is JBTest {
59
59
  IPermit2 public permit2 = IPermit2(makeAddr("permit2"));
60
60
  address trustedForwarder = makeAddr("forwarder");
61
61
 
62
+ uint256 _feeProjectId = 1;
62
63
  uint256 _projectId = 2;
63
64
  address _mockToken = makeAddr("token");
64
65
  address _beneficiary = makeAddr("beneficiary");
65
66
 
67
+ function _setAccountingContext(uint256 projectId, address token, uint8 decimals, uint32 currency) internal {
68
+ bytes32 contextSlot = keccak256(abi.encode(projectId, uint256(0)));
69
+ bytes32 slot = keccak256(abi.encode(token, contextSlot));
70
+ bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
71
+ vm.store(address(_terminal), slot, packed);
72
+ }
73
+
66
74
  function setUp() public {
67
75
  // Constructor will call to find directory and rulesets from the terminal store
68
76
  mockExpect(address(store), abi.encodeCall(IJBTerminalStore.DIRECTORY, ()), abi.encode(address(directory)));
@@ -118,6 +126,9 @@ contract TestProcessHeldFeesOf_Local is JBTest {
118
126
  // The fee amount that will be calculated from the held amount
119
127
  uint256 expectedFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: heldAmount, feePercent: _terminal.FEE()});
120
128
 
129
+ // Set up accounting context for the fee beneficiary project (project 1) so _pay can build the token amount.
130
+ _setAccountingContext(_feeProjectId, _mockToken, 0, uint32(uint160(_mockToken)));
131
+
121
132
  // Mock the directory call to find the fee terminal - return _terminal itself so it uses internal _pay
122
133
  mockExpect(
123
134
  address(directory),
@@ -128,8 +139,6 @@ contract TestProcessHeldFeesOf_Local is JBTest {
128
139
  // Mock executeProcessFee: when the terminal calls itself, it will call recordPaymentFrom on the store.
129
140
  // Since executeProcessFee is external and calls pay on the feeTerminal (which is _terminal itself),
130
141
  // we need to mock the internal pay path: recordPaymentFrom on the store.
131
- // The token amount struct for the fee payment
132
- // Note: accounting context for project 1 on _mockToken is unset, so decimals=0 and currency=0.
133
142
  vm.mockCall(
134
143
  address(store),
135
144
  abi.encodeWithSelector(IJBTerminalStore.recordPaymentFrom.selector),
@@ -191,12 +191,13 @@ contract TestCurrentOf_Local is JBRulesetsSetup {
191
191
  {
192
192
  // it will return the ruleset the pending approval ruleset is basedOn
193
193
 
194
- uint256 _firstRulesetId = block.timestamp;
195
- uint256 _rulesetWithHookId = block.timestamp + 1;
194
+ // Capture IDs from actual storage (avoid via_ir reordering of block.timestamp).
195
+ uint256 _firstRulesetId = _rulesets.currentOf(_projectId).id;
196
+ uint256 _rulesetWithHookId = _firstRulesetId + 1;
196
197
 
197
198
  JBRuleset memory _queuedRuleset = _rulesets.getRulesetOf(_projectId, _rulesetWithHookId);
198
199
 
199
- vm.warp(block.timestamp + 3 days);
200
+ vm.warp(_firstRulesetId + 3 days);
200
201
 
201
202
  // mock approvalStatusOf to return Pending
202
203
  mockExpect(
@@ -215,12 +216,13 @@ contract TestCurrentOf_Local is JBRulesetsSetup {
215
216
  {
216
217
  // it will return the basedOn of the latest ruleset
217
218
 
218
- uint256 _firstRulesetId = block.timestamp;
219
- uint256 _rulesetWithHookId = block.timestamp + 1;
219
+ // Capture IDs from actual storage (avoid via_ir reordering of block.timestamp).
220
+ uint256 _firstRulesetId = _rulesets.currentOf(_projectId).id;
221
+ uint256 _rulesetWithHookId = _firstRulesetId + 1;
220
222
 
221
223
  JBRuleset memory _queuedRuleset = _rulesets.getRulesetOf(_projectId, _rulesetWithHookId);
222
224
 
223
- vm.warp(block.timestamp + 4 days);
225
+ vm.warp(_firstRulesetId + 4 days);
224
226
 
225
227
  // mock approvalStatusOf to return Pending
226
228
  mockExpect(
@@ -403,7 +403,8 @@ contract TestJBRulesetsUnits_Local is JBTest {
403
403
  mustStartAtOrAfter: _mustStartAt
404
404
  });
405
405
 
406
- uint256 firstId = block.timestamp;
406
+ // Capture firstId from actual storage (avoid via_ir reordering of block.timestamp).
407
+ uint256 firstId = _rulesets.latestRulesetIdOf(_projectId);
407
408
 
408
409
  // Mock call to approval hook duration
409
410
  bytes memory _encodedDurationCall = abi.encodeCall(IJBRulesetApprovalHook.DURATION, ());
@@ -412,9 +413,9 @@ contract TestJBRulesetsUnits_Local is JBTest {
412
413
  mockExpect(address(_mockApprovalHook), _encodedDurationCall, _willReturnDuration);
413
414
 
414
415
  // avoid overwrite
415
- vm.warp(block.timestamp + 1);
416
+ vm.warp(firstId + 1);
416
417
 
417
- uint256 latestId = block.timestamp;
418
+ uint256 latestId;
418
419
 
419
420
  // Send: Anotha One! Call from this contract as it's been mock authorized above.
420
421
  _rulesets.queueFor({
@@ -424,12 +425,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
424
425
  weightCutPercent: _weightCutPercent,
425
426
  approvalHook: _mockApprovalHook,
426
427
  metadata: _packedWithApprovalHook,
427
- mustStartAtOrAfter: block.timestamp
428
+ mustStartAtOrAfter: 0
428
429
  });
429
430
 
431
+ // Capture latestId from storage (avoid via_ir reordering of block.timestamp).
432
+ latestId = _rulesets.latestRulesetIdOf(_projectId);
433
+
430
434
  // avoid overwrite
431
- vm.warp(block.timestamp + 2 days);
432
- uint256 previouslyApprovedDurationEnds = block.timestamp + 3 days - 2 days - 1;
435
+ vm.warp(latestId + 2 days);
433
436
 
434
437
  // Get the ruleset.
435
438
  JBRuleset memory latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
@@ -449,16 +452,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
449
452
  weightCutPercent: _weightCutPercent,
450
453
  approvalHook: _mockApprovalHook,
451
454
  metadata: _packedWithApprovalHook,
452
- mustStartAtOrAfter: block.timestamp
455
+ mustStartAtOrAfter: 0
453
456
  });
454
457
 
455
- latestId = block.timestamp;
458
+ latestId = _rulesets.latestRulesetIdOf(_projectId);
456
459
  latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
457
460
 
458
461
  // avoid overwrite
459
- vm.warp(block.timestamp + 1);
460
-
461
- previouslyApprovedDurationEnds = block.timestamp + 6 days - 2 days - 2;
462
+ vm.warp(latestId + 1);
462
463
 
463
464
  // Mock call to approvalStatusOf and return an approvalExpected status
464
465
  _encodedApprovalCall = abi.encodeCall(IJBRulesetApprovalHook.approvalStatusOf, (1, latesetQueuedRuleset));
@@ -474,15 +475,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
474
475
  weightCutPercent: _weightCutPercent,
475
476
  approvalHook: _mockApprovalHook,
476
477
  metadata: _packedWithApprovalHook,
477
- mustStartAtOrAfter: block.timestamp
478
+ mustStartAtOrAfter: 0
478
479
  });
479
480
 
480
- latestId = block.timestamp;
481
+ latestId = _rulesets.latestRulesetIdOf(_projectId);
481
482
  latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
482
483
 
483
484
  // avoid overwrite
484
- vm.warp(block.timestamp + 1);
485
- previouslyApprovedDurationEnds = block.timestamp + 6 days - 2 days - 3;
485
+ vm.warp(latestId + 1);
486
486
 
487
487
  // Mock call to approvalStatusOf and return a failed status
488
488
  _encodedApprovalCall = abi.encodeCall(IJBRulesetApprovalHook.approvalStatusOf, (1, latesetQueuedRuleset));
@@ -498,16 +498,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
498
498
  weightCutPercent: _weightCutPercent,
499
499
  approvalHook: _mockApprovalHook,
500
500
  metadata: _packedWithApprovalHook,
501
- mustStartAtOrAfter: block.timestamp
501
+ mustStartAtOrAfter: 0
502
502
  });
503
503
 
504
- latestId = block.timestamp;
504
+ latestId = _rulesets.latestRulesetIdOf(_projectId);
505
505
  latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
506
506
 
507
507
  // avoid overwrite
508
- vm.warp(block.timestamp + 1);
509
-
510
- previouslyApprovedDurationEnds = block.timestamp + 6 days - 2 days - 4;
508
+ vm.warp(latestId + 1);
511
509
 
512
510
  // Mock call to approvalStatusOf and return an empty status
513
511
  _encodedApprovalCall = abi.encodeCall(IJBRulesetApprovalHook.approvalStatusOf, (1, latesetQueuedRuleset));
@@ -523,14 +521,17 @@ contract TestJBRulesetsUnits_Local is JBTest {
523
521
  weightCutPercent: _weightCutPercent,
524
522
  approvalHook: _mockApprovalHook,
525
523
  metadata: _packedWithApprovalHook,
526
- mustStartAtOrAfter: block.timestamp
524
+ mustStartAtOrAfter: 0
527
525
  });
528
526
 
529
- JBRuleset[] memory queuedRulesetsOf = _rulesets.allOf(_projectId, block.timestamp, 3);
527
+ // Capture final latestId from storage.
528
+ latestId = _rulesets.latestRulesetIdOf(_projectId);
529
+
530
+ JBRuleset[] memory queuedRulesetsOf = _rulesets.allOf(_projectId, latestId, 3);
530
531
 
531
- // check: 2 rulesets will be enqueued, we just overwrote the last queued
532
+ // check: 3 rulesets will be enqueued
532
533
  assertEq(queuedRulesetsOf.length, 3);
533
- assertEq(queuedRulesetsOf[0].id, block.timestamp);
534
+ assertEq(queuedRulesetsOf[0].id, latestId);
534
535
 
535
536
  // check first timestamp
536
537
  assertEq(queuedRulesetsOf[2].id, firstId);
@@ -457,8 +457,6 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
457
457
  function test_baseRulesetDurationDNEQZero() external {
458
458
  // it will simulate a ruleset basedOn
459
459
 
460
- uint256 ogTimestamp = block.timestamp;
461
-
462
460
  // put code at hook address
463
461
  vm.etch(address(_mockApprovalHook), abi.encode(1));
464
462
 
@@ -476,20 +474,6 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
476
474
 
477
475
  mockExpect(address(directory), _encodedCall, _willReturn);
478
476
 
479
- // Setup: expect ruleset event (RulesetQueued) is emitted
480
- vm.expectEmit();
481
- emit IJBRulesets.RulesetQueued(
482
- block.timestamp,
483
- _projectId,
484
- _duration,
485
- _weight,
486
- _weightCutPercent,
487
- _mockApprovalHook,
488
- _packedWithApprovalHook,
489
- block.timestamp,
490
- address(this)
491
- );
492
-
493
477
  // Send: Call from this contract as it's been mock authorized above.
494
478
  _rulesets.queueFor({
495
479
  projectId: _projectId,
@@ -501,6 +485,9 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
501
485
  mustStartAtOrAfter: _mustStartAt
502
486
  });
503
487
 
488
+ // Capture the first ruleset's id from storage (avoid via_ir reordering of block.timestamp).
489
+ uint256 ogTimestamp = _rulesets.currentOf(_projectId).id;
490
+
504
491
  // mock call to hook duration
505
492
  mockExpect(
506
493
  address(_mockApprovalHook), abi.encodeCall(IJBRulesetApprovalHook.DURATION, ()), abi.encode(_hookDuration)
@@ -517,7 +504,7 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
517
504
  mustStartAtOrAfter: 0
518
505
  });
519
506
 
520
- vm.warp(block.timestamp + 3 days);
507
+ vm.warp(ogTimestamp + 3 days);
521
508
 
522
509
  uint256 _latestQueuedId = _rulesets.latestRulesetIdOf(_projectId);
523
510
  JBRuleset memory _queuedRuleset = _rulesets.getRulesetOf(_projectId, _latestQueuedId);
@@ -1,14 +1,18 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ import {IJBCashOutTerminal} from "../../../../src/interfaces/IJBCashOutTerminal.sol";
4
5
  import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
5
6
  import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
7
+ import {JBCashOutHookSpecification} from "../../../../src/structs/JBCashOutHookSpecification.sol";
8
+ import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
9
+ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
6
10
  import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
7
11
  import {JBTest} from "../../../helpers/JBTest.sol";
8
12
  import {JBSurplus} from "../../../../src/libraries/JBSurplus.sol";
9
13
 
10
14
  /// @notice Mock terminal that returns a fixed surplus for testing JBSurplus.
11
- contract MockSurplusTerminal is ERC165, IJBTerminal {
15
+ contract MockSurplusTerminal is ERC165, IJBCashOutTerminal {
12
16
  uint256 public surplusAmount;
13
17
 
14
18
  constructor(uint256 _surplus) {
@@ -30,7 +34,8 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
30
34
  }
31
35
 
32
36
  function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
33
- return interfaceId == type(IJBTerminal).interfaceId || super.supportsInterface(interfaceId);
37
+ return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBCashOutTerminal).interfaceId
38
+ || super.supportsInterface(interfaceId);
34
39
  }
35
40
 
36
41
  // Stub implementations for IJBTerminal
@@ -41,6 +46,31 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
41
46
  returns (JBAccountingContext memory)
42
47
  {}
43
48
  function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory) {}
49
+ function previewCashOutFrom(
50
+ address,
51
+ uint256,
52
+ uint256,
53
+ address,
54
+ address payable,
55
+ bytes calldata
56
+ )
57
+ external
58
+ pure
59
+ override
60
+ returns (JBRuleset memory, uint256, uint256, JBCashOutHookSpecification[] memory)
61
+ {}
62
+ function previewPayFor(
63
+ uint256,
64
+ address,
65
+ uint256,
66
+ address,
67
+ bytes calldata
68
+ )
69
+ external
70
+ pure
71
+ override
72
+ returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
73
+ {}
44
74
  function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
45
75
  function addToBalanceOf(
46
76
  uint256,
@@ -75,6 +105,23 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
75
105
  {
76
106
  return 0;
77
107
  }
108
+
109
+ function cashOutTokensOf(
110
+ address,
111
+ uint256,
112
+ uint256,
113
+ address,
114
+ uint256,
115
+ address payable,
116
+ bytes calldata
117
+ )
118
+ external
119
+ pure
120
+ override
121
+ returns (uint256)
122
+ {
123
+ return 0;
124
+ }
78
125
  }
79
126
 
80
127
  /// @notice Fuzz tests for the JBSurplus library.
@@ -444,4 +444,219 @@ contract TestCurrentReclaimableSurplusOf_Local is JBTerminalStoreSetup {
444
444
  uint256 reclaimable = _store.currentReclaimableSurplusOf(_projectId, _tokenCount, 1e18, 1e18);
445
445
  assertEq(1e18, reclaimable);
446
446
  }
447
+
448
+ function test_GivenTotalReclaimableWithSurplus() external whenProjectHasBalance {
449
+ // it will default to all terminals and all accounting contexts and return the reclaimable surplus
450
+
451
+ // setup calldata
452
+ JBAccountingContext[] memory _contexts = new JBAccountingContext[](1);
453
+ _contexts[0] = JBAccountingContext({token: address(_token), decimals: 18, currency: _currency});
454
+
455
+ JBRulesetMetadata memory _metadata = JBRulesetMetadata({
456
+ reservedPercent: 0,
457
+ cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE / 2,
458
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
459
+ pausePay: false,
460
+ pauseCreditTransfers: false,
461
+ allowOwnerMinting: false,
462
+ allowSetCustomToken: false,
463
+ allowTerminalMigration: false,
464
+ allowSetTerminals: false,
465
+ ownerMustSendPayouts: false,
466
+ allowSetController: false,
467
+ allowAddAccountingContext: true,
468
+ allowAddPriceFeed: false,
469
+ holdFees: false,
470
+ useTotalSurplusForCashOuts: false,
471
+ useDataHookForPay: false,
472
+ useDataHookForCashOut: false,
473
+ dataHook: address(0),
474
+ metadata: 0
475
+ });
476
+
477
+ uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
478
+
479
+ // JBRulesets return calldata
480
+ JBRuleset memory _returnedRuleset = JBRuleset({
481
+ cycleNumber: uint48(block.timestamp),
482
+ id: uint48(block.timestamp),
483
+ basedOnId: 0,
484
+ start: uint48(block.timestamp),
485
+ duration: uint32(block.timestamp + 1000),
486
+ weight: 1e18,
487
+ weightCutPercent: 0,
488
+ approvalHook: IJBRulesetApprovalHook(address(0)),
489
+ metadata: _packedMetadata
490
+ });
491
+
492
+ // mock call to JBRulesets currentOf
493
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
494
+
495
+ // mock call to JBDirectory controllerOf
496
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
497
+
498
+ uint256 _supply = 1e19;
499
+ uint256 _surplus = 1e18;
500
+ uint256 _cashoutAmount = 1e18;
501
+
502
+ // mock JBDirectory terminalsOf to return the terminal
503
+ IJBTerminal[] memory _terminals = new IJBTerminal[](1);
504
+ _terminals[0] = _terminal;
505
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
506
+
507
+ // surplus call to the terminal (empty accounting contexts passed through)
508
+ JBAccountingContext[] memory _emptyContexts = new JBAccountingContext[](0);
509
+ mockExpect(
510
+ address(_terminal),
511
+ abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
512
+ abi.encode(_surplus)
513
+ );
514
+
515
+ // mock JBController totalTokenSupplyWithReservedTokensOf
516
+ mockExpect(
517
+ address(_controller),
518
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
519
+ abi.encode(_supply)
520
+ );
521
+
522
+ // Call the new convenience function (no terminals, no accounting contexts).
523
+ uint256 reclaimable = _store.currentTotalReclaimableSurplusOf(_projectId, _cashoutAmount, 18, _currency);
524
+
525
+ // Should match the 6-param overload result.
526
+ uint256 assumed =
527
+ JBCashOuts.cashOutFrom(_surplus, _cashoutAmount, _supply, JBConstants.MAX_CASH_OUT_TAX_RATE / 2);
528
+
529
+ assertEq(assumed, reclaimable);
530
+ }
531
+
532
+ function test_GivenTotalReclaimableWithZeroSurplus() external {
533
+ // it will return zero when there is no surplus
534
+
535
+ JBAccountingContext[] memory _emptyContexts = new JBAccountingContext[](0);
536
+
537
+ // JBRulesets return calldata
538
+ JBRuleset memory _returnedRuleset = JBRuleset({
539
+ cycleNumber: uint48(block.timestamp),
540
+ id: uint48(block.timestamp),
541
+ basedOnId: 0,
542
+ start: uint48(block.timestamp),
543
+ duration: uint32(block.timestamp + 1000),
544
+ weight: 1e18,
545
+ weightCutPercent: 0,
546
+ approvalHook: IJBRulesetApprovalHook(address(0)),
547
+ metadata: 0
548
+ });
549
+
550
+ // mock call to JBRulesets currentOf
551
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
552
+
553
+ // mock JBDirectory terminalsOf to return the terminal
554
+ IJBTerminal[] memory _terminals = new IJBTerminal[](1);
555
+ _terminals[0] = _terminal;
556
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
557
+
558
+ // mock current surplus as zero
559
+ mockExpect(
560
+ address(_terminal),
561
+ abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
562
+ abi.encode(0)
563
+ );
564
+
565
+ uint256 reclaimable = _store.currentTotalReclaimableSurplusOf(_projectId, _tokenCount, 18, _currency);
566
+ assertEq(0, reclaimable);
567
+ }
568
+
569
+ function test_GivenTotalReclaimableMatchesSixParamOverload() external whenProjectHasBalance {
570
+ // it will produce the same result as calling the 6-param overload with empty arrays
571
+
572
+ JBAccountingContext[] memory _emptyContexts = new JBAccountingContext[](0);
573
+
574
+ JBRulesetMetadata memory _metadata = JBRulesetMetadata({
575
+ reservedPercent: 0,
576
+ cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE / 2,
577
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
578
+ pausePay: false,
579
+ pauseCreditTransfers: false,
580
+ allowOwnerMinting: false,
581
+ allowSetCustomToken: false,
582
+ allowTerminalMigration: false,
583
+ allowSetTerminals: false,
584
+ ownerMustSendPayouts: false,
585
+ allowSetController: false,
586
+ allowAddAccountingContext: true,
587
+ allowAddPriceFeed: false,
588
+ holdFees: false,
589
+ useTotalSurplusForCashOuts: false,
590
+ useDataHookForPay: false,
591
+ useDataHookForCashOut: false,
592
+ dataHook: address(0),
593
+ metadata: 0
594
+ });
595
+
596
+ uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
597
+
598
+ JBRuleset memory _returnedRuleset = JBRuleset({
599
+ cycleNumber: uint48(block.timestamp),
600
+ id: uint48(block.timestamp),
601
+ basedOnId: 0,
602
+ start: uint48(block.timestamp),
603
+ duration: uint32(block.timestamp + 1000),
604
+ weight: 1e18,
605
+ weightCutPercent: 0,
606
+ approvalHook: IJBRulesetApprovalHook(address(0)),
607
+ metadata: _packedMetadata
608
+ });
609
+
610
+ uint256 _supply = 1e19;
611
+ uint256 _surplus = 5e17;
612
+ uint256 _cashoutAmount = 1e18;
613
+
614
+ IJBTerminal[] memory _terminals = new IJBTerminal[](1);
615
+ _terminals[0] = _terminal;
616
+
617
+ // The new overload calls the 6-param via `this`, so JBRulesets.currentOf gets called twice.
618
+ // Mock it to return the same ruleset both times.
619
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
620
+
621
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
622
+
623
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
624
+
625
+ mockExpect(
626
+ address(_terminal),
627
+ abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
628
+ abi.encode(_surplus)
629
+ );
630
+
631
+ mockExpect(
632
+ address(_controller),
633
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
634
+ abi.encode(_supply)
635
+ );
636
+
637
+ // Call the new convenience function.
638
+ uint256 reclaimableDefault = _store.currentTotalReclaimableSurplusOf(_projectId, _cashoutAmount, 18, _currency);
639
+
640
+ // Re-mock for the 6-param call (mocks are consumed).
641
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
642
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
643
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
644
+ mockExpect(
645
+ address(_terminal),
646
+ abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
647
+ abi.encode(_surplus)
648
+ );
649
+ mockExpect(
650
+ address(_controller),
651
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
652
+ abi.encode(_supply)
653
+ );
654
+
655
+ // Call the 6-param overload with empty arrays.
656
+ uint256 reclaimableExplicit = _store.currentReclaimableSurplusOf(
657
+ _projectId, _cashoutAmount, new IJBTerminal[](0), new JBAccountingContext[](0), 18, _currency
658
+ );
659
+
660
+ assertEq(reclaimableDefault, reclaimableExplicit);
661
+ }
447
662
  }