@bananapus/core-v6 0.0.18 → 0.0.19

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.
@@ -65,6 +65,22 @@ interface IJBTerminalStore {
65
65
  view
66
66
  returns (uint256);
67
67
 
68
+ /// @notice Returns the reclaimable surplus for a project across all terminals using all accounting contexts.
69
+ /// @param projectId The ID of the project.
70
+ /// @param cashOutCount The number of tokens being cashed out.
71
+ /// @param decimals The number of decimals to express the result with.
72
+ /// @param currency The currency to express the result in.
73
+ /// @return The reclaimable surplus amount.
74
+ function currentTotalReclaimableSurplusOf(
75
+ uint256 projectId,
76
+ uint256 cashOutCount,
77
+ uint256 decimals,
78
+ uint256 currency
79
+ )
80
+ external
81
+ view
82
+ returns (uint256);
83
+
68
84
  /// @notice Returns the current surplus for a terminal and project.
69
85
  /// @param terminal The terminal to get the surplus of.
70
86
  /// @param projectId The ID of the project.
@@ -97,6 +113,60 @@ interface IJBTerminalStore {
97
113
  view
98
114
  returns (uint256);
99
115
 
116
+ /// @notice Simulates a cash out without modifying state.
117
+ /// @param terminal The terminal address to simulate the cash out from.
118
+ /// @param holder The address cashing out.
119
+ /// @param projectId The ID of the project being cashed out from.
120
+ /// @param cashOutCount The number of project tokens being cashed out.
121
+ /// @param accountingContext The accounting context of the token being reclaimed.
122
+ /// @param balanceAccountingContexts The accounting contexts to include in the balance calculation.
123
+ /// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address.
124
+ /// @param metadata Extra data to pass along to the data hook.
125
+ /// @return ruleset The project's current ruleset.
126
+ /// @return reclaimAmount The amount that would be reclaimed.
127
+ /// @return cashOutTaxRate The cash out tax rate that would be applied.
128
+ /// @return hookSpecifications Any cash out hook specifications from the data hook.
129
+ function previewCashOutFrom(
130
+ address terminal,
131
+ address holder,
132
+ uint256 projectId,
133
+ uint256 cashOutCount,
134
+ JBAccountingContext calldata accountingContext,
135
+ JBAccountingContext[] calldata balanceAccountingContexts,
136
+ bool beneficiaryIsFeeless,
137
+ bytes calldata metadata
138
+ )
139
+ external
140
+ view
141
+ returns (
142
+ JBRuleset memory ruleset,
143
+ uint256 reclaimAmount,
144
+ uint256 cashOutTaxRate,
145
+ JBCashOutHookSpecification[] memory hookSpecifications
146
+ );
147
+
148
+ /// @notice Simulates a payment without modifying state.
149
+ /// @param terminal The terminal address to simulate the payment from.
150
+ /// @param payer The address of the payer.
151
+ /// @param amount The amount being paid.
152
+ /// @param projectId The ID of the project being paid.
153
+ /// @param beneficiary The address to mint project tokens to.
154
+ /// @param metadata Extra data to pass along to the data hook.
155
+ /// @return ruleset The project's current ruleset.
156
+ /// @return tokenCount The number of project tokens that would be minted.
157
+ /// @return hookSpecifications Any pay hook specifications from the data hook.
158
+ function previewPayFrom(
159
+ address terminal,
160
+ address payer,
161
+ JBTokenAmount memory amount,
162
+ uint256 projectId,
163
+ address beneficiary,
164
+ bytes calldata metadata
165
+ )
166
+ external
167
+ view
168
+ returns (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications);
169
+
100
170
  /// @notice Returns the amount of payout limit used by a terminal for a project in a given cycle.
101
171
  /// @param terminal The terminal to get the used payout limit of.
102
172
  /// @param projectId The ID of the project.
@@ -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
+ }
@@ -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
  }