@bananapus/core-v6 0.0.16 → 0.0.17

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 (43) 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 +375 -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
@@ -1,28 +1,216 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
4
+ import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
5
+ import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
6
+ import {IJBFeeTerminal} from "../../../../src/interfaces/IJBFeeTerminal.sol";
7
+ import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
8
+ import {IJBPermissions} from "../../../../src/interfaces/IJBPermissions.sol";
9
+ import {IJBProjects} from "../../../../src/interfaces/IJBProjects.sol";
10
+ import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
11
+ import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
12
+ import {IJBSplits} from "../../../../src/interfaces/IJBSplits.sol";
13
+ import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
14
+ import {IJBTokens} from "../../../../src/interfaces/IJBTokens.sol";
15
+ import {JBFees} from "../../../../src/libraries/JBFees.sol";
16
+ import {JBFee} from "../../../../src/structs/JBFee.sol";
17
+ import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
18
+ import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
19
+ import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
20
+ import {JBTest} from "../../../helpers/JBTest.sol";
5
21
 
6
- contract TestProcessHeldFeesOf_Local is JBMultiTerminalSetup {
7
- // covered extensively in other units but leaving the scaffolding for now
8
- /* function setUp() public {
9
- super.multiTerminalSetup();
10
- }
22
+ /// @dev Harness that exposes internal held fee storage for direct manipulation in tests.
23
+ contract ForTest_JBMultiTerminal is JBMultiTerminal {
24
+ constructor(
25
+ IJBFeelessAddresses feelessAddresses,
26
+ IJBPermissions permissions,
27
+ IJBProjects projects,
28
+ IJBSplits splits,
29
+ IJBTerminalStore store,
30
+ IJBTokens tokens,
31
+ IPermit2 permit2,
32
+ address trustedForwarder
33
+ )
34
+ JBMultiTerminal(feelessAddresses, permissions, projects, splits, store, tokens, permit2, trustedForwarder)
35
+ {}
11
36
 
12
- function test_WhenHeldFeeUnlockTimestampGTBlocktimestamp() external {
13
- // it will add the fee back to _heldFeesOf
14
- }
37
+ function forTestAddHeldFee(uint256 projectId, address token, JBFee memory fee) external {
38
+ _heldFeesOf[projectId][token].push(fee);
39
+ }
40
+
41
+ function forTestSetNextHeldFeeIndex(uint256 projectId, address token, uint256 index) external {
42
+ _nextHeldFeeIndexOf[projectId][token] = index;
43
+ }
44
+ }
45
+
46
+ contract TestProcessHeldFeesOf_Local is JBTest {
47
+ // Target Contract (harness)
48
+ ForTest_JBMultiTerminal public _terminal;
49
+
50
+ // Mocks
51
+ IJBPermissions public permissions = IJBPermissions(makeAddr("permissions"));
52
+ IJBProjects public projects = IJBProjects(makeAddr("projects"));
53
+ IJBDirectory public directory = IJBDirectory(makeAddr("directory"));
54
+ IJBRulesets public rulesets = IJBRulesets(makeAddr("rulesets"));
55
+ IJBTokens public tokens = IJBTokens(makeAddr("tokens"));
56
+ IJBSplits public splits = IJBSplits(makeAddr("splits"));
57
+ IJBTerminalStore public store = IJBTerminalStore(makeAddr("store"));
58
+ IJBFeelessAddresses public feelessAddresses = IJBFeelessAddresses(makeAddr("feeless"));
59
+ IPermit2 public permit2 = IPermit2(makeAddr("permit2"));
60
+ address trustedForwarder = makeAddr("forwarder");
61
+
62
+ uint256 _projectId = 2;
63
+ address _mockToken = makeAddr("token");
64
+ address _beneficiary = makeAddr("beneficiary");
65
+
66
+ function setUp() public {
67
+ // Constructor will call to find directory and rulesets from the terminal store
68
+ mockExpect(address(store), abi.encodeCall(IJBTerminalStore.DIRECTORY, ()), abi.encode(address(directory)));
69
+ mockExpect(address(store), abi.encodeCall(IJBTerminalStore.RULESETS, ()), abi.encode(address(rulesets)));
70
+
71
+ _terminal = new ForTest_JBMultiTerminal(
72
+ feelessAddresses, permissions, projects, splits, store, tokens, permit2, trustedForwarder
73
+ );
74
+ }
75
+
76
+ function test_WhenHeldFeeUnlockTimestampGTBlocktimestamp() external {
77
+ // it will not process the fee (fee remains held)
78
+
79
+ // Add a held fee with unlockTimestamp in the future
80
+ uint48 futureTimestamp = uint48(block.timestamp + 1000);
81
+ _terminal.forTestAddHeldFee(
82
+ _projectId, _mockToken, JBFee({amount: 100, beneficiary: _beneficiary, unlockTimestamp: futureTimestamp})
83
+ );
84
+
85
+ // Mock the directory call to find the fee terminal (project 1 is the fee beneficiary)
86
+ mockExpect(
87
+ address(directory),
88
+ abi.encodeCall(IJBDirectory.primaryTerminalOf, (1, _mockToken)),
89
+ abi.encode(address(_terminal))
90
+ );
91
+
92
+ // Call processHeldFeesOf - the fee should NOT be processed because it's still locked
93
+ _terminal.processHeldFeesOf(_projectId, _mockToken, 1);
94
+
95
+ // Verify the fee is still held
96
+ JBFee[] memory remaining = _terminal.heldFeesOf(_projectId, _mockToken, 10);
97
+ assertEq(remaining.length, 1, "fee should still be held");
98
+ assertEq(remaining[0].amount, 100, "fee amount should be unchanged");
99
+ assertEq(remaining[0].unlockTimestamp, futureTimestamp, "unlock timestamp should be unchanged");
100
+ }
101
+
102
+ modifier whenHeldFeeIsUnlocked() {
103
+ _;
104
+ }
105
+
106
+ function test_GivenExecuteProcessFeeSucceeds() external whenHeldFeeIsUnlocked {
107
+ // it will process the fee and emit ProcessFee
108
+
109
+ // Add a held fee that is already unlocked
110
+ uint48 pastTimestamp = uint48(block.timestamp - 1);
111
+ uint256 heldAmount = 1000;
112
+ _terminal.forTestAddHeldFee(
113
+ _projectId,
114
+ _mockToken,
115
+ JBFee({amount: heldAmount, beneficiary: _beneficiary, unlockTimestamp: pastTimestamp})
116
+ );
117
+
118
+ // The fee amount that will be calculated from the held amount
119
+ uint256 expectedFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: heldAmount, feePercent: _terminal.FEE()});
120
+
121
+ // Mock the directory call to find the fee terminal - return _terminal itself so it uses internal _pay
122
+ mockExpect(
123
+ address(directory),
124
+ abi.encodeCall(IJBDirectory.primaryTerminalOf, (1, _mockToken)),
125
+ abi.encode(address(_terminal))
126
+ );
127
+
128
+ // Mock executeProcessFee: when the terminal calls itself, it will call recordPaymentFrom on the store.
129
+ // Since executeProcessFee is external and calls pay on the feeTerminal (which is _terminal itself),
130
+ // 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
+ vm.mockCall(
134
+ address(store),
135
+ abi.encodeWithSelector(IJBTerminalStore.recordPaymentFrom.selector),
136
+ abi.encode(
137
+ JBRuleset({
138
+ cycleNumber: 1,
139
+ id: 1,
140
+ basedOnId: 0,
141
+ start: 0,
142
+ duration: 0,
143
+ weight: 0,
144
+ weightCutPercent: 0,
145
+ approvalHook: IJBRulesetApprovalHook(address(0)),
146
+ metadata: 0
147
+ }),
148
+ uint256(0),
149
+ new JBPayHookSpecification[](0)
150
+ )
151
+ );
152
+
153
+ // Expect ProcessFee event
154
+ vm.expectEmit();
155
+ emit IJBFeeTerminal.ProcessFee({
156
+ projectId: _projectId,
157
+ token: _mockToken,
158
+ amount: expectedFeeAmount,
159
+ wasHeld: true,
160
+ beneficiary: _beneficiary,
161
+ caller: address(this)
162
+ });
163
+
164
+ _terminal.processHeldFeesOf(_projectId, _mockToken, 1);
165
+
166
+ // Verify held fees are cleaned up
167
+ JBFee[] memory remaining = _terminal.heldFeesOf(_projectId, _mockToken, 10);
168
+ assertEq(remaining.length, 0, "held fees should be empty after processing");
169
+ }
170
+
171
+ function test_GivenExecuteProcessFeeFails() external whenHeldFeeIsUnlocked {
172
+ // it will readd balance and emit FeeReverted
173
+
174
+ // Add a held fee that is already unlocked
175
+ uint48 pastTimestamp = uint48(block.timestamp - 1);
176
+ uint256 heldAmount = 1000;
177
+ _terminal.forTestAddHeldFee(
178
+ _projectId,
179
+ _mockToken,
180
+ JBFee({amount: heldAmount, beneficiary: _beneficiary, unlockTimestamp: pastTimestamp})
181
+ );
182
+
183
+ // The fee amount that will be calculated from the held amount
184
+ uint256 expectedFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: heldAmount, feePercent: _terminal.FEE()});
185
+
186
+ // Mock the directory call to find the fee terminal - return address(0) which will cause
187
+ // executeProcessFee to revert with FeeTerminalNotFound
188
+ mockExpect(
189
+ address(directory), abi.encodeCall(IJBDirectory.primaryTerminalOf, (1, _mockToken)), abi.encode(address(0))
190
+ );
15
191
 
16
- modifier whenHeldFeeIsUnlocked() {
17
- _;
18
- }
192
+ // Mock the recordAddedBalanceFor call that happens on fee revert (balance returned to project)
193
+ mockExpect(
194
+ address(store),
195
+ abi.encodeCall(IJBTerminalStore.recordAddedBalanceFor, (_projectId, _mockToken, expectedFeeAmount)),
196
+ abi.encode()
197
+ );
19
198
 
20
- function test_GivenExecuteProcessFeeSucceeds() external whenHeldFeeIsUnlocked {
21
- // it will process the fee and emit ProcessFee
22
- }
199
+ // Expect FeeReverted event
200
+ vm.expectEmit(true, true, true, false);
201
+ emit IJBFeeTerminal.FeeReverted({
202
+ projectId: _projectId,
203
+ token: _mockToken,
204
+ feeProjectId: 1,
205
+ amount: expectedFeeAmount,
206
+ reason: "",
207
+ caller: address(this)
208
+ });
23
209
 
24
- function test_GivenExecuteProcessFeeFails() external whenHeldFeeIsUnlocked {
25
- // it will readd balance and emit FeeReverted
26
- } */
210
+ _terminal.processHeldFeesOf(_projectId, _mockToken, 1);
27
211
 
212
+ // Verify held fees are cleaned up (entry was deleted even though fee processing failed)
213
+ JBFee[] memory remaining = _terminal.heldFeesOf(_projectId, _mockToken, 10);
214
+ assertEq(remaining.length, 0, "held fees should be empty after failed processing");
28
215
  }
