@bananapus/core-v6 0.0.16 → 0.0.18

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 (140) hide show
  1. package/ADMINISTRATION.md +1 -1
  2. package/ARCHITECTURE.md +2 -1
  3. package/AUDIT_INSTRUCTIONS.md +342 -0
  4. package/CHANGE_LOG.md +400 -0
  5. package/README.md +4 -4
  6. package/RISKS.md +171 -50
  7. package/SKILLS.md +9 -6
  8. package/USER_JOURNEYS.md +622 -0
  9. package/package.json +2 -2
  10. package/script/DeployPeriphery.s.sol +7 -1
  11. package/src/JBController.sol +5 -0
  12. package/src/JBDeadline.sol +3 -0
  13. package/src/JBDirectory.sol +2 -1
  14. package/src/JBMultiTerminal.sol +50 -9
  15. package/src/JBPermissions.sol +2 -0
  16. package/src/JBPrices.sol +8 -2
  17. package/src/JBRulesets.sol +3 -0
  18. package/src/JBSplits.sol +9 -5
  19. package/src/JBTerminalStore.sol +54 -47
  20. package/src/JBTokens.sol +3 -0
  21. package/src/interfaces/IJBTerminalStore.sol +3 -0
  22. package/src/libraries/JBFees.sol +2 -0
  23. package/src/libraries/JBMetadataResolver.sol +17 -4
  24. package/src/structs/JBBeforeCashOutRecordedContext.sol +4 -0
  25. package/test/TestAuditResponseDesignProofs.sol +434 -0
  26. package/test/TestDataHookFuzzing.sol +520 -0
  27. package/test/TestFeeFreeCashOutBypass.sol +617 -0
  28. package/test/TestL2SequencerPriceFeed.sol +292 -0
  29. package/test/TestMetadataOffsetOverflow.sol +179 -0
  30. package/test/TestMultiTerminalSurplus.sol +348 -0
  31. package/test/TestPermit2DataHook.t.sol +360 -0
  32. package/test/TestRulesetQueueing.sol +1 -2
  33. package/test/TestRulesetWeightCaching.sol +122 -124
  34. package/test/WeirdTokenTests.t.sol +37 -0
  35. package/test/regression/HoldFeesCashOutReserved.t.sol +415 -0
  36. package/test/regression/WeightCacheBoundary.t.sol +291 -0
  37. package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +2 -2
  38. package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +18 -17
  39. package/test/units/static/JBMultiTerminal/TestPay.sol +6 -4
  40. package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +206 -18
  41. package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +280 -0
  42. package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +55 -12
  43. package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +72 -0
  44. package/docs/book.css +0 -13
  45. package/docs/book.toml +0 -12
  46. package/docs/solidity.min.js +0 -74
  47. package/docs/src/README.md +0 -703
  48. package/docs/src/SUMMARY.md +0 -94
  49. package/docs/src/src/JBChainlinkV3PriceFeed.sol/contract.JBChainlinkV3PriceFeed.md +0 -83
  50. package/docs/src/src/JBChainlinkV3SequencerPriceFeed.sol/contract.JBChainlinkV3SequencerPriceFeed.md +0 -88
  51. package/docs/src/src/JBController.sol/contract.JBController.md +0 -1121
  52. package/docs/src/src/JBDeadline.sol/contract.JBDeadline.md +0 -84
  53. package/docs/src/src/JBDirectory.sol/contract.JBDirectory.md +0 -294
  54. package/docs/src/src/JBERC20.sol/contract.JBERC20.md +0 -190
  55. package/docs/src/src/JBFeelessAddresses.sol/contract.JBFeelessAddresses.md +0 -80
  56. package/docs/src/src/JBFundAccessLimits.sol/contract.JBFundAccessLimits.md +0 -253
  57. package/docs/src/src/JBMultiTerminal.sol/contract.JBMultiTerminal.md +0 -1472
  58. package/docs/src/src/JBPermissions.sol/contract.JBPermissions.md +0 -199
  59. package/docs/src/src/JBPrices.sol/contract.JBPrices.md +0 -154
  60. package/docs/src/src/JBProjects.sol/contract.JBProjects.md +0 -131
  61. package/docs/src/src/JBRulesets.sol/contract.JBRulesets.md +0 -677
  62. package/docs/src/src/JBSplits.sol/contract.JBSplits.md +0 -237
  63. package/docs/src/src/JBTerminalStore.sol/contract.JBTerminalStore.md +0 -591
  64. package/docs/src/src/JBTokens.sol/contract.JBTokens.md +0 -353
  65. package/docs/src/src/README.md +0 -25
  66. package/docs/src/src/abstract/JBControlled.sol/abstract.JBControlled.md +0 -64
  67. package/docs/src/src/abstract/JBPermissioned.sol/abstract.JBPermissioned.md +0 -84
  68. package/docs/src/src/abstract/README.md +0 -5
  69. package/docs/src/src/enums/JBApprovalStatus.sol/enum.JBApprovalStatus.md +0 -17
  70. package/docs/src/src/enums/README.md +0 -4
  71. package/docs/src/src/interfaces/IJBCashOutHook.sol/interface.IJBCashOutHook.md +0 -29
  72. package/docs/src/src/interfaces/IJBCashOutTerminal.sol/interface.IJBCashOutTerminal.md +0 -57
  73. package/docs/src/src/interfaces/IJBControlled.sol/interface.IJBControlled.md +0 -12
  74. package/docs/src/src/interfaces/IJBController.sol/interface.IJBController.md +0 -334
  75. package/docs/src/src/interfaces/IJBDirectory.sol/interface.IJBDirectory.md +0 -108
  76. package/docs/src/src/interfaces/IJBDirectoryAccessControl.sol/interface.IJBDirectoryAccessControl.md +0 -19
  77. package/docs/src/src/interfaces/IJBFeeTerminal.sol/interface.IJBFeeTerminal.md +0 -91
  78. package/docs/src/src/interfaces/IJBFeelessAddresses.sol/interface.IJBFeelessAddresses.md +0 -26
  79. package/docs/src/src/interfaces/IJBFundAccessLimits.sol/interface.IJBFundAccessLimits.md +0 -88
  80. package/docs/src/src/interfaces/IJBMigratable.sol/interface.IJBMigratable.md +0 -29
  81. package/docs/src/src/interfaces/IJBMultiTerminal.sol/interface.IJBMultiTerminal.md +0 -50
  82. package/docs/src/src/interfaces/IJBPayHook.sol/interface.IJBPayHook.md +0 -28
  83. package/docs/src/src/interfaces/IJBPayoutTerminal.sol/interface.IJBPayoutTerminal.md +0 -105
  84. package/docs/src/src/interfaces/IJBPermissioned.sol/interface.IJBPermissioned.md +0 -12
  85. package/docs/src/src/interfaces/IJBPermissions.sol/interface.IJBPermissions.md +0 -74
  86. package/docs/src/src/interfaces/IJBPermitTerminal.sol/interface.IJBPermitTerminal.md +0 -15
  87. package/docs/src/src/interfaces/IJBPriceFeed.sol/interface.IJBPriceFeed.md +0 -12
  88. package/docs/src/src/interfaces/IJBPrices.sol/interface.IJBPrices.md +0 -74
  89. package/docs/src/src/interfaces/IJBProjectUriRegistry.sol/interface.IJBProjectUriRegistry.md +0 -19
  90. package/docs/src/src/interfaces/IJBProjects.sol/interface.IJBProjects.md +0 -49
  91. package/docs/src/src/interfaces/IJBRulesetApprovalHook.sol/interface.IJBRulesetApprovalHook.md +0 -35
  92. package/docs/src/src/interfaces/IJBRulesetDataHook.sol/interface.IJBRulesetDataHook.md +0 -97
  93. package/docs/src/src/interfaces/IJBRulesets.sol/interface.IJBRulesets.md +0 -165
  94. package/docs/src/src/interfaces/IJBSplitHook.sol/interface.IJBSplitHook.md +0 -31
  95. package/docs/src/src/interfaces/IJBSplits.sol/interface.IJBSplits.md +0 -35
  96. package/docs/src/src/interfaces/IJBTerminal.sol/interface.IJBTerminal.md +0 -141
  97. package/docs/src/src/interfaces/IJBTerminalStore.sol/interface.IJBTerminalStore.md +0 -198
  98. package/docs/src/src/interfaces/IJBToken.sol/interface.IJBToken.md +0 -54
  99. package/docs/src/src/interfaces/IJBTokenUriResolver.sol/interface.IJBTokenUriResolver.md +0 -12
  100. package/docs/src/src/interfaces/IJBTokens.sol/interface.IJBTokens.md +0 -151
  101. package/docs/src/src/interfaces/README.md +0 -33
  102. package/docs/src/src/libraries/JBCashOuts.sol/library.JBCashOuts.md +0 -40
  103. package/docs/src/src/libraries/JBConstants.sol/library.JBConstants.md +0 -52
  104. package/docs/src/src/libraries/JBCurrencyIds.sol/library.JBCurrencyIds.md +0 -19
  105. package/docs/src/src/libraries/JBFees.sol/library.JBFees.md +0 -52
  106. package/docs/src/src/libraries/JBFixedPointNumber.sol/library.JBFixedPointNumber.md +0 -12
  107. package/docs/src/src/libraries/JBMetadataResolver.sol/library.JBMetadataResolver.md +0 -242
  108. package/docs/src/src/libraries/JBRulesetMetadataResolver.sol/library.JBRulesetMetadataResolver.md +0 -180
  109. package/docs/src/src/libraries/JBSplitGroupIds.sol/library.JBSplitGroupIds.md +0 -14
  110. package/docs/src/src/libraries/JBSurplus.sol/library.JBSurplus.md +0 -44
  111. package/docs/src/src/libraries/README.md +0 -12
  112. package/docs/src/src/periphery/JBDeadline1Day.sol/contract.JBDeadline1Day.md +0 -15
  113. package/docs/src/src/periphery/JBDeadline3Days.sol/contract.JBDeadline3Days.md +0 -15
  114. package/docs/src/src/periphery/JBDeadline3Hours.sol/contract.JBDeadline3Hours.md +0 -15
  115. package/docs/src/src/periphery/JBDeadline7Days.sol/contract.JBDeadline7Days.md +0 -15
  116. package/docs/src/src/periphery/JBMatchingPriceFeed.sol/contract.JBMatchingPriceFeed.md +0 -22
  117. package/docs/src/src/periphery/README.md +0 -8
  118. package/docs/src/src/structs/JBAccountingContext.sol/struct.JBAccountingContext.md +0 -20
  119. package/docs/src/src/structs/JBAfterCashOutRecordedContext.sol/struct.JBAfterCashOutRecordedContext.md +0 -43
  120. package/docs/src/src/structs/JBAfterPayRecordedContext.sol/struct.JBAfterPayRecordedContext.md +0 -42
  121. package/docs/src/src/structs/JBBeforeCashOutRecordedContext.sol/struct.JBBeforeCashOutRecordedContext.md +0 -45
  122. package/docs/src/src/structs/JBBeforePayRecordedContext.sol/struct.JBBeforePayRecordedContext.md +0 -41
  123. package/docs/src/src/structs/JBCashOutHookSpecification.sol/struct.JBCashOutHookSpecification.md +0 -22
  124. package/docs/src/src/structs/JBCurrencyAmount.sol/struct.JBCurrencyAmount.md +0 -17
  125. package/docs/src/src/structs/JBFee.sol/struct.JBFee.md +0 -20
  126. package/docs/src/src/structs/JBFundAccessLimitGroup.sol/struct.JBFundAccessLimitGroup.md +0 -39
  127. package/docs/src/src/structs/JBPayHookSpecification.sol/struct.JBPayHookSpecification.md +0 -22
  128. package/docs/src/src/structs/JBPermissionsData.sol/struct.JBPermissionsData.md +0 -21
  129. package/docs/src/src/structs/JBRuleset.sol/struct.JBRuleset.md +0 -55
  130. package/docs/src/src/structs/JBRulesetConfig.sol/struct.JBRulesetConfig.md +0 -51
  131. package/docs/src/src/structs/JBRulesetMetadata.sol/struct.JBRulesetMetadata.md +0 -79
  132. package/docs/src/src/structs/JBRulesetWeightCache.sol/struct.JBRulesetWeightCache.md +0 -16
  133. package/docs/src/src/structs/JBRulesetWithMetadata.sol/struct.JBRulesetWithMetadata.md +0 -16
  134. package/docs/src/src/structs/JBSingleAllowance.sol/struct.JBSingleAllowance.md +0 -26
  135. package/docs/src/src/structs/JBSplit.sol/struct.JBSplit.md +0 -49
  136. package/docs/src/src/structs/JBSplitGroup.sol/struct.JBSplitGroup.md +0 -17
  137. package/docs/src/src/structs/JBSplitHookContext.sol/struct.JBSplitHookContext.md +0 -29
  138. package/docs/src/src/structs/JBTerminalConfig.sol/struct.JBTerminalConfig.md +0 -16
  139. package/docs/src/src/structs/JBTokenAmount.sol/struct.JBTokenAmount.md +0 -23
  140. package/docs/src/src/structs/README.md +0 -25
