@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.
- package/ADMINISTRATION.md +3 -0
- package/ARCHITECTURE.md +24 -0
- package/AUDIT_INSTRUCTIONS.md +4 -2
- package/CHANGE_LOG.md +29 -1
- package/README.md +12 -2
- package/RISKS.md +10 -2
- package/SKILLS.md +9 -0
- package/USER_JOURNEYS.md +6 -0
- package/foundry.toml +1 -0
- package/package.json +1 -1
- package/src/JBController.sol +52 -5
- package/src/JBMultiTerminal.sol +197 -179
- package/src/JBTerminalStore.sol +367 -171
- package/src/interfaces/IJBCashOutTerminal.sol +30 -0
- package/src/interfaces/IJBController.sol +15 -0
- package/src/interfaces/IJBTerminal.sol +28 -0
- package/src/interfaces/IJBTerminalStore.sol +66 -0
- package/src/libraries/JBPayoutSplitGroupLib.sol +157 -0
- package/src/structs/JBCashOutHookSpecification.sol +2 -0
- package/src/structs/JBPayHookSpecification.sol +2 -0
- package/test/CoreExploitTests.t.sol +21 -10
- package/test/TestCashOutHooks.sol +6 -4
- package/test/TestDataHookFuzzing.sol +6 -2
- package/test/TestPayHooks.sol +1 -1
- package/test/TestRulesetQueueing.sol +4 -5
- package/test/TestRulesetQueuingStress.sol +5 -3
- package/test/TestTerminalPreviewParity.sol +208 -0
- package/test/fork/TestSequencerPriceFeedFork.sol +168 -0
- package/test/fork/TestTerminalPreviewParityFork.sol +109 -0
- package/test/units/static/JBController/TestPreviewMintOf.sol +116 -0
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +144 -25
- package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +11 -1
- package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +15 -2
- package/test/units/static/JBMultiTerminal/TestPay.sol +64 -2
- package/test/units/static/JBMultiTerminal/TestPreviewCashOutFrom.sol +116 -0
- package/test/units/static/JBMultiTerminal/TestPreviewPayFor.sol +98 -0
- package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +11 -2
- package/test/units/static/JBRulesets/TestCurrentOf.sol +8 -6
- package/test/units/static/JBRulesets/TestRulesets.sol +25 -24
- package/test/units/static/JBRulesets/TestUpcomingRulesetOf.sol +4 -17
- package/test/units/static/JBSurplus/TestSurplusFuzz.sol +49 -2
- package/test/units/static/JBTerminalStore/TestCurrentReclaimableSurplusOf.sol +215 -0
- package/test/units/static/JBTerminalStore/TestPreviewCashOutFrom.sol +475 -0
- package/test/units/static/JBTerminalStore/TestPreviewPayFrom.sol +464 -0
- package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +113 -2
- package/test/units/static/JBTerminalStore/TestRecordPaymentFrom.sol +227 -5
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
|
|
5
|
+
import {IJBController} from "../../../../src/interfaces/IJBController.sol";
|
|
6
|
+
import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBPayHook} from "../../../../src/interfaces/IJBPayHook.sol";
|
|
8
|
+
import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
|
|
9
|
+
import {IJBTerminalStore} from "../../../../src/interfaces/IJBTerminalStore.sol";
|
|
10
|
+
import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
|
|
11
|
+
import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
|
|
12
|
+
import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
|
|
13
|
+
import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
|
|
14
|
+
import {JBTokenAmount} from "../../../../src/structs/JBTokenAmount.sol";
|
|
15
|
+
import {JBMultiTerminalSetup} from "./JBMultiTerminalSetup.sol";
|
|
16
|
+
|
|
17
|
+
contract TestPreviewPayFor_Local is JBMultiTerminalSetup {
|
|
18
|
+
uint256 _projectId = 1;
|
|
19
|
+
uint256 _amount = 1e18;
|
|
20
|
+
address _token = JBConstants.NATIVE_TOKEN;
|
|
21
|
+
address _beneficiary = makeAddr("beneficiary");
|
|
22
|
+
address _payer = makeAddr("payer");
|
|
23
|
+
IJBController _controller = IJBController(makeAddr("controller"));
|
|
24
|
+
|
|
25
|
+
function setUp() public {
|
|
26
|
+
super.multiTerminalSetup();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _setAccountingContext(address token, uint8 decimals, uint32 currency) internal {
|
|
30
|
+
bytes32 contextSlot = keccak256(abi.encode(_projectId, uint256(0)));
|
|
31
|
+
bytes32 slot = keccak256(abi.encode(token, contextSlot));
|
|
32
|
+
bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
|
|
33
|
+
vm.store(address(_terminal), slot, packed);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function test_RevertsWhenTokenIsNotAccepted() external {
|
|
37
|
+
vm.prank(_payer);
|
|
38
|
+
vm.expectRevert(abi.encodeWithSelector(JBMultiTerminal.JBMultiTerminal_TokenNotAccepted.selector, _token));
|
|
39
|
+
JBMultiTerminal(address(_terminal)).previewPayFor(_projectId, _token, _amount, _beneficiary, "");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function test_ReturnsRulesetMintSplitAndHookSpecifications() external {
|
|
43
|
+
_setAccountingContext(_token, 18, uint32(uint160(_token)));
|
|
44
|
+
|
|
45
|
+
JBRuleset memory ruleset = JBRuleset({
|
|
46
|
+
cycleNumber: 1,
|
|
47
|
+
id: 1,
|
|
48
|
+
basedOnId: 0,
|
|
49
|
+
start: uint48(block.timestamp),
|
|
50
|
+
duration: 0,
|
|
51
|
+
weight: 0,
|
|
52
|
+
weightCutPercent: 0,
|
|
53
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
54
|
+
metadata: 0
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
JBPayHookSpecification[] memory specs = new JBPayHookSpecification[](1);
|
|
58
|
+
specs[0] =
|
|
59
|
+
JBPayHookSpecification({hook: IJBPayHook(makeAddr("hook")), noop: false, amount: 123, metadata: hex"1234"});
|
|
60
|
+
|
|
61
|
+
JBTokenAmount memory tokenAmount =
|
|
62
|
+
JBTokenAmount({token: _token, decimals: 18, currency: uint32(uint160(_token)), value: _amount});
|
|
63
|
+
|
|
64
|
+
mockExpect(
|
|
65
|
+
address(store),
|
|
66
|
+
abi.encodeCall(IJBTerminalStore.previewPayFrom, (_payer, tokenAmount, _projectId, _beneficiary, bytes(""))),
|
|
67
|
+
abi.encode(ruleset, 1000, specs)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
mockExpect(
|
|
71
|
+
address(directory),
|
|
72
|
+
abi.encodeCall(IJBDirectory.controllerOf, (_projectId)),
|
|
73
|
+
abi.encode(address(_controller))
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
mockExpect(
|
|
77
|
+
address(_controller),
|
|
78
|
+
abi.encodeCall(IJBController.previewMintOf, (_projectId, 1000, true)),
|
|
79
|
+
abi.encode(750, 250)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
vm.prank(_payer);
|
|
83
|
+
(
|
|
84
|
+
JBRuleset memory previewRuleset,
|
|
85
|
+
uint256 beneficiaryTokenCount,
|
|
86
|
+
uint256 reservedTokenCount,
|
|
87
|
+
JBPayHookSpecification[] memory previewSpecs
|
|
88
|
+
) = JBMultiTerminal(address(_terminal)).previewPayFor(_projectId, _token, _amount, _beneficiary, "");
|
|
89
|
+
|
|
90
|
+
assertEq(previewRuleset.id, ruleset.id);
|
|
91
|
+
assertEq(beneficiaryTokenCount, 750);
|
|
92
|
+
assertEq(reservedTokenCount, 250);
|
|
93
|
+
assertEq(previewSpecs.length, 1);
|
|
94
|
+
assertEq(address(previewSpecs[0].hook), address(specs[0].hook));
|
|
95
|
+
assertEq(previewSpecs[0].amount, specs[0].amount);
|
|
96
|
+
assertEq(previewSpecs[0].metadata, specs[0].metadata);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -59,10 +59,18 @@ contract TestProcessHeldFeesOf_Local is JBTest {
|
|
|
59
59
|
IPermit2 public permit2 = IPermit2(makeAddr("permit2"));
|
|
60
60
|
address trustedForwarder = makeAddr("forwarder");
|
|
61
61
|
|
|
62
|
+
uint256 _feeProjectId = 1;
|
|
62
63
|
uint256 _projectId = 2;
|
|
63
64
|
address _mockToken = makeAddr("token");
|
|
64
65
|
address _beneficiary = makeAddr("beneficiary");
|
|
65
66
|
|
|
67
|
+
function _setAccountingContext(uint256 projectId, address token, uint8 decimals, uint32 currency) internal {
|
|
68
|
+
bytes32 contextSlot = keccak256(abi.encode(projectId, uint256(0)));
|
|
69
|
+
bytes32 slot = keccak256(abi.encode(token, contextSlot));
|
|
70
|
+
bytes32 packed = bytes32(uint256(uint160(token)) | (uint256(decimals) << 160) | (uint256(currency) << 168));
|
|
71
|
+
vm.store(address(_terminal), slot, packed);
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
function setUp() public {
|
|
67
75
|
// Constructor will call to find directory and rulesets from the terminal store
|
|
68
76
|
mockExpect(address(store), abi.encodeCall(IJBTerminalStore.DIRECTORY, ()), abi.encode(address(directory)));
|
|
@@ -118,6 +126,9 @@ contract TestProcessHeldFeesOf_Local is JBTest {
|
|
|
118
126
|
// The fee amount that will be calculated from the held amount
|
|
119
127
|
uint256 expectedFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: heldAmount, feePercent: _terminal.FEE()});
|
|
120
128
|
|
|
129
|
+
// Set up accounting context for the fee beneficiary project (project 1) so _pay can build the token amount.
|
|
130
|
+
_setAccountingContext(_feeProjectId, _mockToken, 0, uint32(uint160(_mockToken)));
|
|
131
|
+
|
|
121
132
|
// Mock the directory call to find the fee terminal - return _terminal itself so it uses internal _pay
|
|
122
133
|
mockExpect(
|
|
123
134
|
address(directory),
|
|
@@ -128,8 +139,6 @@ contract TestProcessHeldFeesOf_Local is JBTest {
|
|
|
128
139
|
// Mock executeProcessFee: when the terminal calls itself, it will call recordPaymentFrom on the store.
|
|
129
140
|
// Since executeProcessFee is external and calls pay on the feeTerminal (which is _terminal itself),
|
|
130
141
|
// we need to mock the internal pay path: recordPaymentFrom on the store.
|
|
131
|
-
// The token amount struct for the fee payment
|
|
132
|
-
// Note: accounting context for project 1 on _mockToken is unset, so decimals=0 and currency=0.
|
|
133
142
|
vm.mockCall(
|
|
134
143
|
address(store),
|
|
135
144
|
abi.encodeWithSelector(IJBTerminalStore.recordPaymentFrom.selector),
|
|
@@ -191,12 +191,13 @@ contract TestCurrentOf_Local is JBRulesetsSetup {
|
|
|
191
191
|
{
|
|
192
192
|
// it will return the ruleset the pending approval ruleset is basedOn
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
uint256
|
|
194
|
+
// Capture IDs from actual storage (avoid via_ir reordering of block.timestamp).
|
|
195
|
+
uint256 _firstRulesetId = _rulesets.currentOf(_projectId).id;
|
|
196
|
+
uint256 _rulesetWithHookId = _firstRulesetId + 1;
|
|
196
197
|
|
|
197
198
|
JBRuleset memory _queuedRuleset = _rulesets.getRulesetOf(_projectId, _rulesetWithHookId);
|
|
198
199
|
|
|
199
|
-
vm.warp(
|
|
200
|
+
vm.warp(_firstRulesetId + 3 days);
|
|
200
201
|
|
|
201
202
|
// mock approvalStatusOf to return Pending
|
|
202
203
|
mockExpect(
|
|
@@ -215,12 +216,13 @@ contract TestCurrentOf_Local is JBRulesetsSetup {
|
|
|
215
216
|
{
|
|
216
217
|
// it will return the basedOn of the latest ruleset
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
uint256
|
|
219
|
+
// Capture IDs from actual storage (avoid via_ir reordering of block.timestamp).
|
|
220
|
+
uint256 _firstRulesetId = _rulesets.currentOf(_projectId).id;
|
|
221
|
+
uint256 _rulesetWithHookId = _firstRulesetId + 1;
|
|
220
222
|
|
|
221
223
|
JBRuleset memory _queuedRuleset = _rulesets.getRulesetOf(_projectId, _rulesetWithHookId);
|
|
222
224
|
|
|
223
|
-
vm.warp(
|
|
225
|
+
vm.warp(_firstRulesetId + 4 days);
|
|
224
226
|
|
|
225
227
|
// mock approvalStatusOf to return Pending
|
|
226
228
|
mockExpect(
|
|
@@ -403,7 +403,8 @@ contract TestJBRulesetsUnits_Local is JBTest {
|
|
|
403
403
|
mustStartAtOrAfter: _mustStartAt
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
|
|
406
|
+
// Capture firstId from actual storage (avoid via_ir reordering of block.timestamp).
|
|
407
|
+
uint256 firstId = _rulesets.latestRulesetIdOf(_projectId);
|
|
407
408
|
|
|
408
409
|
// Mock call to approval hook duration
|
|
409
410
|
bytes memory _encodedDurationCall = abi.encodeCall(IJBRulesetApprovalHook.DURATION, ());
|
|
@@ -412,9 +413,9 @@ contract TestJBRulesetsUnits_Local is JBTest {
|
|
|
412
413
|
mockExpect(address(_mockApprovalHook), _encodedDurationCall, _willReturnDuration);
|
|
413
414
|
|
|
414
415
|
// avoid overwrite
|
|
415
|
-
vm.warp(
|
|
416
|
+
vm.warp(firstId + 1);
|
|
416
417
|
|
|
417
|
-
uint256 latestId
|
|
418
|
+
uint256 latestId;
|
|
418
419
|
|
|
419
420
|
// Send: Anotha One! Call from this contract as it's been mock authorized above.
|
|
420
421
|
_rulesets.queueFor({
|
|
@@ -424,12 +425,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
|
|
|
424
425
|
weightCutPercent: _weightCutPercent,
|
|
425
426
|
approvalHook: _mockApprovalHook,
|
|
426
427
|
metadata: _packedWithApprovalHook,
|
|
427
|
-
mustStartAtOrAfter:
|
|
428
|
+
mustStartAtOrAfter: 0
|
|
428
429
|
});
|
|
429
430
|
|
|
431
|
+
// Capture latestId from storage (avoid via_ir reordering of block.timestamp).
|
|
432
|
+
latestId = _rulesets.latestRulesetIdOf(_projectId);
|
|
433
|
+
|
|
430
434
|
// avoid overwrite
|
|
431
|
-
vm.warp(
|
|
432
|
-
uint256 previouslyApprovedDurationEnds = block.timestamp + 3 days - 2 days - 1;
|
|
435
|
+
vm.warp(latestId + 2 days);
|
|
433
436
|
|
|
434
437
|
// Get the ruleset.
|
|
435
438
|
JBRuleset memory latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
|
|
@@ -449,16 +452,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
|
|
|
449
452
|
weightCutPercent: _weightCutPercent,
|
|
450
453
|
approvalHook: _mockApprovalHook,
|
|
451
454
|
metadata: _packedWithApprovalHook,
|
|
452
|
-
mustStartAtOrAfter:
|
|
455
|
+
mustStartAtOrAfter: 0
|
|
453
456
|
});
|
|
454
457
|
|
|
455
|
-
latestId =
|
|
458
|
+
latestId = _rulesets.latestRulesetIdOf(_projectId);
|
|
456
459
|
latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
|
|
457
460
|
|
|
458
461
|
// avoid overwrite
|
|
459
|
-
vm.warp(
|
|
460
|
-
|
|
461
|
-
previouslyApprovedDurationEnds = block.timestamp + 6 days - 2 days - 2;
|
|
462
|
+
vm.warp(latestId + 1);
|
|
462
463
|
|
|
463
464
|
// Mock call to approvalStatusOf and return an approvalExpected status
|
|
464
465
|
_encodedApprovalCall = abi.encodeCall(IJBRulesetApprovalHook.approvalStatusOf, (1, latesetQueuedRuleset));
|
|
@@ -474,15 +475,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
|
|
|
474
475
|
weightCutPercent: _weightCutPercent,
|
|
475
476
|
approvalHook: _mockApprovalHook,
|
|
476
477
|
metadata: _packedWithApprovalHook,
|
|
477
|
-
mustStartAtOrAfter:
|
|
478
|
+
mustStartAtOrAfter: 0
|
|
478
479
|
});
|
|
479
480
|
|
|
480
|
-
latestId =
|
|
481
|
+
latestId = _rulesets.latestRulesetIdOf(_projectId);
|
|
481
482
|
latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
|
|
482
483
|
|
|
483
484
|
// avoid overwrite
|
|
484
|
-
vm.warp(
|
|
485
|
-
previouslyApprovedDurationEnds = block.timestamp + 6 days - 2 days - 3;
|
|
485
|
+
vm.warp(latestId + 1);
|
|
486
486
|
|
|
487
487
|
// Mock call to approvalStatusOf and return a failed status
|
|
488
488
|
_encodedApprovalCall = abi.encodeCall(IJBRulesetApprovalHook.approvalStatusOf, (1, latesetQueuedRuleset));
|
|
@@ -498,16 +498,14 @@ contract TestJBRulesetsUnits_Local is JBTest {
|
|
|
498
498
|
weightCutPercent: _weightCutPercent,
|
|
499
499
|
approvalHook: _mockApprovalHook,
|
|
500
500
|
metadata: _packedWithApprovalHook,
|
|
501
|
-
mustStartAtOrAfter:
|
|
501
|
+
mustStartAtOrAfter: 0
|
|
502
502
|
});
|
|
503
503
|
|
|
504
|
-
latestId =
|
|
504
|
+
latestId = _rulesets.latestRulesetIdOf(_projectId);
|
|
505
505
|
latesetQueuedRuleset = _rulesets.getRulesetOf(_projectId, latestId);
|
|
506
506
|
|
|
507
507
|
// avoid overwrite
|
|
508
|
-
vm.warp(
|
|
509
|
-
|
|
510
|
-
previouslyApprovedDurationEnds = block.timestamp + 6 days - 2 days - 4;
|
|
508
|
+
vm.warp(latestId + 1);
|
|
511
509
|
|
|
512
510
|
// Mock call to approvalStatusOf and return an empty status
|
|
513
511
|
_encodedApprovalCall = abi.encodeCall(IJBRulesetApprovalHook.approvalStatusOf, (1, latesetQueuedRuleset));
|
|
@@ -523,14 +521,17 @@ contract TestJBRulesetsUnits_Local is JBTest {
|
|
|
523
521
|
weightCutPercent: _weightCutPercent,
|
|
524
522
|
approvalHook: _mockApprovalHook,
|
|
525
523
|
metadata: _packedWithApprovalHook,
|
|
526
|
-
mustStartAtOrAfter:
|
|
524
|
+
mustStartAtOrAfter: 0
|
|
527
525
|
});
|
|
528
526
|
|
|
529
|
-
|
|
527
|
+
// Capture final latestId from storage.
|
|
528
|
+
latestId = _rulesets.latestRulesetIdOf(_projectId);
|
|
529
|
+
|
|
530
|
+
JBRuleset[] memory queuedRulesetsOf = _rulesets.allOf(_projectId, latestId, 3);
|
|
530
531
|
|
|
531
|
-
// check:
|
|
532
|
+
// check: 3 rulesets will be enqueued
|
|
532
533
|
assertEq(queuedRulesetsOf.length, 3);
|
|
533
|
-
assertEq(queuedRulesetsOf[0].id,
|
|
534
|
+
assertEq(queuedRulesetsOf[0].id, latestId);
|
|
534
535
|
|
|
535
536
|
// check first timestamp
|
|
536
537
|
assertEq(queuedRulesetsOf[2].id, firstId);
|
|
@@ -457,8 +457,6 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
|
|
|
457
457
|
function test_baseRulesetDurationDNEQZero() external {
|
|
458
458
|
// it will simulate a ruleset basedOn
|
|
459
459
|
|
|
460
|
-
uint256 ogTimestamp = block.timestamp;
|
|
461
|
-
|
|
462
460
|
// put code at hook address
|
|
463
461
|
vm.etch(address(_mockApprovalHook), abi.encode(1));
|
|
464
462
|
|
|
@@ -476,20 +474,6 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
|
|
|
476
474
|
|
|
477
475
|
mockExpect(address(directory), _encodedCall, _willReturn);
|
|
478
476
|
|
|
479
|
-
// Setup: expect ruleset event (RulesetQueued) is emitted
|
|
480
|
-
vm.expectEmit();
|
|
481
|
-
emit IJBRulesets.RulesetQueued(
|
|
482
|
-
block.timestamp,
|
|
483
|
-
_projectId,
|
|
484
|
-
_duration,
|
|
485
|
-
_weight,
|
|
486
|
-
_weightCutPercent,
|
|
487
|
-
_mockApprovalHook,
|
|
488
|
-
_packedWithApprovalHook,
|
|
489
|
-
block.timestamp,
|
|
490
|
-
address(this)
|
|
491
|
-
);
|
|
492
|
-
|
|
493
477
|
// Send: Call from this contract as it's been mock authorized above.
|
|
494
478
|
_rulesets.queueFor({
|
|
495
479
|
projectId: _projectId,
|
|
@@ -501,6 +485,9 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
|
|
|
501
485
|
mustStartAtOrAfter: _mustStartAt
|
|
502
486
|
});
|
|
503
487
|
|
|
488
|
+
// Capture the first ruleset's id from storage (avoid via_ir reordering of block.timestamp).
|
|
489
|
+
uint256 ogTimestamp = _rulesets.currentOf(_projectId).id;
|
|
490
|
+
|
|
504
491
|
// mock call to hook duration
|
|
505
492
|
mockExpect(
|
|
506
493
|
address(_mockApprovalHook), abi.encodeCall(IJBRulesetApprovalHook.DURATION, ()), abi.encode(_hookDuration)
|
|
@@ -517,7 +504,7 @@ contract TestUpcomingOf_Local is JBRulesetsSetup {
|
|
|
517
504
|
mustStartAtOrAfter: 0
|
|
518
505
|
});
|
|
519
506
|
|
|
520
|
-
vm.warp(
|
|
507
|
+
vm.warp(ogTimestamp + 3 days);
|
|
521
508
|
|
|
522
509
|
uint256 _latestQueuedId = _rulesets.latestRulesetIdOf(_projectId);
|
|
523
510
|
JBRuleset memory _queuedRuleset = _rulesets.getRulesetOf(_projectId, _latestQueuedId);
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
import {IJBCashOutTerminal} from "../../../../src/interfaces/IJBCashOutTerminal.sol";
|
|
4
5
|
import {IJBTerminal} from "../../../../src/interfaces/IJBTerminal.sol";
|
|
5
6
|
import {JBAccountingContext} from "../../../../src/structs/JBAccountingContext.sol";
|
|
7
|
+
import {JBCashOutHookSpecification} from "../../../../src/structs/JBCashOutHookSpecification.sol";
|
|
8
|
+
import {JBPayHookSpecification} from "../../../../src/structs/JBPayHookSpecification.sol";
|
|
9
|
+
import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
|
|
6
10
|
import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
7
11
|
import {JBTest} from "../../../helpers/JBTest.sol";
|
|
8
12
|
import {JBSurplus} from "../../../../src/libraries/JBSurplus.sol";
|
|
9
13
|
|
|
10
14
|
/// @notice Mock terminal that returns a fixed surplus for testing JBSurplus.
|
|
11
|
-
contract MockSurplusTerminal is ERC165,
|
|
15
|
+
contract MockSurplusTerminal is ERC165, IJBCashOutTerminal {
|
|
12
16
|
uint256 public surplusAmount;
|
|
13
17
|
|
|
14
18
|
constructor(uint256 _surplus) {
|
|
@@ -30,7 +34,8 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
|
|
33
|
-
return interfaceId == type(IJBTerminal).interfaceId ||
|
|
37
|
+
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBCashOutTerminal).interfaceId
|
|
38
|
+
|| super.supportsInterface(interfaceId);
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
// Stub implementations for IJBTerminal
|
|
@@ -41,6 +46,31 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
|
|
|
41
46
|
returns (JBAccountingContext memory)
|
|
42
47
|
{}
|
|
43
48
|
function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory) {}
|
|
49
|
+
function previewCashOutFrom(
|
|
50
|
+
address,
|
|
51
|
+
uint256,
|
|
52
|
+
uint256,
|
|
53
|
+
address,
|
|
54
|
+
address payable,
|
|
55
|
+
bytes calldata
|
|
56
|
+
)
|
|
57
|
+
external
|
|
58
|
+
pure
|
|
59
|
+
override
|
|
60
|
+
returns (JBRuleset memory, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
61
|
+
{}
|
|
62
|
+
function previewPayFor(
|
|
63
|
+
uint256,
|
|
64
|
+
address,
|
|
65
|
+
uint256,
|
|
66
|
+
address,
|
|
67
|
+
bytes calldata
|
|
68
|
+
)
|
|
69
|
+
external
|
|
70
|
+
pure
|
|
71
|
+
override
|
|
72
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
|
|
73
|
+
{}
|
|
44
74
|
function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
|
|
45
75
|
function addToBalanceOf(
|
|
46
76
|
uint256,
|
|
@@ -75,6 +105,23 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
|
|
|
75
105
|
{
|
|
76
106
|
return 0;
|
|
77
107
|
}
|
|
108
|
+
|
|
109
|
+
function cashOutTokensOf(
|
|
110
|
+
address,
|
|
111
|
+
uint256,
|
|
112
|
+
uint256,
|
|
113
|
+
address,
|
|
114
|
+
uint256,
|
|
115
|
+
address payable,
|
|
116
|
+
bytes calldata
|
|
117
|
+
)
|
|
118
|
+
external
|
|
119
|
+
pure
|
|
120
|
+
override
|
|
121
|
+
returns (uint256)
|
|
122
|
+
{
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
78
125
|
}
|
|
79
126
|
|
|
80
127
|
/// @notice Fuzz tests for the JBSurplus library.
|
|
@@ -444,4 +444,219 @@ contract TestCurrentReclaimableSurplusOf_Local is JBTerminalStoreSetup {
|
|
|
444
444
|
uint256 reclaimable = _store.currentReclaimableSurplusOf(_projectId, _tokenCount, 1e18, 1e18);
|
|
445
445
|
assertEq(1e18, reclaimable);
|
|
446
446
|
}
|
|
447
|
+
|
|
448
|
+
function test_GivenTotalReclaimableWithSurplus() external whenProjectHasBalance {
|
|
449
|
+
// it will default to all terminals and all accounting contexts and return the reclaimable surplus
|
|
450
|
+
|
|
451
|
+
// setup calldata
|
|
452
|
+
JBAccountingContext[] memory _contexts = new JBAccountingContext[](1);
|
|
453
|
+
_contexts[0] = JBAccountingContext({token: address(_token), decimals: 18, currency: _currency});
|
|
454
|
+
|
|
455
|
+
JBRulesetMetadata memory _metadata = JBRulesetMetadata({
|
|
456
|
+
reservedPercent: 0,
|
|
457
|
+
cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE / 2,
|
|
458
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
459
|
+
pausePay: false,
|
|
460
|
+
pauseCreditTransfers: false,
|
|
461
|
+
allowOwnerMinting: false,
|
|
462
|
+
allowSetCustomToken: false,
|
|
463
|
+
allowTerminalMigration: false,
|
|
464
|
+
allowSetTerminals: false,
|
|
465
|
+
ownerMustSendPayouts: false,
|
|
466
|
+
allowSetController: false,
|
|
467
|
+
allowAddAccountingContext: true,
|
|
468
|
+
allowAddPriceFeed: false,
|
|
469
|
+
holdFees: false,
|
|
470
|
+
useTotalSurplusForCashOuts: false,
|
|
471
|
+
useDataHookForPay: false,
|
|
472
|
+
useDataHookForCashOut: false,
|
|
473
|
+
dataHook: address(0),
|
|
474
|
+
metadata: 0
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
|
|
478
|
+
|
|
479
|
+
// JBRulesets return calldata
|
|
480
|
+
JBRuleset memory _returnedRuleset = JBRuleset({
|
|
481
|
+
cycleNumber: uint48(block.timestamp),
|
|
482
|
+
id: uint48(block.timestamp),
|
|
483
|
+
basedOnId: 0,
|
|
484
|
+
start: uint48(block.timestamp),
|
|
485
|
+
duration: uint32(block.timestamp + 1000),
|
|
486
|
+
weight: 1e18,
|
|
487
|
+
weightCutPercent: 0,
|
|
488
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
489
|
+
metadata: _packedMetadata
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// mock call to JBRulesets currentOf
|
|
493
|
+
mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
|
|
494
|
+
|
|
495
|
+
// mock call to JBDirectory controllerOf
|
|
496
|
+
mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
|
|
497
|
+
|
|
498
|
+
uint256 _supply = 1e19;
|
|
499
|
+
uint256 _surplus = 1e18;
|
|
500
|
+
uint256 _cashoutAmount = 1e18;
|
|
501
|
+
|
|
502
|
+
// mock JBDirectory terminalsOf to return the terminal
|
|
503
|
+
IJBTerminal[] memory _terminals = new IJBTerminal[](1);
|
|
504
|
+
_terminals[0] = _terminal;
|
|
505
|
+
mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
|
|
506
|
+
|
|
507
|
+
// surplus call to the terminal (empty accounting contexts passed through)
|
|
508
|
+
JBAccountingContext[] memory _emptyContexts = new JBAccountingContext[](0);
|
|
509
|
+
mockExpect(
|
|
510
|
+
address(_terminal),
|
|
511
|
+
abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
|
|
512
|
+
abi.encode(_surplus)
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
// mock JBController totalTokenSupplyWithReservedTokensOf
|
|
516
|
+
mockExpect(
|
|
517
|
+
address(_controller),
|
|
518
|
+
abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
|
|
519
|
+
abi.encode(_supply)
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// Call the new convenience function (no terminals, no accounting contexts).
|
|
523
|
+
uint256 reclaimable = _store.currentTotalReclaimableSurplusOf(_projectId, _cashoutAmount, 18, _currency);
|
|
524
|
+
|
|
525
|
+
// Should match the 6-param overload result.
|
|
526
|
+
uint256 assumed =
|
|
527
|
+
JBCashOuts.cashOutFrom(_surplus, _cashoutAmount, _supply, JBConstants.MAX_CASH_OUT_TAX_RATE / 2);
|
|
528
|
+
|
|
529
|
+
assertEq(assumed, reclaimable);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function test_GivenTotalReclaimableWithZeroSurplus() external {
|
|
533
|
+
// it will return zero when there is no surplus
|
|
534
|
+
|
|
535
|
+
JBAccountingContext[] memory _emptyContexts = new JBAccountingContext[](0);
|
|
536
|
+
|
|
537
|
+
// JBRulesets return calldata
|
|
538
|
+
JBRuleset memory _returnedRuleset = JBRuleset({
|
|
539
|
+
cycleNumber: uint48(block.timestamp),
|
|
540
|
+
id: uint48(block.timestamp),
|
|
541
|
+
basedOnId: 0,
|
|
542
|
+
start: uint48(block.timestamp),
|
|
543
|
+
duration: uint32(block.timestamp + 1000),
|
|
544
|
+
weight: 1e18,
|
|
545
|
+
weightCutPercent: 0,
|
|
546
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
547
|
+
metadata: 0
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// mock call to JBRulesets currentOf
|
|
551
|
+
mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
|
|
552
|
+
|
|
553
|
+
// mock JBDirectory terminalsOf to return the terminal
|
|
554
|
+
IJBTerminal[] memory _terminals = new IJBTerminal[](1);
|
|
555
|
+
_terminals[0] = _terminal;
|
|
556
|
+
mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
|
|
557
|
+
|
|
558
|
+
// mock current surplus as zero
|
|
559
|
+
mockExpect(
|
|
560
|
+
address(_terminal),
|
|
561
|
+
abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
|
|
562
|
+
abi.encode(0)
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
uint256 reclaimable = _store.currentTotalReclaimableSurplusOf(_projectId, _tokenCount, 18, _currency);
|
|
566
|
+
assertEq(0, reclaimable);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function test_GivenTotalReclaimableMatchesSixParamOverload() external whenProjectHasBalance {
|
|
570
|
+
// it will produce the same result as calling the 6-param overload with empty arrays
|
|
571
|
+
|
|
572
|
+
JBAccountingContext[] memory _emptyContexts = new JBAccountingContext[](0);
|
|
573
|
+
|
|
574
|
+
JBRulesetMetadata memory _metadata = JBRulesetMetadata({
|
|
575
|
+
reservedPercent: 0,
|
|
576
|
+
cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE / 2,
|
|
577
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
578
|
+
pausePay: false,
|
|
579
|
+
pauseCreditTransfers: false,
|
|
580
|
+
allowOwnerMinting: false,
|
|
581
|
+
allowSetCustomToken: false,
|
|
582
|
+
allowTerminalMigration: false,
|
|
583
|
+
allowSetTerminals: false,
|
|
584
|
+
ownerMustSendPayouts: false,
|
|
585
|
+
allowSetController: false,
|
|
586
|
+
allowAddAccountingContext: true,
|
|
587
|
+
allowAddPriceFeed: false,
|
|
588
|
+
holdFees: false,
|
|
589
|
+
useTotalSurplusForCashOuts: false,
|
|
590
|
+
useDataHookForPay: false,
|
|
591
|
+
useDataHookForCashOut: false,
|
|
592
|
+
dataHook: address(0),
|
|
593
|
+
metadata: 0
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
uint256 _packedMetadata = JBRulesetMetadataResolver.packRulesetMetadata(_metadata);
|
|
597
|
+
|
|
598
|
+
JBRuleset memory _returnedRuleset = JBRuleset({
|
|
599
|
+
cycleNumber: uint48(block.timestamp),
|
|
600
|
+
id: uint48(block.timestamp),
|
|
601
|
+
basedOnId: 0,
|
|
602
|
+
start: uint48(block.timestamp),
|
|
603
|
+
duration: uint32(block.timestamp + 1000),
|
|
604
|
+
weight: 1e18,
|
|
605
|
+
weightCutPercent: 0,
|
|
606
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
607
|
+
metadata: _packedMetadata
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
uint256 _supply = 1e19;
|
|
611
|
+
uint256 _surplus = 5e17;
|
|
612
|
+
uint256 _cashoutAmount = 1e18;
|
|
613
|
+
|
|
614
|
+
IJBTerminal[] memory _terminals = new IJBTerminal[](1);
|
|
615
|
+
_terminals[0] = _terminal;
|
|
616
|
+
|
|
617
|
+
// The new overload calls the 6-param via `this`, so JBRulesets.currentOf gets called twice.
|
|
618
|
+
// Mock it to return the same ruleset both times.
|
|
619
|
+
mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
|
|
620
|
+
|
|
621
|
+
mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
|
|
622
|
+
|
|
623
|
+
mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
|
|
624
|
+
|
|
625
|
+
mockExpect(
|
|
626
|
+
address(_terminal),
|
|
627
|
+
abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
|
|
628
|
+
abi.encode(_surplus)
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
mockExpect(
|
|
632
|
+
address(_controller),
|
|
633
|
+
abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
|
|
634
|
+
abi.encode(_supply)
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
// Call the new convenience function.
|
|
638
|
+
uint256 reclaimableDefault = _store.currentTotalReclaimableSurplusOf(_projectId, _cashoutAmount, 18, _currency);
|
|
639
|
+
|
|
640
|
+
// Re-mock for the 6-param call (mocks are consumed).
|
|
641
|
+
mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(_returnedRuleset));
|
|
642
|
+
mockExpect(address(directory), abi.encodeCall(IJBDirectory.controllerOf, (_projectId)), abi.encode(_controller));
|
|
643
|
+
mockExpect(address(directory), abi.encodeCall(IJBDirectory.terminalsOf, (_projectId)), abi.encode(_terminals));
|
|
644
|
+
mockExpect(
|
|
645
|
+
address(_terminal),
|
|
646
|
+
abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, _emptyContexts, 18, _currency)),
|
|
647
|
+
abi.encode(_surplus)
|
|
648
|
+
);
|
|
649
|
+
mockExpect(
|
|
650
|
+
address(_controller),
|
|
651
|
+
abi.encodeCall(IJBController.totalTokenSupplyWithReservedTokensOf, (_projectId)),
|
|
652
|
+
abi.encode(_supply)
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// Call the 6-param overload with empty arrays.
|
|
656
|
+
uint256 reclaimableExplicit = _store.currentReclaimableSurplusOf(
|
|
657
|
+
_projectId, _cashoutAmount, new IJBTerminal[](0), new JBAccountingContext[](0), 18, _currency
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
assertEq(reclaimableDefault, reclaimableExplicit);
|
|
661
|
+
}
|
|
447
662
|
}
|