216
+ }
@@ -4,6 +4,7 @@ pragma solidity 0.8.26;
4
4
  import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
5
5
  import {IJBController} from "../../../../src/interfaces/IJBController.sol";
6
6
  import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
7
+ import {IJBFeeTerminal} from "../../../../src/interfaces/IJBFeeTerminal.sol";
7
8
  import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
8
9
  import {IJBPayoutTerminal} from "../../../../src/interfaces/IJBPayoutTerminal.sol";
9
10
  import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
@@ -12,6 +13,9 @@ import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol"
12
13
  import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
13
14
  import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
14
15
  import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
16
+ import {JBRulesetMetadata} from "../../../../src/structs/JBRulesetMetadata.sol";
17
+ import {JBRulesetMetadataResolver} from "../../../../src/libraries/JBRulesetMetadataResolver.sol";
18
+ import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
15
19
  import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
16
20
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
17
21
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
@@ -19,6 +23,8 @@ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
19
23
  import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
20
24
 
21
25
  contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
26
+ using JBRulesetMetadataResolver for JBRulesetMetadata;
27
+
22
28
  uint256 _projectId = 1;
23
29
 
24
30
  function setUp() public {
@@ -310,10 +316,284 @@ contract TestUseAllowanceOf_Local is JBMultiTerminalSetup {
310
316
 
311
317
  function test_GivenRulesetHoldFeesEQTrue() external whenMsgSenderDNEQFeeless {
312
318
  // it will hold fees and emit HoldFee
319
+ address mockToken = makeAddr("token");
320
+ address beneficiary = makeAddr("bene");
321
+
322
+ // Mock controller (needed for addAccountingContextsFor permission check)
323
+ address controller = makeAddr("controller");
324
+
325
+ // forge-lint: disable-next-line(unsafe-typecast)
326
+ uint32 currencyId = uint32(uint160(mockToken));
327
+
328
+ // Mock controllerOf for addAccountingContextsFor permission check
329
+ mockExpect(
330
+ address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(controller))
331
+ );
332
+
333
+ // mock owner call
334
+ mockExpect(address(projects), abi.encodeCall(IERC721.ownerOf, (_projectId)), abi.encode(address(this)));
335
+
336
+ // Build a ruleset with holdFees=true via packed metadata
337
+ JBRulesetMetadata memory _rulesMetadata = JBRulesetMetadata({
338
+ reservedPercent: 0,
339
+ cashOutTaxRate: 0,
340
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
341
+ pausePay: false,
342
+ pauseCreditTransfers: false,
343
+ allowOwnerMinting: false,
344
+ allowSetCustomToken: false,
345
+ allowTerminalMigration: false,
346
+ allowSetTerminals: false,
347
+ ownerMustSendPayouts: false,
348
+ allowSetController: false,
349
+ allowAddAccountingContext: true,
350
+ allowAddPriceFeed: false,
351
+ holdFees: true,
352
+ useTotalSurplusForCashOuts: false,
353
+ useDataHookForPay: false,
354
+ useDataHookForCashOut: false,
355
+ dataHook: address(0),
356
+ metadata: 0
357
+ });
358
+
359
+ uint256 packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_rulesMetadata);
360
+
361
+ JBRuleset memory returnedRuleset = JBRuleset({
362
+ cycleNumber: 1,
363
+ id: 1,
364
+ basedOnId: 0,
365
+ start: 0,
366
+ duration: 0,
367
+ weight: 0,
368
+ weightCutPercent: 0,
369
+ approvalHook: IJBRulesetApprovalHook(address(0)),
370
+ metadata: packedMetadata
371
+ });
372
+
373
+ // mock call to tokens decimals()
374
+ mockExpect(mockToken, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(18));
375
+
376
+ // mock call to rulesets currentOf
377
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
378
+
379
+ // Set up accounting context so the token is recognized
380
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
381
+ _tokens[0] = JBAccountingContext({token: mockToken, decimals: 18, currency: currencyId});
382
+
383
+ _terminal.addAccountingContextsFor(_projectId, _tokens);
384
+
385
+ // recordUsedAllowance
386
+ mockExpect(
387
+ address(store),
388
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0], 100, 0)),
389
+ abi.encode(returnedRuleset, 100)
390
+ );
391
+
392
+ // first feeless check: owner is NOT feeless
393
+ mockExpect(
394
+ address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (address(this))), abi.encode(false)
395
+ );
396
+
397
+ // second feeless check: beneficiary is NOT feeless
398
+ mockExpect(
399
+ address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (beneficiary)), abi.encode(false)
400
+ );
401
+
402
+ // Fee is 2.5% of 100 = 2, so net = 98
403
+ mockExpect(mockToken, abi.encodeCall(IERC20.transfer, (beneficiary, 98)), abi.encode(true));
404
+
405
+ // Expect HoldFee event (fee is held, not processed)
406
+ vm.expectEmit(true, true, true, true);
407
+ emit IJBFeeTerminal.HoldFee({
408
+ projectId: _projectId,
409
+ token: mockToken,
410
+ amount: 100,
411
+ fee: 25,
412
+ beneficiary: address(this),
413
+ caller: address(this)
414
+ });
415
+
416
+ // Expect UseAllowance event
417
+ vm.expectEmit();
418
+ emit IJBPayoutTerminal.UseAllowance({
419
+ rulesetId: returnedRuleset.id,
420
+ rulesetCycleNumber: returnedRuleset.cycleNumber,
421
+ projectId: _projectId,
422
+ beneficiary: beneficiary,
423
+ feeBeneficiary: address(this),
424
+ amount: 100,
425
+ amountPaidOut: 100,
426
+ netAmountPaidOut: 98,
427
+ memo: "",
428
+ caller: address(this)
429
+ });
430
+
431
+ _terminal.useAllowanceOf({
432
+ projectId: _projectId,
433
+ token: mockToken,
434
+ amount: 100,
435
+ currency: 0,
436
+ minTokensPaidOut: 97,
437
+ beneficiary: payable(beneficiary),
438
+ feeBeneficiary: payable(address(this)),
439
+ memo: ""
440
+ });
313
441
  }