@@ -0,0 +1,291 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity >=0.8.6;
3
+
4
+ import {TestBaseWorkflow} from "../helpers/TestBaseWorkflow.sol";
5
+ import {IJBController} from "../../src/interfaces/IJBController.sol";
6
+ import {IJBRulesets} from "../../src/interfaces/IJBRulesets.sol";
7
+ import {IJBRulesetApprovalHook} from "../../src/interfaces/IJBRulesetApprovalHook.sol";
8
+ import {JBConstants} from "../../src/libraries/JBConstants.sol";
9
+ import {JBRulesetMetadata} from "../../src/structs/JBRulesetMetadata.sol";
10
+ import {JBRulesetConfig} from "../../src/structs/JBRulesetConfig.sol";
11
+ import {JBRuleset} from "../../src/structs/JBRuleset.sol";
12
+ import {JBSplitGroup} from "../../src/structs/JBSplitGroup.sol";
13
+ import {JBFundAccessLimitGroup} from "../../src/structs/JBFundAccessLimitGroup.sol";
14
+ import {JBTerminalConfig} from "../../src/structs/JBTerminalConfig.sol";
15
+ import {JBRulesets} from "../../src/JBRulesets.sol";
16
+
17
+ import {mulDiv} from "@prb/math/src/Common.sol";
18
+
19
+ /// @notice Deterministic (non-fuzz) tests for the JBRulesets weight cache at the 20,000 iteration boundary.
20
+ /// The contract reverts with `JBRulesets_WeightCacheRequired` when the cycle gap exceeds the threshold
21
+ /// and the cache has not been populated. These tests verify:
22
+ /// 1. Exactly 20,000 cycles work without cache.
23
+ /// 2. 20,001 cycles revert without cache.
24
+ /// 3. Progressive `updateRulesetWeightCache()` calls unblock the revert.
25
+ /// 4. The cached weight equals the iteratively computed weight.
26
+ contract WeightCacheBoundary_Local is TestBaseWorkflow {
27
+ uint256 private constant _THRESHOLD = 20_000;
28
+
29
+ /// @dev 1-second duration makes each cycle = 1 second, so we can hit the boundary fast.
30
+ uint32 private constant _DURATION = 1;
31
+
32
+ /// @dev Tiny weight cut (0.1%) so the weight doesn't decay to zero over 20k cycles.
33
+ uint32 private constant _WEIGHT_CUT_PERCENT = 1_000_000; // 0.1% of MAX_WEIGHT_CUT_PERCENT (1e9)
34
+
35
+ uint112 private constant _INITIAL_WEIGHT = 1000e18;
36
+
37
+ IJBController private _controller;
38
+ IJBRulesets private _rulesets;
39
+ address private _projectOwner;
40
+
41
+ JBRulesetMetadata private _metadata;
42
+
43
+ function setUp() public override {
44
+ super.setUp();
45
+
46
+ _projectOwner = multisig();
47
+ _rulesets = jbRulesets();
48
+ _controller = jbController();
49
+
50
+ _metadata = JBRulesetMetadata({
51
+ reservedPercent: 0,
52
+ cashOutTaxRate: 0,
53
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
54
+ pausePay: false,
55
+ pauseCreditTransfers: false,
56
+ allowOwnerMinting: false,
57
+ allowSetCustomToken: false,
58
+ allowTerminalMigration: false,
59
+ allowSetTerminals: false,
60
+ ownerMustSendPayouts: false,
61
+ allowSetController: false,
62
+ allowAddAccountingContext: true,
63
+ allowAddPriceFeed: false,
64
+ holdFees: false,
65
+ useTotalSurplusForCashOuts: true,
66
+ useDataHookForPay: false,
67
+ useDataHookForCashOut: false,
68
+ dataHook: address(0),
69
+ metadata: 0
70
+ });
71
+ }
72
+
73
+ /// @dev Helper: launch a project with a 1-second duration and the given weight cut percent.
74
+ function _launchProject() internal returns (uint256 projectId) {
75
+ JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
76
+ configs[0].mustStartAtOrAfter = 0;
77
+ configs[0].duration = _DURATION;
78
+ configs[0].weight = _INITIAL_WEIGHT;
79
+ configs[0].weightCutPercent = _WEIGHT_CUT_PERCENT;
80
+ configs[0].approvalHook = IJBRulesetApprovalHook(address(0));
81
+ configs[0].metadata = _metadata;
82
+ configs[0].splitGroups = new JBSplitGroup[](0);
83
+ configs[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
84
+
85
+ projectId = _controller.launchProjectFor({
86
+ owner: _projectOwner,
87
+ projectUri: "weightCacheBoundary",
88
+ rulesetConfigurations: configs,
89
+ terminalConfigurations: new JBTerminalConfig[](0),
90
+ memo: ""
91
+ });
92
+ }
93
+
94
+ /// @dev Compute expected weight by iterating the cut percent `n` times on the initial weight.
95
+ function _expectedWeight(uint256 n) internal pure returns (uint256 weight) {
96
+ weight = _INITIAL_WEIGHT;
97
+ for (uint256 i; i < n; i++) {
98
+ weight = mulDiv(
99
+ weight, JBConstants.MAX_WEIGHT_CUT_PERCENT - _WEIGHT_CUT_PERCENT, JBConstants.MAX_WEIGHT_CUT_PERCENT
100
+ );
101
+ if (weight == 0) break;
102
+ }
103
+ }
104
+
105
+ // ═══════════════════════════════════════════════════════════════════
106
+ // Test 1: Exactly 20,000 cycles works without cache
107
+ // ═══════════════════════════════════════════════════════════════════
108
+
109
+ function test_exactlyAtThreshold_noCache_succeeds() public {
110
+ uint256 projectId = _launchProject();
111
+
112
+ // Warp forward exactly 20,000 durations.
113
+ vm.warp(block.timestamp + _DURATION * _THRESHOLD);
114
+
115
+ // currentOf should succeed — still within the iteration limit.
116
+ JBRuleset memory ruleset = _rulesets.currentOf(projectId);
117
+
118
+ // Verify weight matches the iterative calculation.
119
+ uint256 expected = _expectedWeight(_THRESHOLD);
120
+ assertEq(ruleset.weight, expected, "Weight at exactly 20,000 cycles should match iterative calculation");
121
+ assertTrue(ruleset.weight > 0, "Weight should not have decayed to zero at 20k cycles with 0.1% cut");
122
+ }
123
+
124
+ // ═══════════════════════════════════════════════════════════════════
125
+ // Test 2: 20,001 cycles reverts without cache
126
+ // ═══════════════════════════════════════════════════════════════════
127
+
128
+ function test_aboveThreshold_noCache_reverts() public {
129
+ uint256 projectId = _launchProject();
130
+
131
+ // Warp forward 20,001 durations.
132
+ vm.warp(block.timestamp + _DURATION * (_THRESHOLD + 1));
133
+
134
+ // currentOf should revert because the iteration count exceeds the threshold.
135
+ vm.expectRevert(abi.encodeWithSelector(JBRulesets.JBRulesets_WeightCacheRequired.selector, projectId));
136
+ _rulesets.currentOf(projectId);
137
+ }
138
+
139
+ // ═══════════════════════════════════════════════════════════════════
140
+ // Test 3: Single cache call unblocks 20,001 cycles
141
+ // ═══════════════════════════════════════════════════════════════════
142
+
143
+ function test_aboveThreshold_singleCacheCall_succeeds() public {
144
+ uint256 projectId = _launchProject();
145
+ uint256 rulesetId = _rulesets.latestRulesetIdOf(projectId);
146
+
147
+ // Warp forward 20,001 durations.
148
+ vm.warp(block.timestamp + _DURATION * (_THRESHOLD + 1));
149
+
150
+ // Without cache, this reverts.
151
+ vm.expectRevert(abi.encodeWithSelector(JBRulesets.JBRulesets_WeightCacheRequired.selector, projectId));
152
+ _rulesets.currentOf(projectId);
153
+
154
+ // Populate the cache.
155
+ _rulesets.updateRulesetWeightCache(projectId, rulesetId);
156
+
157
+ // Now currentOf should succeed.
158
+ JBRuleset memory ruleset = _rulesets.currentOf(projectId);
159
+
160
+ // Verify weight matches iterative calculation.
161
+ uint256 expected = _expectedWeight(_THRESHOLD + 1);
162
+ assertEq(ruleset.weight, expected, "Weight at 20,001 cycles should match after single cache call");
163
+ }
164
+
165
+ // ═══════════════════════════════════════════════════════════════════
166
+ // Test 4: Progressive caching for large gaps (40,001 cycles)
167
+ // ═══════════════════════════════════════════════════════════════════
168
+
169
+ function test_largeGap_progressiveCaching_succeeds() public {
170
+ uint256 projectId = _launchProject();
171
+ uint256 rulesetId = _rulesets.latestRulesetIdOf(projectId);
172
+
173
+ // Warp forward 40,001 durations (needs 2 cache calls).
174
+ vm.warp(block.timestamp + _DURATION * (_THRESHOLD * 2 + 1));
175
+
176
+ // Without cache, reverts.
177
+ vm.expectRevert(abi.encodeWithSelector(JBRulesets.JBRulesets_WeightCacheRequired.selector, projectId));
178
+ _rulesets.currentOf(projectId);
179
+
180
+ // First cache call: advances cache by up to 20,000 iterations.
181
+ _rulesets.updateRulesetWeightCache(projectId, rulesetId);
182
+
183
+ // Still reverts — we need another cache call to cover the remaining gap.
184
+ vm.expectRevert(abi.encodeWithSelector(JBRulesets.JBRulesets_WeightCacheRequired.selector, projectId));
185
+ _rulesets.currentOf(projectId);
186
+
187
+ // Second cache call: covers the rest.
188
+ _rulesets.updateRulesetWeightCache(projectId, rulesetId);
189
+
190
+ // Now it should succeed.
191
+ JBRuleset memory ruleset = _rulesets.currentOf(projectId);
192
+
193
+ // Verify weight matches iterative calculation.
194
+ uint256 expected = _expectedWeight(_THRESHOLD * 2 + 1);
195
+ assertEq(ruleset.weight, expected, "Weight at 40,001 cycles should match after two cache calls");
196
+ }
197
+
198
+ // ═══════════════════════════════════════════════════════════════════
199
+ // Test 5: Cache produces same weight as direct iteration
200
+ // ═══════════════════════════════════════════════════════════════════
201
+
202
+ function test_cachedWeight_matchesDirect_atBoundary() public {
203
+ // Launch two identical projects.
204
+ uint256 projectA = _launchProject();
205
+ uint256 projectB = _launchProject();
206
+
207
+ uint256 rulesetIdB = _rulesets.latestRulesetIdOf(projectB);
208
+
209
+ // Warp to exactly 20,000 cycles (within threshold for both).
210
+ vm.warp(block.timestamp + _DURATION * _THRESHOLD);
211
+
212
+ // Project A: no cache, direct iteration.
213
+ JBRuleset memory rulesetA = _rulesets.currentOf(projectA);
214
+
215
+ // Project B: populate cache first, then query.
216
+ _rulesets.updateRulesetWeightCache(projectB, rulesetIdB);
217
+ JBRuleset memory rulesetB = _rulesets.currentOf(projectB);
218
+
219
+ // Both should produce identical weights.
220
+ assertEq(rulesetA.weight, rulesetB.weight, "Cached and uncached weights should be identical at threshold");
221
+ }
222
+
223
+ // ═══════════════════════════════════════════════════════════════════
224
+ // Test 6: Zero weightCutPercent — cache is no-op
225
+ // ═══════════════════════════════════════════════════════════════════
226
+
227
+ function test_zeroWeightCut_cacheNoOp() public {
228
+ // Launch a project with zero weight cut.
229
+ JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
230
+ configs[0].mustStartAtOrAfter = 0;
231
+ configs[0].duration = _DURATION;
232
+ configs[0].weight = _INITIAL_WEIGHT;
233
+ configs[0].weightCutPercent = 0; // No decay
234
+ configs[0].approvalHook = IJBRulesetApprovalHook(address(0));
235
+ configs[0].metadata = _metadata;
236
+ configs[0].splitGroups = new JBSplitGroup[](0);
237
+ configs[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
238
+
239
+ uint256 projectId = _controller.launchProjectFor({
240
+ owner: _projectOwner,
241
+ projectUri: "zeroCut",
242
+ rulesetConfigurations: configs,
243
+ terminalConfigurations: new JBTerminalConfig[](0),
244
+ memo: ""
245
+ });
246
+
247
+ // Warp far beyond the threshold — since weightCutPercent = 0, no iterations needed.
248
+ vm.warp(block.timestamp + _DURATION * (_THRESHOLD * 5));
249
+
250
+ // Should succeed without any cache because the weight cut loop is skipped.
251
+ JBRuleset memory ruleset = _rulesets.currentOf(projectId);
252
+ assertEq(ruleset.weight, _INITIAL_WEIGHT, "Weight should remain unchanged with zero cut percent");
253
+ }
254
+
255
+ // ═══════════════════════════════════════════════════════════════════
256
+ // Test 7: Cache with zero duration — updateRulesetWeightCache is no-op
257
+ // ═══════════════════════════════════════════════════════════════════
258
+
259
+ function test_zeroDuration_cacheNoOp() public {
260
+ // Launch a project with zero duration.
261
+ JBRulesetConfig[] memory configs = new JBRulesetConfig[](1);
262
+ configs[0].mustStartAtOrAfter = 0;
263
+ configs[0].duration = 0; // Never expires
264
+ configs[0].weight = _INITIAL_WEIGHT;
265
+ configs[0].weightCutPercent = _WEIGHT_CUT_PERCENT;
266
+ configs[0].approvalHook = IJBRulesetApprovalHook(address(0));
267
+ configs[0].metadata = _metadata;
268
+ configs[0].splitGroups = new JBSplitGroup[](0);
269
+ configs[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
270
+
271
+ uint256 projectId = _controller.launchProjectFor({
272
+ owner: _projectOwner,
273
+ projectUri: "zeroDuration",
274
+ rulesetConfigurations: configs,
275
+ terminalConfigurations: new JBTerminalConfig[](0),
276
+ memo: ""
277
+ });
278
+
279
+ uint256 rulesetId = _rulesets.latestRulesetIdOf(projectId);
280
+
281
+ // Warp far ahead.
282
+ vm.warp(block.timestamp + 1_000_000);
283
+
284
+ // updateRulesetWeightCache should be a no-op (returns without caching).
285
+ _rulesets.updateRulesetWeightCache(projectId, rulesetId);
286
+
287
+ // currentOf should succeed — zero duration means the same ruleset is always current.
288
+ JBRuleset memory ruleset = _rulesets.currentOf(projectId);
289
+ assertEq(ruleset.weight, _INITIAL_WEIGHT, "Weight should remain unchanged with zero duration");
290
+ }
291
+ }
@@ -85,8 +85,8 @@ contract TestAddToBalanceOf_Local is JBMultiTerminalSetup {
85
85
  JBAccountingContext memory _storedContext = _terminal.accountingContextForTokenOf(_projectId, _native);
86
86
  assertEq(_storedContext.token, _native);
87
87
 
88
- // Find the storage slot for fees array
89
- bytes32 feeSlot = keccak256(abi.encode(_projectId, uint256(2)));
88
+ // Find the storage slot for fees array (_heldFeesOf is at storage slot 3)
89
+ bytes32 feeSlot = keccak256(abi.encode(_projectId, uint256(3)));
90
90
  bytes32 slotForArrayLength = keccak256(abi.encode(_native, feeSlot));
91
91
 
92
92
  // Set the length of the fees array in the storage slot
@@ -106,12 +106,15 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
106
106
  metadata: 0
107
107
  });
108
108
 
109
+ // mock feeless address check
110
+ mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
111
+
109
112
  // mock call to JBTerminalStore recordCashOutFor
110
113
  mockExpect(
111
114
  address(store),
112
115
  abi.encodeCall(
113
116
  IJBTerminalStore.recordCashOutFor,
114
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, "")
117
+ (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
115
118
  ),
116
119
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
117
120
  );
@@ -129,9 +132,6 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
129
132
  // put code at mockToken address to pass OZ Address check
130
133
  vm.etch(_mockToken, abi.encode(1));
131
134
 
132
- // mock feeless address check
133
- mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
134
-
135
135
  _terminal.cashOutTokensOf(_holder, _projectId, _defaultAmount, _mockToken, _minReclaimed, _bene, "");
136
136
  }
137
137
 
@@ -154,12 +154,15 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
154
154
  metadata: 0
155
155
  });
