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