@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,475 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {JBTerminalStore} from "../../../../src/JBTerminalStore.sol";
5
+ import {IJBCashOutHook} from "../../../../src/interfaces/IJBCashOutHook.sol";
6
+ import {IJBController} from "../../../../src/interfaces/IJBController.sol";
7
+ import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
8
+ import {IJBFundAccessLimits} from "../../../../src/interfaces/IJBFundAccessLimits.sol";
9
+ import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
10
+ import {IJBRulesetDataHook} from "../../../../src/interfaces/IJBRulesetDataHook.sol";
11
+ import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
12
+ import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
13
+ import {IJBToken} from "../../../../src/interfaces/IJBToken.sol";
14
+ import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
15
+ import {JBRulesetMetadataResolver} from "../../../../src/libraries/JBRulesetMetadataResolver.sol";
16
+ import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
17
+ import {JBBeforeCashOutRecordedContext} from "../../../../src/structs/JBBeforeCashOutRecordedContext.sol";
18
+ import {JBCashOutHookSpecification} from "../../../../src/structs/JBCashOutHookSpecification.sol";
19
+ import {JBCurrencyAmount} from "../../../../src/structs/JBCurrencyAmount.sol";
20
+ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
21
+ import {JBRulesetMetadata} from "../../../../src/structs/JBRulesetMetadata.sol";
22
+ import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
23
+ import {mulDiv} from "@prb/math/src/Common.sol";
24
+ import {JBTerminalStoreSetup} from "./JBTerminalStoreSetup.sol";
25
+
26
+ contract TestPreviewCashOutFor_Local is JBTerminalStoreSetup {
27
+ uint64 _projectId = 1;
28
+ uint256 _decimals = 18;
29
+ uint256 _balance = 10e18;
30
+ uint256 _totalSupply = 20e18;
31
+
32
+ // Mocks
33
+ IJBTerminal _terminal1 = IJBTerminal(makeAddr("terminal1"));
34
+ IJBTerminal _terminal2 = IJBTerminal(makeAddr("terminal2"));
35
+ IJBToken _token = IJBToken(makeAddr("token"));
36
+ IJBController _controller = IJBController(makeAddr("controller"));
37
+ IJBFundAccessLimits _accessLimits = IJBFundAccessLimits(makeAddr("funds"));
38
+ IJBRulesetDataHook _dataHook = IJBRulesetDataHook(makeAddr("dataHook"));
39
+ IJBCashOutHook _cashOutHook = IJBCashOutHook(makeAddr("cashOutHook"));
40
+
41
+ uint32 _currency = uint32(uint160(address(_token)));
42
+
43
+ function setUp() public {
44
+ super.terminalStoreSetup();
45
+ }
46
+
47
+ function _setBalance(address terminal, uint256 balance) internal {
48
+ bytes32 balanceOfSlot = keccak256(abi.encode(terminal, uint256(0)));
49
+ bytes32 projectSlot = keccak256(abi.encode(_projectId, uint256(balanceOfSlot)));
50
+ bytes32 slot = keccak256(abi.encode(address(_token), uint256(projectSlot)));
51
+ vm.store(address(_store), slot, bytes32(balance));
52
+ }
53
+
54
+ function _mockUseTotalSurplus() internal {
55
+ IJBTerminal[] memory _terminals = new IJBTerminal[](2);
56
+ _terminals[0] = _terminal1;
57
+ _terminals[1] = _terminal2;
58
+
59
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
60
+
61
+ mockExpect(
62
+ address(_terminal1),
63
+ abi.encodeCall(
64
+ IJBTerminal.currentSurplusOf, (_projectId, new JBAccountingContext[](0), _decimals, _currency)
65
+ ),
66
+ abi.encode(1e18)
67
+ );
68
+ mockExpect(
69
+ address(_terminal2),
70
+ abi.encodeCall(
71
+ IJBTerminal.currentSurplusOf, (_projectId, new JBAccountingContext[](0), _decimals, _currency)
72
+ ),
73
+ abi.encode(2e18)
74
+ );
75
+ }
76
+
77
+ function test_MatchesRecordCashOutFor() external {
78
+ // Setup: set balance for address(this) (acting as terminal for both preview and record)
79
+ _setBalance(address(this), _balance);
80
+
81
+ _mockUseTotalSurplus();
82
+
83
+ JBRulesetMetadata memory _metadata = JBRulesetMetadata({
84
+ reservedPercent: 0,
85
+ cashOutTaxRate: 0,
86
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
87
+ pausePay: false,
88
+ pauseCreditTransfers: false,
89
+ allowOwnerMinting: false,
90
+ allowSetCustomToken: false,
91
+ allowTerminalMigration: false,
92
+ allowSetTerminals: false,
93
+ ownerMustSendPayouts: false,
94
+ allowSetController: false,
95
+ allowAddAccountingContext: true,
96
+ allowAddPriceFeed: false,
97
+ holdFees: false,
98
+ useTotalSurplusForCashOuts: true,
99
+ useDataHookForPay: false,
100
+ useDataHookForCashOut: false,
101
+ dataHook: address(0),
102
+ metadata: 0
103
+ });
104
+
105
+ uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
106
+
107
+ JBRuleset memory _returnedRuleset = JBRuleset({
108
+ cycleNumber: uint48(block.timestamp),
109
+ id: uint48(block.timestamp),
110
+ basedOnId: 0,
111
+ start: uint48(block.timestamp),
112
+ duration: uint32(block.timestamp + 1000),
113
+ weight: 1e18,
114
+ weightCutPercent: 0,
115
+ approvalHook: IJBRulesetApprovalHook(address(0)),
116
+ metadata: _packedMetadata
117
+ });
118
+
119
+ uint256 _cashOutCount = 10e18;
120
+
121
+ JBAccountingContext memory _accountingContext =
122
+ JBAccountingContext({token: address(_token), decimals: uint8(_decimals), currency: _currency});
123
+
124
+ JBAccountingContext[] memory _balanceContexts = new JBAccountingContext[](0);
125
+
126
+ // Mock for preview call
127
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
128
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
129
+ mockExpect(
130
+ address(_controller),
131
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
132
+ abi.encode(_totalSupply)
133
+ );
134
+
135
+ (, uint256 previewReclaimAmount, uint256 previewTaxRate, JBCashOutHookSpecification[] memory previewSpecs) = _store.previewCashOutFrom({
136
+ holder: address(this),
137
+ projectId: _projectId,
138
+ cashOutCount: _cashOutCount,
139
+ accountingContext: _accountingContext,
140
+ balanceAccountingContexts: _balanceContexts,
141
+ beneficiaryIsFeeless: false,
142
+ metadata: ""
143
+ });
144
+
145
+ // Re-mock for record call (same mocks, fresh expectations)
146
+ _mockUseTotalSurplus();
147
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
148
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
149
+ mockExpect(
150
+ address(_controller),
151
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
152
+ abi.encode(_totalSupply)
153
+ );
154
+
155
+ (, uint256 recordReclaimAmount, uint256 recordTaxRate, JBCashOutHookSpecification[] memory recordSpecs) = _store.recordCashOutFor({
156
+ holder: address(this),
157
+ projectId: _projectId,
158
+ cashOutCount: _cashOutCount,
159
+ accountingContext: _accountingContext,
160
+ balanceAccountingContexts: _balanceContexts,
161
+ beneficiaryIsFeeless: false,
162
+ metadata: ""
163
+ });
164
+
165
+ assertEq(previewReclaimAmount, recordReclaimAmount);
166
+ assertEq(previewTaxRate, recordTaxRate);
167
+ assertEq(previewSpecs.length, recordSpecs.length);
168
+ }
169
+
170
+ function test_DoesNotModifyState() external {
171
+ _setBalance(address(this), _balance);
172
+ _mockUseTotalSurplus();
173
+
174
+ JBRulesetMetadata memory _metadata = JBRulesetMetadata({
175
+ reservedPercent: 0,
176
+ cashOutTaxRate: 0,
177
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
178
+ pausePay: false,
179
+ pauseCreditTransfers: false,
180
+ allowOwnerMinting: false,
181
+ allowSetCustomToken: false,
182
+ allowTerminalMigration: false,
183
+ allowSetTerminals: false,
184
+ ownerMustSendPayouts: false,
185
+ allowSetController: false,
186
+ allowAddAccountingContext: true,
187
+ allowAddPriceFeed: false,
188
+ holdFees: false,
189
+ useTotalSurplusForCashOuts: true,
190
+ useDataHookForPay: false,
191
+ useDataHookForCashOut: false,
192
+ dataHook: address(0),
193
+ metadata: 0
194
+ });
195
+
196
+ uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
197
+
198
+ JBRuleset memory _returnedRuleset = JBRuleset({
199
+ cycleNumber: uint48(block.timestamp),
200
+ id: uint48(block.timestamp),
201
+ basedOnId: 0,
202
+ start: uint48(block.timestamp),
203
+ duration: uint32(block.timestamp + 1000),
204
+ weight: 1e18,
205
+ weightCutPercent: 0,
206
+ approvalHook: IJBRulesetApprovalHook(address(0)),
207
+ metadata: _packedMetadata
208
+ });
209
+
210
+ JBAccountingContext memory _accountingContext =
211
+ JBAccountingContext({token: address(_token), decimals: uint8(_decimals), currency: _currency});
212
+
213
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
214
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
215
+ mockExpect(
216
+ address(_controller),
217
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
218
+ abi.encode(_totalSupply)
219
+ );
220
+
221
+ uint256 balanceBefore = _store.balanceOf(address(this), _projectId, address(_token));
222
+
223
+ _store.previewCashOutFrom({
224
+ holder: address(this),
225
+ projectId: _projectId,
226
+ cashOutCount: 5e18,
227
+ accountingContext: _accountingContext,
228
+ balanceAccountingContexts: new JBAccountingContext[](0),
229
+ beneficiaryIsFeeless: false,
230
+ metadata: ""
231
+ });
232
+
233
+ uint256 balanceAfter = _store.balanceOf(address(this), _projectId, address(_token));
234
+ assertEq(balanceBefore, balanceAfter);
235
+ }
236
+
237
+ function test_RevertsWhenCashOutCountExceedsTotalSupply() external {
238
+ _setBalance(address(this), _balance);
239
+ _mockUseTotalSurplus();
240
+
241
+ JBRulesetMetadata memory _metadata = JBRulesetMetadata({
242
+ reservedPercent: 0,
243
+ cashOutTaxRate: 0,
244
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
245
+ pausePay: false,
246
+ pauseCreditTransfers: false,
247
+ allowOwnerMinting: false,
248
+ allowSetCustomToken: false,
249
+ allowTerminalMigration: false,
250
+ allowSetTerminals: false,
251
+ ownerMustSendPayouts: false,
252
+ allowSetController: false,
253
+ allowAddAccountingContext: true,
254
+ allowAddPriceFeed: false,
255
+ holdFees: false,
256
+ useTotalSurplusForCashOuts: true,
257
+ useDataHookForPay: false,
258
+ useDataHookForCashOut: false,
259
+ dataHook: address(0),
260
+ metadata: 0
261
+ });
262
+
263
+ uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
264
+
265
+ JBRuleset memory _returnedRuleset = JBRuleset({
266
+ cycleNumber: uint48(block.timestamp),
267
+ id: uint48(block.timestamp),
268
+ basedOnId: 0,
269
+ start: uint48(block.timestamp),
270
+ duration: uint32(block.timestamp + 1000),
271
+ weight: 1e18,
272
+ weightCutPercent: 0,
273
+ approvalHook: IJBRulesetApprovalHook(address(0)),
274
+ metadata: _packedMetadata
275
+ });
276
+
277
+ JBAccountingContext memory _accountingContext =
278
+ JBAccountingContext({token: address(_token), decimals: uint8(_decimals), currency: _currency});
279
+
280
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
281
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
282
+ mockExpect(
283
+ address(_controller),
284
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
285
+ abi.encode(_totalSupply)
286
+ );
287
+
288
+ uint256 _excessiveCashOutCount = _totalSupply + 1;
289
+
290
+ vm.expectRevert(
291
+ abi.encodeWithSelector(
292
+ JBTerminalStore.JBTerminalStore_InsufficientTokens.selector, _excessiveCashOutCount, _totalSupply
293
+ )
294
+ );
295
+ _store.previewCashOutFrom({
296
+ holder: address(this),
297
+ projectId: _projectId,
298
+ cashOutCount: _excessiveCashOutCount,
299
+ accountingContext: _accountingContext,
300
+ balanceAccountingContexts: new JBAccountingContext[](0),
301
+ beneficiaryIsFeeless: false,
302
+ metadata: ""
303
+ });
304
+ }
305
+
306
+ function test_ReturnsZeroWhenSurplusIsZero() external {
307
+ // No balance set, no surplus
308
+
309
+ JBRulesetMetadata memory _metadata = JBRulesetMetadata({
310
+ reservedPercent: 0,
311
+ cashOutTaxRate: 0,
312
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
313
+ pausePay: false,
314
+ pauseCreditTransfers: false,
315
+ allowOwnerMinting: false,
316
+ allowSetCustomToken: false,
317
+ allowTerminalMigration: false,
318
+ allowSetTerminals: false,
319
+ ownerMustSendPayouts: false,
320
+ allowSetController: false,
321
+ allowAddAccountingContext: true,
322
+ allowAddPriceFeed: false,
323
+ holdFees: false,
324
+ useTotalSurplusForCashOuts: true,
325
+ useDataHookForPay: false,
326
+ useDataHookForCashOut: false,
327
+ dataHook: address(0),
328
+ metadata: 0
329
+ });
330
+
331
+ uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
332
+
333
+ JBRuleset memory _returnedRuleset = JBRuleset({
334
+ cycleNumber: uint48(block.timestamp),
335
+ id: uint48(block.timestamp),
336
+ basedOnId: 0,
337
+ start: uint48(block.timestamp),
338
+ duration: uint32(block.timestamp + 1000),
339
+ weight: 1e18,
340
+ weightCutPercent: 0,
341
+ approvalHook: IJBRulesetApprovalHook(address(0)),
342
+ metadata: _packedMetadata
343
+ });
344
+
345
+ JBAccountingContext memory _accountingContext =
346
+ JBAccountingContext({token: address(_token), decimals: uint8(_decimals), currency: _currency});
347
+
348
+ IJBTerminal[] memory _terminals = new IJBTerminal[](1);
349
+ _terminals[0] = _terminal1;
350
+
351
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
352
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
353
+ mockExpect(
354
+ address(_terminal1),
355
+ abi.encodeCall(
356
+ IJBTerminal.currentSurplusOf, (_projectId, new JBAccountingContext[](0), _decimals, _currency)
357
+ ),
358
+ abi.encode(0)
359
+ );
360
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
361
+ mockExpect(
362
+ address(_controller),
363
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
364
+ abi.encode(_totalSupply)
365
+ );
366
+
367
+ (, uint256 reclaimAmount,,) = _store.previewCashOutFrom({
368
+ holder: address(this),
369
+ projectId: _projectId,
370
+ cashOutCount: 5e18,
371
+ accountingContext: _accountingContext,
372
+ balanceAccountingContexts: new JBAccountingContext[](0),
373
+ beneficiaryIsFeeless: false,
374
+ metadata: ""
375
+ });
376
+
377
+ assertEq(reclaimAmount, 0);
378
+ }
379
+
380
+ function test_WithDataHookAndZeroAmountNoopSpec() external {
381
+ _setBalance(address(this), _balance);
382
+ _mockUseTotalSurplus();
383
+
384
+ JBRulesetMetadata memory _metadata = JBRulesetMetadata({
385
+ reservedPercent: 0,
386
+ cashOutTaxRate: 0,
387
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
388
+ pausePay: false,
389
+ pauseCreditTransfers: false,
390
+ allowOwnerMinting: false,
391
+ allowSetCustomToken: false,
392
+ allowTerminalMigration: false,
393
+ allowSetTerminals: false,
394
+ ownerMustSendPayouts: false,
395
+ allowSetController: false,
396
+ allowAddAccountingContext: true,
397
+ allowAddPriceFeed: false,
398
+ holdFees: false,
399
+ useTotalSurplusForCashOuts: true,
400
+ useDataHookForPay: false,
401
+ useDataHookForCashOut: true,
402
+ dataHook: address(_dataHook),
403
+ metadata: 0
404
+ });
405
+
406
+ uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
407
+
408
+ JBRuleset memory _returnedRuleset = JBRuleset({
409
+ cycleNumber: uint48(block.timestamp),
410
+ id: uint48(block.timestamp),
411
+ basedOnId: 0,
412
+ start: uint48(block.timestamp),
413
+ duration: uint32(block.timestamp + 1000),
414
+ weight: 1e18,
415
+ weightCutPercent: 0,
416
+ approvalHook: IJBRulesetApprovalHook(address(0)),
417
+ metadata: _packedMetadata
418
+ });
419
+
420
+ JBAccountingContext memory _accountingContext =
421
+ JBAccountingContext({token: address(_token), decimals: uint8(_decimals), currency: _currency});
422
+
423
+ JBBeforeCashOutRecordedContext memory _context = JBBeforeCashOutRecordedContext({
424
+ terminal: address(this),
425
+ holder: address(this),
426
+ projectId: _projectId,
427
+ rulesetId: uint48(block.timestamp),
428
+ cashOutCount: 10e18,
429
+ totalSupply: _totalSupply,
430
+ surplus: JBTokenAmount({
431
+ token: _accountingContext.token,
432
+ value: 3e18,
433
+ decimals: _accountingContext.decimals,
434
+ currency: _accountingContext.currency
435
+ }),
436
+ useTotalSurplus: true,
437
+ cashOutTaxRate: 0,
438
+ beneficiaryIsFeeless: false,
439
+ metadata: ""
440
+ });
441
+
442
+ JBCashOutHookSpecification[] memory _spec = new JBCashOutHookSpecification[](1);
443
+ _spec[0] = JBCashOutHookSpecification({hook: _cashOutHook, noop: true, amount: 0, metadata: "info"});
444
+
445
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
446
+ mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
447
+ mockExpect(
448
+ address(_controller),
449
+ abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
450
+ abi.encode(_totalSupply)
451
+ );
452
+ mockExpect(
453
+ address(_dataHook),
454
+ abi.encodeCall(IJBRulesetDataHook.beforeCashOutRecordedWith, (_context)),
455
+ abi.encode(0, 10e18, _totalSupply, _spec)
456
+ );
457
+
458
+ (, uint256 reclaimAmount, uint256 cashOutTaxRate, JBCashOutHookSpecification[] memory hookSpecifications) = _store.previewCashOutFrom({
459
+ holder: address(this),
460
+ projectId: _projectId,
461
+ cashOutCount: 10e18,
462
+ accountingContext: _accountingContext,
463
+ balanceAccountingContexts: new JBAccountingContext[](0),
464
+ beneficiaryIsFeeless: false,
465
+ metadata: ""
466
+ });
467
+
468
+ assertEq(reclaimAmount, mulDiv(3e18, 10e18, _totalSupply));
469
+ assertEq(cashOutTaxRate, 0);
470
+ assertEq(hookSpecifications.length, 1);
471
+ assertEq(hookSpecifications[0].amount, 0);
472
+ assertEq(hookSpecifications[0].noop, true);
473
+ assertEq(hookSpecifications[0].metadata, bytes("info"));
474
+ }
475
+ }