@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
@@ -6,6 +6,8 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6
6
  import {IJBPayHook} from "./IJBPayHook.sol";
7
7
  import {JBAccountingContext} from "../structs/JBAccountingContext.sol";
8
8
  import {JBAfterPayRecordedContext} from "../structs/JBAfterPayRecordedContext.sol";
9
+ import {JBPayHookSpecification} from "../structs/JBPayHookSpecification.sol";
10
+ import {JBRuleset} from "../structs/JBRuleset.sol";
9
11
 
10
12
  /// @notice A terminal that accepts payments and can be migrated.
11
13
  interface IJBTerminal is IERC165 {
@@ -102,6 +104,32 @@ interface IJBTerminal is IERC165 {
102
104
  view
103
105
  returns (uint256);
104
106
 
107
+ /// @notice Simulates paying a project through this terminal without modifying state.
108
+ /// @param projectId The ID of the project being paid.
109
+ /// @param token The token being paid in.
110
+ /// @param amount The amount of tokens being paid.
111
+ /// @param beneficiary The address to mint project tokens to.
112
+ /// @param metadata Extra data to pass along to the data hook and pay hooks.
113
+ /// @return ruleset The project's current ruleset.
114
+ /// @return beneficiaryTokenCount The number of project tokens that would be minted for the beneficiary.
115
+ /// @return reservedTokenCount The number of project tokens that would be reserved.
116
+ /// @return hookSpecifications Any pay hook specifications from the data hook.
117
+ function previewPayFor(
118
+ uint256 projectId,
119
+ address token,
120
+ uint256 amount,
121
+ address beneficiary,
122
+ bytes calldata metadata
123
+ )
124
+ external
125
+ view
126
+ returns (
127
+ JBRuleset memory ruleset,
128
+ uint256 beneficiaryTokenCount,
129
+ uint256 reservedTokenCount,
130
+ JBPayHookSpecification[] memory hookSpecifications
131
+ );
132
+
105
133
  /// @notice Adds accounting contexts for a project's tokens.
106
134
  /// @param projectId The ID of the project to add accounting contexts for.
107
135
  /// @param accountingContexts The accounting contexts to add.
@@ -65,6 +65,22 @@ interface IJBTerminalStore {
65
65
  view
66
66
  returns (uint256);
67
67
 
68
+ /// @notice Returns the reclaimable surplus for a project across all terminals using all accounting contexts.
69
+ /// @param projectId The ID of the project.
70
+ /// @param cashOutCount The number of tokens being cashed out.
71
+ /// @param decimals The number of decimals to express the result with.
72
+ /// @param currency The currency to express the result in.
73
+ /// @return The reclaimable surplus amount.
74
+ function currentTotalReclaimableSurplusOf(
75
+ uint256 projectId,
76
+ uint256 cashOutCount,
77
+ uint256 decimals,
78
+ uint256 currency
79
+ )
80
+ external
81
+ view
82
+ returns (uint256);
83
+
68
84
  /// @notice Returns the current surplus for a terminal and project.
69
85
  /// @param terminal The terminal to get the surplus of.
70
86
  /// @param projectId The ID of the project.
@@ -97,6 +113,56 @@ interface IJBTerminalStore {
97
113
  view
98
114
  returns (uint256);
99
115
 
116
+ /// @notice Simulates a cash out without modifying state.
117
+ /// @param holder The address cashing out.
118
+ /// @param projectId The ID of the project being cashed out from.
119
+ /// @param cashOutCount The number of project tokens being cashed out.
120
+ /// @param accountingContext The accounting context of the token being reclaimed.
121
+ /// @param balanceAccountingContexts The accounting contexts to include in the balance calculation.
122
+ /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
123
+ /// @param metadata Extra data to pass along to the data hook.
124
+ /// @return ruleset The project's current ruleset.
125
+ /// @return reclaimAmount The amount that would be reclaimed.
126
+ /// @return cashOutTaxRate The cash out tax rate that would be applied.
127
+ /// @return hookSpecifications Any cash out hook specifications from the data hook.
128
+ function previewCashOutFrom(
129
+ address holder,
130
+ uint256 projectId,
131
+ uint256 cashOutCount,
132
+ JBAccountingContext calldata accountingContext,
133
+ JBAccountingContext[] calldata balanceAccountingContexts,
134
+ bool beneficiaryIsFeeless,
135
+ bytes calldata metadata
136
+ )
137
+ external
138
+ view
139
+ returns (
140
+ JBRuleset memory ruleset,
141
+ uint256 reclaimAmount,
142
+ uint256 cashOutTaxRate,
143
+ JBCashOutHookSpecification[] memory hookSpecifications
144
+ );
145
+
146
+ /// @notice Simulates a payment without modifying state.
147
+ /// @param payer The address of the payer.
148
+ /// @param amount The amount being paid.
149
+ /// @param projectId The ID of the project being paid.
150
+ /// @param beneficiary The address to mint project tokens to.
151
+ /// @param metadata Extra data to pass along to the data hook.
152
+ /// @return ruleset The project's current ruleset.
153
+ /// @return tokenCount The number of project tokens that would be minted, including reserved tokens.
154
+ /// @return hookSpecifications Any pay hook specifications from the data hook.
155
+ function previewPayFrom(
156
+ address payer,
157
+ JBTokenAmount memory amount,
158
+ uint256 projectId,
159
+ address beneficiary,
160
+ bytes calldata metadata
161
+ )
162
+ external
163
+ view
164
+ returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications);
165
+
100
166
  /// @notice Returns the amount of payout limit used by a terminal for a project in a given cycle.
101
167
  /// @param terminal The terminal to get the used payout limit of.
102
168
  /// @param projectId The ID of the project.
@@ -0,0 +1,157 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {mulDiv} from "@prb/math/src/Common.sol";
5
+
6
+ import {IJBPayoutTerminal} from "../interfaces/IJBPayoutTerminal.sol";
7
+ import {IJBSplits} from "../interfaces/IJBSplits.sol";
8
+ import {IJBTerminalStore} from "../interfaces/IJBTerminalStore.sol";
9
+ import {JBSplit} from "../structs/JBSplit.sol";
10
+ import {JBConstants} from "./JBConstants.sol";
11
+
12
+ /// @notice Minimal callback surface used only by this library to call back into the terminal's `executePayout(...)`.
13
+ /// @dev Kept local to this file because `executePayout(...)` is an implementation detail, not a shared public
14
+ /// interface.
15
+ interface IJBPayoutSplitGroupExecutor {
16
+ function executePayout(
17
+ JBSplit calldata split,
18
+ uint256 projectId,
19
+ address token,
20
+ uint256 amount,
21
+ address originalMessageSender
22
+ )
23
+ external
24
+ returns (uint256 netPayoutAmount);
25
+ }
26
+
27
+ /// @notice External library for payout split-group distribution extracted to reduce terminal bytecode.
28
+ /// @dev Called via DELEGATECALL from the terminal, so events are emitted from the terminal's address.
29
+ library JBPayoutSplitGroupLib {
30
+ event PayoutReverted(uint256 indexed projectId, JBSplit split, uint256 amount, bytes reason, address caller);
31
+ event SendPayoutToSplit(
32
+ uint256 indexed projectId,
33
+ uint256 indexed rulesetId,
34
+ uint256 indexed group,
35
+ JBSplit split,
36
+ uint256 amount,
37
+ uint256 netAmount,
38
+ address caller
39
+ );
40
+
41
+ /// @notice Sends payouts to the payout splits group specified in a project's ruleset.
42
+ /// @param splits The splits contract to read splits from.
43
+ /// @param store The terminal store used to restore balance when a payout fails.
44
+ /// @param projectId The ID of the project to send the payouts of.
45
+ /// @param token The address of the token being paid out.
46
+ /// @param rulesetId The ID of the ruleset of the split group being paid.
47
+ /// @param amount The total amount being paid out.
48
+ /// @param caller The original caller of the terminal payout flow.
49
+ /// @return leftoverAmount The leftover amount after split payouts.
50
+ /// @return amountEligibleForFees The amount of payouts that are eligible for fees.
51
+ function sendPayoutsToSplitGroupOf(
52
+ IJBSplits splits,
53
+ IJBTerminalStore store,
54
+ uint256 projectId,
55
+ address token,
56
+ uint256 rulesetId,
57
+ uint256 amount,
58
+ address caller
59
+ )
60
+ external
61
+ returns (uint256 leftoverAmount, uint256 amountEligibleForFees)
62
+ {
63
+ // The total percentage available to split.
64
+ uint256 leftoverPercentage = JBConstants.SPLITS_TOTAL_PERCENT;
65
+ uint256 group = uint256(uint160(token));
66
+
67
+ // Get a reference to the project's payout splits.
68
+ JBSplit[] memory payoutSplits = splits.splitsOf({projectId: projectId, rulesetId: rulesetId, groupId: group});
69
+
70
+ leftoverAmount = amount;
71
+
72
+ // Transfer between all splits.
73
+ for (uint256 i; i < payoutSplits.length; i++) {
74
+ // Get a reference to the split being iterated on.
75
+ JBSplit memory split = payoutSplits[i];
76
+
77
+ // The amount to send to the split.
78
+ uint256 payoutAmount = mulDiv(leftoverAmount, split.percent, leftoverPercentage);
79
+
80
+ // The final payout amount after taking out any fees.
81
+ uint256 netPayoutAmount = _sendPayoutToSplit({
82
+ store: store, split: split, projectId: projectId, token: token, amount: payoutAmount, caller: caller
83
+ });
84
+
85
+ // If the split hook is a feeless address, this payout doesn't incur a fee.
86
+ if (netPayoutAmount != 0 && netPayoutAmount != payoutAmount) {
87
+ amountEligibleForFees += payoutAmount;
88
+ }
89
+
90
+ if (payoutAmount != 0) {
91
+ // Subtract from the amount to be sent to the beneficiary.
92
+ unchecked {
93
+ leftoverAmount -= payoutAmount;
94
+ }
95
+ }
96
+
97
+ unchecked {
98
+ // Decrement the leftover percentage.
99
+ leftoverPercentage -= split.percent;
100
+ }
101
+
102
+ emit SendPayoutToSplit({
103
+ projectId: projectId,
104
+ rulesetId: rulesetId,
105
+ group: group,
106
+ split: split,
107
+ amount: payoutAmount,
108
+ netAmount: netPayoutAmount,
109
+ caller: caller
110
+ });
111
+ }
112
+ }
113
+
114
+ /// @notice Sends a payout to a split.
115
+ /// @param store The terminal store used to restore balance when a payout fails.
116
+ /// @param split The split to pay.
117
+ /// @param projectId The ID of the project the split was specified by.
118
+ /// @param token The address of the token being paid out.
119
+ /// @param amount The total amount that the split is being paid.
120
+ /// @param caller The original caller of the terminal payout flow.
121
+ /// @return netPayoutAmount The amount sent to the split after subtracting fees.
122
+ function _sendPayoutToSplit(
123
+ IJBTerminalStore store,
124
+ JBSplit memory split,
125
+ uint256 projectId,
126
+ address token,
127
+ uint256 amount,
128
+ address caller
129
+ )
130
+ private
131
+ returns (uint256 netPayoutAmount)
132
+ {
133
+ // Failed split payouts consume the payout limit by design. The try-catch prevents a single
134
+ // split from DoS-ing the entire payout. Failed splits' amounts are returned to the project balance via
135
+ // `recordAddedBalanceFor`. Payout limit consumption is correct because the project authorized the
136
+ // distribution.
137
+ // slither-disable-next-line reentrancy-events
138
+ try IJBPayoutSplitGroupExecutor(address(this))
139
+ .executePayout({
140
+ split: split, projectId: projectId, token: token, amount: amount, originalMessageSender: caller
141
+ }) returns (
142
+ uint256 payoutAmount
143
+ ) {
144
+ return payoutAmount;
145
+ } catch (bytes memory failureReason) {
146
+ emit PayoutReverted({
147
+ projectId: projectId, split: split, amount: amount, reason: failureReason, caller: caller
148
+ });
149
+
150
+ // Add balance back to the project.
151
+ store.recordAddedBalanceFor({projectId: projectId, token: token, amount: amount});
152
+
153
+ // Since the payout failed the netPayoutAmount is zero.
154
+ return 0;
155
+ }
156
+ }
157
+ }
@@ -6,11 +6,13 @@ import {IJBCashOutHook} from "../interfaces/IJBCashOutHook.sol";
6
6
  /// @notice A cash out hook specification sent from the ruleset's data hook back to the terminal. This specification is
7
7
  /// fulfilled by the terminal.
8
8
  /// @custom:member hook The cash out hook to use when fulfilling this specification.
9
+ /// @custom:member noop A flag indicating if the hook callback should be skipped.
9
10
  /// @custom:member amount The amount to send to the hook.
10
11
  /// @custom:member metadata Metadata to pass to the hook.
11
12
  // forge-lint: disable-next-line(pascal-case-struct)
12
13
  struct JBCashOutHookSpecification {
13
14
  IJBCashOutHook hook;
15
+ bool noop;
14
16
  uint256 amount;
15
17
  bytes metadata;
16
18
  }
@@ -6,11 +6,13 @@ import {IJBPayHook} from "../interfaces/IJBPayHook.sol";
6
6
  /// @notice A pay hook specification sent from the ruleset's data hook back to the terminal. This specification is
7
7
  /// fulfilled by the terminal.
8
8
  /// @custom:member hook The pay hook to use when fulfilling this specification.
9
+ /// @custom:member noop A flag indicating if the hook callback should be skipped.
9
10
  /// @custom:member amount The amount to send to the hook.
10
11
  /// @custom:member metadata Metadata to pass the hook.
11
12
  // forge-lint: disable-next-line(pascal-case-struct)
12
13
  struct JBPayHookSpecification {
13
14
  IJBPayHook hook;
15
+ bool noop;
14
16
  uint256 amount;
15
17
  bytes metadata;
16
18
  }
@@ -358,7 +358,7 @@ contract EdgeCases_Local is TestBaseWorkflow {
358
358
  /// requires a call boundary to catch the panic.
359
359
  function externalWriteToIndex1(JBPayHookSpecification[] memory specs) external pure {
360
360
  // This will revert with Panic(0x32) -- array index out of bounds.
361
- specs[1] = JBPayHookSpecification({hook: IJBPayHook(address(0)), amount: 0, metadata: ""});
361
+ specs[1] = JBPayHookSpecification({hook: IJBPayHook(address(0)), noop: false, amount: 0, metadata: ""});
362
362
  }
363
363
 
364
364
  /// @notice Verify that when BOTH hooks are present, the array size is 2 and no OOB occurs.
@@ -372,8 +372,10 @@ contract EdgeCases_Local is TestBaseWorkflow {
372
372
  JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](arraySize);
373
373
 
374
374
  // Both writes are valid.
375
- hookSpecifications[0] = JBPayHookSpecification({hook: IJBPayHook(address(0)), amount: 0, metadata: ""});
376
- hookSpecifications[1] = JBPayHookSpecification({hook: IJBPayHook(address(0)), amount: 0, metadata: ""});
375
+ hookSpecifications[0] =
376
+ JBPayHookSpecification({hook: IJBPayHook(address(0)), noop: false, amount: 0, metadata: ""});
377
+ hookSpecifications[1] =
378
+ JBPayHookSpecification({hook: IJBPayHook(address(0)), noop: false, amount: 0, metadata: ""});
377
379
  }
378
380
 
379
381
  /// @notice Verify that when neither hook is present, no write occurs.
@@ -1171,7 +1173,9 @@ contract EdgeCases_Local is TestBaseWorkflow {
1171
1173
  // Replicate REVDeployer logic exactly:
1172
1174
  // if (usesTiered721Hook) hookSpecifications[0] = ...
1173
1175
  if (usesT) {
1174
- specs[0] = JBPayHookSpecification({hook: IJBPayHook(address(0xdead)), amount: 1 ether, metadata: ""});
1176
+ specs[0] = JBPayHookSpecification({
1177
+ hook: IJBPayHook(address(0xdead)), noop: false, amount: 1 ether, metadata: ""
1178
+ });
1175
1179
  }
1176
1180
 
1177
1181
  // if (usesBuybackHook) hookSpecifications[1] = ... // ALWAYS index 1!
@@ -1182,8 +1186,9 @@ contract EdgeCases_Local is TestBaseWorkflow {
1182
1186
  this.externalWriteToIndex1(specs);
1183
1187
  } else {
1184
1188
  // usesT=true, usesB=true → size=2, writing to [1] → OK
1185
- specs[1] =
1186
- JBPayHookSpecification({hook: IJBPayHook(address(0xbeef)), amount: 2 ether, metadata: ""});
1189
+ specs[1] = JBPayHookSpecification({
1190
+ hook: IJBPayHook(address(0xbeef)), noop: false, amount: 2 ether, metadata: ""
1191
+ });
1187
1192
  }
1188
1193
  }
1189
1194
  }
@@ -1496,6 +1501,10 @@ contract EdgeCases_Local is TestBaseWorkflow {
1496
1501
 
1497
1502
  /// @notice Verify processHeldFeesOf correctly handles partial unlock (some locked, some unlocked).
1498
1503
  function test_partialLockedFees() public {
1504
+ // Use an explicit warp to a realistic timestamp to avoid via_ir reordering of block.timestamp reads.
1505
+ vm.warp(1_000_000);
1506
+ uint256 _startTime = 1_000_000;
1507
+
1499
1508
  // Launch fee project.
1500
1509
  JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
1501
1510
  JBAccountingContext[] memory tokensToAccept = new JBAccountingContext[](1);
@@ -1615,7 +1624,9 @@ contract EdgeCases_Local is TestBaseWorkflow {
1615
1624
  });
1616
1625
 
1617
1626
  // Warp 14 days (halfway through holding period).
1618
- vm.warp(block.timestamp + 14 days);
1627
+ uint256 _fee1Time = _startTime;
1628
+ uint256 _midPoint = _fee1Time + 14 days;
1629
+ vm.warp(_midPoint);
1619
1630
 
1620
1631
  // Create second held fee (this one will be locked when we process at day 28).
1621
1632
  vm.prank(_projectOwner);
@@ -1635,7 +1646,7 @@ contract EdgeCases_Local is TestBaseWorkflow {
1635
1646
  assertEq(heldFees.length, 2, "should have 2 held fees");
1636
1647
 
1637
1648
  // Warp to 28 days + 1 from the start (first fee unlocked, second still locked 14 more days).
1638
- vm.warp(block.timestamp + 14 days + 1);
1649
+ vm.warp(_midPoint + 14 days + 1);
1639
1650
 
1640
1651
  // Process all — should only process the first (unlocked) fee and stop at the second (locked).
1641
1652
  _terminal.processHeldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
@@ -1644,8 +1655,8 @@ contract EdgeCases_Local is TestBaseWorkflow {
1644
1655
  JBFee[] memory remainingFees = _terminal.heldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
1645
1656
  assertEq(remainingFees.length, 1, "one fee should still be locked");
1646
1657
 
1647
- // Warp past second fee's unlock.
1648
- vm.warp(block.timestamp + 14 days + 1);
1658
+ // Warp past second fee's unlock (second fee was created at _midPoint, unlocks at _midPoint + 28 days).
1659
+ vm.warp(_midPoint + 28 days + 1);
1649
1660
 
1650
1661
  // Process the remaining fee.
1651
1662
  _terminal.processHeldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
@@ -156,8 +156,9 @@ contract TestCashOutHooks_Local is TestBaseWorkflow {
156
156
  // Reference cash out hook specifications.
157
157
  JBCashOutHookSpecification[] memory _specifications = new JBCashOutHookSpecification[](1);
158
158
 
159
- _specifications[0] =
160
- JBCashOutHookSpecification({hook: IJBCashOutHook(_cashOutHook), amount: _halfPaid, metadata: ""});
159
+ _specifications[0] = JBCashOutHookSpecification({
160
+ hook: IJBCashOutHook(_cashOutHook), noop: false, amount: _halfPaid, metadata: ""
161
+ });
161
162
 
162
163
  vm.startPrank(multisig());
163
164
  // Set the hook as feeless.
@@ -260,8 +261,9 @@ contract TestCashOutHooks_Local is TestBaseWorkflow {
260
261
  // Reference cash out hook specifications.
261
262
  JBCashOutHookSpecification[] memory _specifications = new JBCashOutHookSpecification[](1);
262
263
 
263
- _specifications[0] =
264
- JBCashOutHookSpecification({hook: IJBCashOutHook(_cashOutHook), amount: _halfPaid, metadata: ""});
264
+ _specifications[0] = JBCashOutHookSpecification({
265
+ hook: IJBCashOutHook(_cashOutHook), noop: false, amount: _halfPaid, metadata: ""
266
+ });
265
267
 
266
268
  uint256 _customCashOutTaxRate = JBConstants.MAX_CASH_OUT_TAX_RATE / 2;
267
269
  uint256 _customCashOutCount = 1 * 10 ** 18;
@@ -206,6 +206,7 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
206
206
  JBPayHookSpecification[] memory _specs = new JBPayHookSpecification[](1);
207
207
  _specs[0] = JBPayHookSpecification({
208
208
  hook: IJBPayHook(_payHook),
209
+ noop: false,
209
210
  amount: _payAmount + 1, // exceeds the paid amount
210
211
  metadata: ""
211
212
  });
@@ -327,7 +328,9 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
327
328
  jbFeelessAddresses().setFeelessAddress(_cashOutHook, true);
328
329
 
329
330
  JBCashOutHookSpecification[] memory _specs = new JBCashOutHookSpecification[](1);
330
- _specs[0] = JBCashOutHookSpecification({hook: IJBCashOutHook(_cashOutHook), amount: _hookAmount, metadata: ""});
331
+ _specs[0] = JBCashOutHookSpecification({
332
+ hook: IJBCashOutHook(_cashOutHook), noop: false, amount: _hookAmount, metadata: ""
333
+ });
331
334
 
332
335
  // Override: cashOutTaxRate=0, half the tokens cashed out, original totalSupply.
333
336
  vm.mockCall(
@@ -491,7 +494,8 @@ contract TestDataHookFuzzing_Local is TestBaseWorkflow {
491
494
  // Cash out all tokens. With cashOutTaxRate=0, reclaim = full surplus = _payAmount.
492
495
  // Set hook spec amount to 1 wei -- total = _payAmount + 1 which exceeds balance.
493
496
  JBCashOutHookSpecification[] memory _specs = new JBCashOutHookSpecification[](1);
494
- _specs[0] = JBCashOutHookSpecification({hook: IJBCashOutHook(makeAddr("hook")), amount: 1, metadata: ""});
497
+ _specs[0] =
498
+ JBCashOutHookSpecification({hook: IJBCashOutHook(makeAddr("hook")), noop: false, amount: 1, metadata: ""});
495
499
 
496
500
  vm.mockCall(
497
501
  _DATA_HOOK,
@@ -136,7 +136,7 @@ contract TestPayHooks_Local is TestBaseWorkflow {
136
136
 
137
137
  // Package up the specification struct.
138
138
  _specifications[i] = JBPayHookSpecification({
139
- hook: IJBPayHook(_hookAddress), amount: _payHookAmounts[i], metadata: _hookMetadata
139
+ hook: IJBPayHook(_hookAddress), noop: false, amount: _payHookAmounts[i], metadata: _hookMetadata
140
140
  });
141
141
 
142
142
  // Keep a reference to the data that'll be received by the hook.
@@ -39,7 +39,7 @@ contract TestRulesetQueuing_Local is TestBaseWorkflow {
39
39
  super.setUp();
40
40
 
41
41
  // Foundry defaults block.timestamp to 1, which causes underflow in tests using past timestamps.
42
- vm.warp(7 days);
42
+ vm.warp(1_000_001);
43
43
 
44
44
  _terminal = jbMultiTerminal();
45
45
  _controller = jbController();
@@ -637,9 +637,6 @@ contract TestRulesetQueuing_Local is TestBaseWorkflow {
637
637
  uint112 _weightFirstQueued = uint112(1234 * 10 ** 18);
638
638
  uint112 _weightSecondQueued = uint112(6969 * 10 ** 18);
639
639
 
640
- // Keep a reference to the expected ruleset IDs (timestamps).
641
- uint256 _initialRulesetId = block.timestamp;
642
-
643
640
  // Package up a config.
644
641
  JBRulesetConfig[] memory _rulesetConfig = new JBRulesetConfig[](1);
645
642
  _rulesetConfig[0].mustStartAtOrAfter = 0;
@@ -657,10 +654,12 @@ contract TestRulesetQueuing_Local is TestBaseWorkflow {
657
654
  // Get the ruleset.
658
655
  JBRuleset memory _ruleset = jbRulesets().currentOf(projectId);
659
656
 
657
+ // Keep a reference to the expected ruleset IDs (use returned id to avoid via_ir reordering of block.timestamp).
658
+ uint256 _initialRulesetId = _ruleset.id;
659
+
660
660
  // Make sure the first ruleset has begun.
661
661
  assertEq(_ruleset.cycleNumber, 1);
662
662
  assertEq(_ruleset.weight, _weightInitial);
663
- assertEq(_ruleset.id, block.timestamp);
664
663
 
665
664
  // Package up a new config.
666
665
  JBRulesetConfig[] memory _firstQueued = new JBRulesetConfig[](1);
@@ -236,10 +236,11 @@ contract MockApprovalHookConfigurable is IJBRulesetApprovalHook {
236
236
  /// @notice Mid-cycle queuing: new ruleset starts at next duration boundary.
237
237
  function test_midCycleQueuing_snapsToNextBoundary() external {
238
238
  uint256 pid = _launchProject(SEVEN_DAYS, INITIAL_WEIGHT, 0, IJBRulesetApprovalHook(address(0)));
239
- uint256 originalStart = block.timestamp;
239
+ // Capture originalStart from the actual ruleset (avoid via_ir reordering of block.timestamp).
240
+ uint256 originalStart = _rulesets.currentOf(pid).start;
240
241
 
241
242
  // Warp to mid-cycle (day 3 of 7).
242
- vm.warp(block.timestamp + 3 days);
243
+ vm.warp(originalStart + 3 days);
243
244
 
244
245
  _queueRuleset(pid, 0, SEVEN_DAYS, INITIAL_WEIGHT * 2, 0, IJBRulesetApprovalHook(address(0)));
245
246
 
@@ -573,7 +574,8 @@ contract MockApprovalHookConfigurable is IJBRulesetApprovalHook {
573
574
  /// @notice deriveStartFrom one second after boundary: snaps to next boundary.
574
575
  function test_deriveStartFrom_oneSecondAfterBoundary_snapsToNext() external {
575
576
  uint256 pid = _launchProject(SEVEN_DAYS, INITIAL_WEIGHT, 0, IJBRulesetApprovalHook(address(0)));
576
- uint256 originalStart = block.timestamp;
577
+ // Capture originalStart from the actual ruleset (avoid via_ir reordering of block.timestamp).
578
+ uint256 originalStart = _rulesets.currentOf(pid).start;
577
579
 
578
580
  // 1 second after first boundary -> should snap to second boundary.
579
581
  _queueRuleset(