156
156
 
157
+ // mock feeless address check
158
+ mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
159
+
157
160
  // mock call to JBTerminalStore recordCashOutFor
158
161
  mockExpect(
159
162
  address(store),
160
163
  abi.encodeCall(
161
164
  IJBTerminalStore.recordCashOutFor,
162
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, "")
165
+ (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
163
166
  ),
164
167
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
165
168
  );
@@ -177,8 +180,6 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
177
180
  // put code at mockToken address to pass OZ Address check
178
181
  vm.etch(_mockToken, abi.encode(1));
179
182
 
180
- // mock feeless address check
181
- mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
182
183
  vm.expectRevert(
183
184
  abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_UnderMinTokensReclaimed.selector, 1e9, 1e18)
184
185
  );
@@ -208,12 +209,15 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
208
209
  metadata: 0
209
210
  });
210
211
 
212
+ // mock feeless address check
213
+ mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(false));
214
+
211
215
  // mock call to JBTerminalStore recordCashOutFor
212
216
  mockExpect(
213
217
  address(store),
214
218
  abi.encodeCall(
215
219
  IJBTerminalStore.recordCashOutFor,
216
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, "")
220
+ (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, false, "")
217
221
  ),
218
222
  abi.encode(returnedRuleset, reclaimAmount, _halfCashOutTaxRate, hookSpecifications)
