@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
@@ -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
+ }
@@ -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.