314
442
 
315
443
  function test_GivenRulesetHoldFeesDNEQTrue() external whenMsgSenderDNEQFeeless {
316
444
  // it will not hold fees and emit ProcessFee
445
+ address mockToken = makeAddr("token2");
446
+ address beneficiary = makeAddr("bene2");
447
+
448
+ // Mock controller for mint call on fee payments
449
+ address controller = makeAddr("controller2");
450
+
451
+ // Weight for a fee calculation that would take place in terminal store
452
+ uint112 weight = 1000 * 10 ** 18;
453
+
454
+ // forge-lint: disable-next-line(unsafe-typecast)
455
+ uint32 currencyId = uint32(uint160(mockToken));
456
+
457
+ // Build a ruleset with holdFees=false explicitly via packed metadata
458
+ JBRulesetMetadata memory _rulesMetadata = JBRulesetMetadata({
459
+ reservedPercent: 0,
460
+ cashOutTaxRate: 0,
461
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
462
+ pausePay: false,
463
+ pauseCreditTransfers: false,
464
+ allowOwnerMinting: false,
465
+ allowSetCustomToken: false,
466
+ allowTerminalMigration: false,
467
+ allowSetTerminals: false,
468
+ ownerMustSendPayouts: false,
469
+ allowSetController: false,
470
+ allowAddAccountingContext: true,
471
+ allowAddPriceFeed: false,
472
+ holdFees: false,
473
+ useTotalSurplusForCashOuts: false,
474
+ useDataHookForPay: false,
475
+ useDataHookForCashOut: false,
476
+ dataHook: address(0),
477
+ metadata: 0
478
+ });
479
+
480
+ uint256 packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_rulesMetadata);
481
+
482
+ // Start the cascade of issuing project tokens to the fee beneficiary.
483
+ mockExpect(
484
+ address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(address(controller))
485
+ );
486
+
487
+ // mock owner call
488
+ mockExpect(address(projects), abi.encodeCall(IERC721.ownerOf, (_projectId)), abi.encode(address(this)));
489
+
490
+ JBRuleset memory returnedRuleset = JBRuleset({
491
+ cycleNumber: 1,
492
+ id: 1,
493
+ basedOnId: 0,
494
+ start: 0,
495
+ duration: 0,
496
+ weight: weight,
497
+ weightCutPercent: 0,
498
+ approvalHook: IJBRulesetApprovalHook(address(0)),
499
+ metadata: packedMetadata
500
+ });
501
+
502
+ // mock call to tokens decimals()
503
+ mockExpect(mockToken, abi.encodeCall(IERC20Metadata.decimals, ()), abi.encode(18));
504
+
505
+ // mock call to rulesets currentOf
506
+ mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(returnedRuleset));
507
+
508
+ // Set up accounting context
509
+ JBAccountingContext[] memory _tokens = new JBAccountingContext[](1);
510
+ _tokens[0] = JBAccountingContext({token: mockToken, decimals: 18, currency: currencyId});
511
+
512
+ _terminal.addAccountingContextsFor(_projectId, _tokens);
513
+
514
+ // recordUsedAllowance
515
+ mockExpect(
516
+ address(store),
517
+ abi.encodeCall(IJBTerminalStore.recordUsedAllowanceOf, (_projectId, _tokens[0], 100, 0)),
518
+ abi.encode(returnedRuleset, 100)
519
+ );
520
+
521
+ // first feeless check: false
522
+ mockExpect(
523
+ address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (address(this))), abi.encode(false)
524
+ );
525
+
526
+ // second feeless check: false
527
+ mockExpect(
528
+ address(feelessAddresses), abi.encodeCall(IJBFeelessAddresses.isFeeless, (beneficiary)), abi.encode(false)
529
+ );
530
+
531
+ // Fee = 2.5% of 100 = 2, net = 98
532
+ mockExpect(mockToken, abi.encodeCall(IERC20.transfer, (beneficiary, 98)), abi.encode(true));
533
+
534
+ // call to find the primary terminal for fee processing
535
+ mockExpect(
536
+ address(directory),
537
+ abi.encodeCall(IJBDirectory.primaryTerminalOf, (1, mockToken)),
538
+ abi.encode(address(_terminal))
539
+ );
540
+
541
+ JBTokenAmount memory tokenContext =
542
+ JBTokenAmount({token: mockToken, decimals: 18, currency: currencyId, value: 2});
543
+
544
+ // mock call to jbterminalstore recordPaymentFrom for the fee payment
545
+ mockExpect(
546
+ address(store),
547
+ abi.encodeCall(
548
+ IJBTerminalStore.recordPaymentFrom,
549
+ (address(_terminal), tokenContext, _projectId, address(this), bytes(abi.encodePacked(_projectId)))
550
+ ),
551
+ abi.encode(returnedRuleset, 1, new JBPayHookSpecification[](0))
552
+ );
553
+
554
+ // Return the mint call as minting one project token for paying a fee.
555
+ mockExpect(
556
+ address(controller),
557
+ abi.encodeCall(IJBController.mintTokensOf, (_projectId, 1, address(this), "", true)),
558
+ abi.encode(2)
559
+ );
560
+
561
+ // Expect ProcessFee event (fee is processed immediately, not held)
562
+ vm.expectEmit(true, true, true, true);
563
+ emit IJBFeeTerminal.ProcessFee({
564
+ projectId: _projectId,
565
+ token: mockToken,
566
+ amount: 2,
567
+ wasHeld: false,
568
+ beneficiary: address(this),
569
+ caller: address(this)
570
+ });
571
+
572
+ // Expect UseAllowance event
573
+ vm.expectEmit();
574
+ emit IJBPayoutTerminal.UseAllowance({
575
+ rulesetId: returnedRuleset.id,
576
+ rulesetCycleNumber: returnedRuleset.cycleNumber,
577
+ projectId: _projectId,
578
+ beneficiary: beneficiary,
579
+ feeBeneficiary: address(this),
580
+ amount: 100,
581
+ amountPaidOut: 100,
582
+ netAmountPaidOut: 98,
583
+ memo: "",
584
+ caller: address(this)
585
+ });
586
+
587
+ _terminal.useAllowanceOf({
588
+ projectId: _projectId,
589
+ token: mockToken,
590
+ amount: 100,
591
+ currency: 0,
592
+ minTokensPaidOut: 97,
593
+ beneficiary: payable(beneficiary),
594
+ feeBeneficiary: payable(address(this)),
595
+ memo: ""
596
+ });
317
597
  }
318
598
 
319
599
  function test_WhenTokenEQNATIVE_TOKEN() external {