219
223
  );
@@ -231,9 +235,6 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
231
235
  // put code at mockToken address to pass OZ Address check
232
236
  vm.etch(_mockToken, abi.encode(1));
233
237
 
234
- // mock feeless address check
235
- mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(false));
236
-
237
238
  // get fee amount
238
239
  uint256 tax = JBFees.feeAmountFrom(reclaimAmount, 25); // 25 = default fee)
239
240
  uint256 transferredAmount = reclaimAmount - tax;
@@ -320,12 +321,14 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
320
321
  metadata: 0
321
322
  });
322
323
 
324
+ mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
325
+
323
326
  // mock call to JBTerminalStore recordCashOutFor
324
327
  mockExpect(
325
328
  address(store),
326
329
  abi.encodeCall(
327
330
  IJBTerminalStore.recordCashOutFor,
328
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, "")
331
+ (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
329
332
  ),
330
333
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
331
334
  );
@@ -340,8 +343,6 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
340
343
  address(this), abi.encodeCall(IJBController.burnTokensOf, (_holder, _projectId, _defaultAmount, "")), ""
341
344
  );
342
345
 
343
- mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
344
-
345
346
  mockExpect(
346
347
  address(feelessAddresses),
347
348
  abi.encodeCall(IJBFeelessAddresses.isFeeless, (address(_mockHook))),
@@ -423,12 +424,14 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
423
424
  metadata: 0
424
425
  });
