@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,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
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {AggregatorV2V3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV2V3Interface.sol";
|
|
7
|
+
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
|
|
8
|
+
|
|
9
|
+
import {JBChainlinkV3SequencerPriceFeed} from "../../src/JBChainlinkV3SequencerPriceFeed.sol";
|
|
10
|
+
|
|
11
|
+
/// @notice Fork tests for JBChainlinkV3SequencerPriceFeed against the live Arbitrum sequencer uptime feed and
|
|
12
|
+
/// ETH/USD Chainlink oracle.
|
|
13
|
+
contract TestSequencerPriceFeedFork is Test {
|
|
14
|
+
// Chainlink feed addresses (Arbitrum mainnet).
|
|
15
|
+
address constant ARB_ETH_USD_FEED = 0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612;
|
|
16
|
+
address constant ARB_SEQUENCER_FEED = 0xFdB631F5EE196F0ed6FAa767959853A9F217697D;
|
|
17
|
+
|
|
18
|
+
// Staleness threshold (1 hour).
|
|
19
|
+
uint256 constant THRESHOLD = 3600;
|
|
20
|
+
|
|
21
|
+
// Grace period after sequencer restart (1 hour).
|
|
22
|
+
uint256 constant GRACE_PERIOD = 3600;
|
|
23
|
+
|
|
24
|
+
// Pinned block for reproducibility (sequencer is up at this block).
|
|
25
|
+
uint256 constant FORK_BLOCK = 300_000_000;
|
|
26
|
+
|
|
27
|
+
JBChainlinkV3SequencerPriceFeed feed;
|
|
28
|
+
|
|
29
|
+
function setUp() public {
|
|
30
|
+
string memory rpc = vm.envOr("RPC_ARBITRUM_MAINNET", string(""));
|
|
31
|
+
if (bytes(rpc).length == 0) {
|
|
32
|
+
// Skip all tests if no Arbitrum RPC is configured.
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
vm.createSelectFork(rpc, FORK_BLOCK);
|
|
37
|
+
|
|
38
|
+
feed = new JBChainlinkV3SequencerPriceFeed(
|
|
39
|
+
AggregatorV3Interface(ARB_ETH_USD_FEED),
|
|
40
|
+
THRESHOLD,
|
|
41
|
+
AggregatorV2V3Interface(ARB_SEQUENCER_FEED),
|
|
42
|
+
GRACE_PERIOD
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ------------------------------------------------------------------
|
|
47
|
+
// Helpers
|
|
48
|
+
// ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
modifier skipIfNoRpc() {
|
|
51
|
+
if (address(feed) == address(0)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
_;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ------------------------------------------------------------------
|
|
58
|
+
// 1. Normal operation — valid price returned
|
|
59
|
+
// ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/// @notice Under normal conditions (sequencer up, grace period elapsed), currentUnitPrice returns a sane price.
|
|
62
|
+
function test_normalOperation_returnsValidPrice() public skipIfNoRpc {
|
|
63
|
+
uint256 price18 = feed.currentUnitPrice(18);
|
|
64
|
+
|
|
65
|
+
// ETH price should be between $500 and $50,000.
|
|
66
|
+
assertGt(price18, 500e18, "ETH price too low");
|
|
67
|
+
assertLt(price18, 50_000e18, "ETH price too high");
|
|
68
|
+
|
|
69
|
+
// Cross-check against raw latestRoundData from the price feed.
|
|
70
|
+
(, int256 rawPrice,,,) = AggregatorV3Interface(ARB_ETH_USD_FEED).latestRoundData();
|
|
71
|
+
uint256 feedDecimals = AggregatorV3Interface(ARB_ETH_USD_FEED).decimals();
|
|
72
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
73
|
+
uint256 expected18 = uint256(rawPrice) * 10 ** (18 - feedDecimals);
|
|
74
|
+
assertEq(price18, expected18, "Price mismatch vs raw feed");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ------------------------------------------------------------------
|
|
78
|
+
// 2. Sequencer down — reverts
|
|
79
|
+
// ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/// @notice When the sequencer feed reports answer=1 (down), currentUnitPrice reverts.
|
|
82
|
+
function test_sequencerDown_reverts() public skipIfNoRpc {
|
|
83
|
+
// Mock the sequencer feed to report answer=1 (down).
|
|
84
|
+
// latestRoundData() selector = 0xfeaf968c
|
|
85
|
+
vm.mockCall(
|
|
86
|
+
ARB_SEQUENCER_FEED,
|
|
87
|
+
abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector),
|
|
88
|
+
abi.encode(
|
|
89
|
+
uint80(1), // roundId
|
|
90
|
+
int256(1), // answer = 1 → sequencer down
|
|
91
|
+
block.timestamp - GRACE_PERIOD - 100, // startedAt (irrelevant when answer=1)
|
|
92
|
+
block.timestamp, // updatedAt
|
|
93
|
+
uint80(1) // answeredInRound
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
vm.expectRevert(
|
|
98
|
+
abi.encodeWithSelector(
|
|
99
|
+
JBChainlinkV3SequencerPriceFeed.JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting.selector,
|
|
100
|
+
block.timestamp,
|
|
101
|
+
GRACE_PERIOD,
|
|
102
|
+
block.timestamp - GRACE_PERIOD - 100
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
feed.currentUnitPrice(18);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ------------------------------------------------------------------
|
|
109
|
+
// 3. Grace period active — reverts
|
|
110
|
+
// ------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/// @notice When the sequencer just came back up (within grace period), currentUnitPrice reverts.
|
|
113
|
+
function test_gracePeriodActive_reverts() public skipIfNoRpc {
|
|
114
|
+
// Mock the sequencer feed: answer=0 (up) but startedAt is 1 second ago (within grace period).
|
|
115
|
+
uint256 startedAt = block.timestamp - 1;
|
|
116
|
+
|
|
117
|
+
vm.mockCall(
|
|
118
|
+
ARB_SEQUENCER_FEED,
|
|
119
|
+
abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector),
|
|
120
|
+
abi.encode(
|
|
121
|
+
uint80(1), // roundId
|
|
122
|
+
int256(0), // answer = 0 → sequencer up
|
|
123
|
+
startedAt, // startedAt = very recent
|
|
124
|
+
block.timestamp, // updatedAt
|
|
125
|
+
uint80(1) // answeredInRound
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
vm.expectRevert(
|
|
130
|
+
abi.encodeWithSelector(
|
|
131
|
+
JBChainlinkV3SequencerPriceFeed.JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting.selector,
|
|
132
|
+
block.timestamp,
|
|
133
|
+
GRACE_PERIOD,
|
|
134
|
+
startedAt
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
feed.currentUnitPrice(18);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ------------------------------------------------------------------
|
|
141
|
+
// 4. Post-grace recovery — succeeds
|
|
142
|
+
// ------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/// @notice After the grace period has elapsed, currentUnitPrice succeeds again.
|
|
145
|
+
function test_postGraceRecovery_succeeds() public skipIfNoRpc {
|
|
146
|
+
// Mock the sequencer feed: answer=0 (up), startedAt is well before the grace period ended.
|
|
147
|
+
uint256 startedAt = block.timestamp - GRACE_PERIOD - 100;
|
|
148
|
+
|
|
149
|
+
vm.mockCall(
|
|
150
|
+
ARB_SEQUENCER_FEED,
|
|
151
|
+
abi.encodeWithSelector(AggregatorV3Interface.latestRoundData.selector),
|
|
152
|
+
abi.encode(
|
|
153
|
+
uint80(1), // roundId
|
|
154
|
+
int256(0), // answer = 0 → sequencer up
|
|
155
|
+
startedAt, // startedAt = well in the past
|
|
156
|
+
block.timestamp, // updatedAt
|
|
157
|
+
uint80(1) // answeredInRound
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// The price feed itself is still the real Chainlink feed, so this should succeed.
|
|
162
|
+
uint256 price18 = feed.currentUnitPrice(18);
|
|
163
|
+
|
|
164
|
+
// Same sanity check: ETH price between $500 and $50,000.
|
|
165
|
+
assertGt(price18, 500e18, "ETH price too low after recovery");
|
|
166
|
+
assertLt(price18, 50_000e18, "ETH price too high after recovery");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
|
+
|
|
4
|
+
import {TestTerminalPreviewParity_Local} from "../TestTerminalPreviewParity.sol";
|
|
5
|
+
import {IJBController} from "../../src/interfaces/IJBController.sol";
|
|
6
|
+
import {IJBTerminal} from "../../src/interfaces/IJBTerminal.sol";
|
|
7
|
+
import {IJBCashOutTerminal} from "../../src/interfaces/IJBCashOutTerminal.sol";
|
|
8
|
+
import {JBConstants} from "../../src/libraries/JBConstants.sol";
|
|
9
|
+
import {JBCashOutHookSpecification} from "../../src/structs/JBCashOutHookSpecification.sol";
|
|
10
|
+
import {JBPayHookSpecification} from "../../src/structs/JBPayHookSpecification.sol";
|
|
11
|
+
import {JBRuleset} from "../../src/structs/JBRuleset.sol";
|
|
12
|
+
|
|
13
|
+
contract TestTerminalPreviewParityFork is TestTerminalPreviewParity_Local {
|
|
14
|
+
uint256 internal constant FORK_BLOCK = 22_000_000;
|
|
15
|
+
|
|
16
|
+
function setUp() public override {
|
|
17
|
+
vm.createSelectFork("ethereum", FORK_BLOCK);
|
|
18
|
+
super.setUp();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function testForkPreviewPayForMatchesPay() external {
|
|
22
|
+
_launchFeeProject();
|
|
23
|
+
uint256 projectId = _launchProject(2500, 10_000);
|
|
24
|
+
|
|
25
|
+
(
|
|
26
|
+
JBRuleset memory ruleset,
|
|
27
|
+
uint256 previewBeneficiaryTokenCount,
|
|
28
|
+
uint256 previewReservedTokenCount,
|
|
29
|
+
JBPayHookSpecification[] memory hookSpecifications
|
|
30
|
+
) = _terminal.previewPayFor(projectId, JBConstants.NATIVE_TOKEN, 1 ether, _beneficiary, "");
|
|
31
|
+
|
|
32
|
+
assertEq(hookSpecifications.length, 0);
|
|
33
|
+
|
|
34
|
+
uint256 balanceBefore = jbTokens().totalBalanceOf(_beneficiary, projectId);
|
|
35
|
+
uint256 reservedBefore = _controller.pendingReservedTokenBalanceOf(projectId);
|
|
36
|
+
|
|
37
|
+
address payer = makeAddr("forkPayer");
|
|
38
|
+
vm.deal(payer, 1 ether);
|
|
39
|
+
|
|
40
|
+
vm.expectEmit();
|
|
41
|
+
emit IJBTerminal.Pay(
|
|
42
|
+
ruleset.id,
|
|
43
|
+
ruleset.cycleNumber,
|
|
44
|
+
projectId,
|
|
45
|
+
payer,
|
|
46
|
+
_beneficiary,
|
|
47
|
+
1 ether,
|
|
48
|
+
previewBeneficiaryTokenCount,
|
|
49
|
+
"",
|
|
50
|
+
"",
|
|
51
|
+
payer
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
vm.prank(payer);
|
|
55
|
+
uint256 beneficiaryTokenCount =
|
|
56
|
+
_terminal.pay{value: 1 ether}(projectId, JBConstants.NATIVE_TOKEN, 1 ether, _beneficiary, 0, "", "");
|
|
57
|
+
|
|
58
|
+
assertEq(beneficiaryTokenCount, previewBeneficiaryTokenCount);
|
|
59
|
+
assertEq(jbTokens().totalBalanceOf(_beneficiary, projectId) - balanceBefore, previewBeneficiaryTokenCount);
|
|
60
|
+
assertEq(_controller.pendingReservedTokenBalanceOf(projectId) - reservedBefore, previewReservedTokenCount);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function testForkPreviewCashOutFromMatchesCashOut() external {
|
|
64
|
+
_launchFeeProject();
|
|
65
|
+
uint256 projectId = _launchProject(0, 5000);
|
|
66
|
+
|
|
67
|
+
vm.prank(_projectOwner);
|
|
68
|
+
jbFeelessAddresses().setFeelessAddress(_beneficiary, true);
|
|
69
|
+
|
|
70
|
+
vm.deal(_beneficiary, 1 ether);
|
|
71
|
+
vm.prank(_beneficiary);
|
|
72
|
+
uint256 minted =
|
|
73
|
+
_terminal.pay{value: 1 ether}(projectId, JBConstants.NATIVE_TOKEN, 1 ether, _beneficiary, 0, "", "");
|
|
74
|
+
|
|
75
|
+
uint256 cashOutCount = minted / 2;
|
|
76
|
+
|
|
77
|
+
(
|
|
78
|
+
JBRuleset memory ruleset,
|
|
79
|
+
uint256 previewReclaimAmount,
|
|
80
|
+
uint256 previewCashOutTaxRate,
|
|
81
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
82
|
+
) = _terminal.previewCashOutFrom(
|
|
83
|
+
_beneficiary, projectId, cashOutCount, JBConstants.NATIVE_TOKEN, payable(_beneficiary), ""
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
assertEq(hookSpecifications.length, 0);
|
|
87
|
+
|
|
88
|
+
vm.expectEmit();
|
|
89
|
+
emit IJBCashOutTerminal.CashOutTokens(
|
|
90
|
+
ruleset.id,
|
|
91
|
+
ruleset.cycleNumber,
|
|
92
|
+
projectId,
|
|
93
|
+
_beneficiary,
|
|
94
|
+
_beneficiary,
|
|
95
|
+
cashOutCount,
|
|
96
|
+
previewCashOutTaxRate,
|
|
97
|
+
previewReclaimAmount,
|
|
98
|
+
"",
|
|
99
|
+
_beneficiary
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
vm.prank(_beneficiary);
|
|
103
|
+
uint256 reclaimAmount = _terminal.cashOutTokensOf(
|
|
104
|
+
_beneficiary, projectId, cashOutCount, JBConstants.NATIVE_TOKEN, 0, payable(_beneficiary), ""
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
assertEq(reclaimAmount, previewReclaimAmount);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {JBController} from "../../../../src/JBController.sol";
|
|
5
|
+
import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
|
|
6
|
+
import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
|
|
7
|
+
import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
|
|
8
|
+
import {JBRulesetMetadataResolver} from "../../../../src/libraries/JBRulesetMetadataResolver.sol";
|
|
9
|
+
import {JBRuleset} from "../../../../src/structs/JBRuleset.sol";
|
|
10
|
+
import {JBRulesetMetadata} from "../../../../src/structs/JBRulesetMetadata.sol";
|
|
11
|
+
import {JBControllerSetup} from "./JBControllerSetup.sol";
|
|
12
|
+
|
|
13
|
+
contract TestPreviewMintOf_Local is JBControllerSetup {
|
|
14
|
+
uint256 _projectId = 1;
|
|
15
|
+
|
|
16
|
+
function setUp() public {
|
|
17
|
+
super.controllerSetup();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function test_RevertsWhenTokenCountIsZero() external {
|
|
21
|
+
vm.expectRevert(JBController.JBController_ZeroTokensToMint.selector);
|
|
22
|
+
_controller.previewMintOf(_projectId, 0, true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function test_ReturnsSplitCountsWhenUsingReservedPercent() external {
|
|
26
|
+
uint256 tokenCount = 1000;
|
|
27
|
+
|
|
28
|
+
JBRulesetMetadata memory metadata = JBRulesetMetadata({
|
|
29
|
+
reservedPercent: 2500,
|
|
30
|
+
cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE,
|
|
31
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
32
|
+
pausePay: false,
|
|
33
|
+
pauseCreditTransfers: false,
|
|
34
|
+
allowOwnerMinting: false,
|
|
35
|
+
allowSetCustomToken: false,
|
|
36
|
+
allowTerminalMigration: false,
|
|
37
|
+
allowSetTerminals: false,
|
|
38
|
+
ownerMustSendPayouts: false,
|
|
39
|
+
allowSetController: false,
|
|
40
|
+
allowAddAccountingContext: true,
|
|
41
|
+
allowAddPriceFeed: false,
|
|
42
|
+
holdFees: false,
|
|
43
|
+
useTotalSurplusForCashOuts: false,
|
|
44
|
+
useDataHookForPay: false,
|
|
45
|
+
useDataHookForCashOut: false,
|
|
46
|
+
dataHook: address(0),
|
|
47
|
+
metadata: 0
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
JBRuleset memory ruleset = JBRuleset({
|
|
51
|
+
cycleNumber: 1,
|
|
52
|
+
id: 1,
|
|
53
|
+
basedOnId: 0,
|
|
54
|
+
start: uint48(block.timestamp),
|
|
55
|
+
duration: 0,
|
|
56
|
+
weight: 0,
|
|
57
|
+
weightCutPercent: 0,
|
|
58
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
59
|
+
metadata: JBRulesetMetadataResolver.packRulesetMetadata(metadata)
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
|
|
63
|
+
|
|
64
|
+
(uint256 beneficiaryTokenCount, uint256 reservedTokenCount) =
|
|
65
|
+
_controller.previewMintOf(_projectId, tokenCount, true);
|
|
66
|
+
|
|
67
|
+
assertEq(beneficiaryTokenCount, 750);
|
|
68
|
+
assertEq(reservedTokenCount, 250);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function test_IgnoresReservedPercentWhenFlagIsFalse() external {
|
|
72
|
+
uint256 tokenCount = 1000;
|
|
73
|
+
|
|
74
|
+
JBRulesetMetadata memory metadata = JBRulesetMetadata({
|
|
75
|
+
reservedPercent: 9000,
|
|
76
|
+
cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE,
|
|
77
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
78
|
+
pausePay: false,
|
|
79
|
+
pauseCreditTransfers: false,
|
|
80
|
+
allowOwnerMinting: false,
|
|
81
|
+
allowSetCustomToken: false,
|
|
82
|
+
allowTerminalMigration: false,
|
|
83
|
+
allowSetTerminals: false,
|
|
84
|
+
ownerMustSendPayouts: false,
|
|
85
|
+
allowSetController: false,
|
|
86
|
+
allowAddAccountingContext: true,
|
|
87
|
+
allowAddPriceFeed: false,
|
|
88
|
+
holdFees: false,
|
|
89
|
+
useTotalSurplusForCashOuts: false,
|
|
90
|
+
useDataHookForPay: false,
|
|
91
|
+
useDataHookForCashOut: false,
|
|
92
|
+
dataHook: address(0),
|
|
93
|
+
metadata: 0
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
JBRuleset memory ruleset = JBRuleset({
|
|
97
|
+
cycleNumber: 1,
|
|
98
|
+
id: 1,
|
|
99
|
+
basedOnId: 0,
|
|
100
|
+
start: uint48(block.timestamp),
|
|
101
|
+
duration: 0,
|
|
102
|
+
weight: 0,
|
|
103
|
+
weightCutPercent: 0,
|
|
104
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
105
|
+
metadata: JBRulesetMetadataResolver.packRulesetMetadata(metadata)
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
mockExpect(address(rulesets), abi.encodeCall(IJBRulesets.currentOf, (_projectId)), abi.encode(ruleset));
|
|
109
|
+
|
|
110
|
+
(uint256 beneficiaryTokenCount, uint256 reservedTokenCount) =
|
|
111
|
+
_controller.previewMintOf(_projectId, tokenCount, false);
|
|
112
|
+
|
|
113
|
+
assertEq(beneficiaryTokenCount, tokenCount);
|
|
114
|
+
assertEq(reservedTokenCount, 0);
|
|
115
|
+
}
|
|
116
|
+
}
|