425
426
 
427
+ mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
428
+
426
429
  // mock call to JBTerminalStore recordCashOutFor
427
430
  mockExpect(
428
431
  address(store),
429
432
  abi.encodeCall(
430
433
  IJBTerminalStore.recordCashOutFor,
431
- (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, "")
434
+ (_holder, _projectId, _defaultAmount, mockTokenContext, mockBalanceContext, true, "")
432
435
  ),
433
436
  abi.encode(returnedRuleset, reclaimAmount, _maxCashOutTaxRate, hookSpecifications)
434
437
  );
@@ -443,8 +446,6 @@ contract TestCashOutTokensOf_Local is JBMultiTerminalSetup {
443
446
  address(this), abi.encodeCall(IJBController.burnTokensOf, (_holder, _projectId, _defaultAmount, "")), ""
444
447
  );
445
448
 
446
- mockExpect(address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (_bene)), abi.encode(true));
447
-
448
449
  mockExpect(
449
450
  address(feelessAddresses),
450
451
  abi.encodeCall(IJBFeelessAddresses.isFeeless, (address(_mockHook))),
@@ -250,10 +250,12 @@ contract TestPay_Local is JBMultiTerminalSetup {
250
250
  _mockToken.approve(address(_mockHook), _defaultAmount);
251
251
 
252
252
  // needed for next mock call returns
253
- JBTokenAmount memory tokenAmount =
254
- // forge-lint: disable-next-line(unsafe-typecast)
255
- JBTokenAmount({
256
- token: address(_mockToken), decimals: 6, currency: uint32(_mockTokenCurrency), value: _defaultAmount
253
+ JBTokenAmount memory tokenAmount = JBTokenAmount({
254
+ // forge-lint: disable-next-line(unsafe-typecast)
255
+ token: address(_mockToken),
256
+ decimals: 6,
257
+ currency: uint32(_mockTokenCurrency),
258
+ value: _defaultAmount
257
259
  });
258
260
  JBPayHookSpecification[] memory hookSpecifications = new JBPayHookSpecification[](1);
259
261
  hookSpecifications[0] = JBPayHookSpecification({hook: _mockHook, amount: _defaultAmount, metadata: ""});