@bananapus/core-v6 0.0.44 → 0.0.46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.44",
3
+ "version": "0.0.46",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
39
39
  },
40
40
  "dependencies": {
41
- "@bananapus/permission-ids-v6": "^0.0.22",
41
+ "@bananapus/permission-ids-v6": "^0.0.23",
42
42
  "@chainlink/contracts": "1.5.0",
43
43
  "@openzeppelin/contracts": "5.6.1",
44
44
  "@prb/math": "4.1.1",
@@ -79,7 +79,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
79
79
  - `pricePerUnitOf()` is on `IJBPrices`, NOT `IJBController` -- access via `IJBController(ctrl).PRICES().pricePerUnitOf(...)`
80
80
  - `JBRulesetConfig` fields need explicit casts: `uint48 mustStartAtOrAfter`, `uint32 duration`, `uint112 weight`, `uint32 weightCutPercent`
81
81
  - Zero-amount `pay{value:0}()` and zero-count `cashOutTokensOf(count=0)` are valid no-ops (mint/return 0)
82
- - `sendPayoutsOf()` reverts when `amount > payout limit` -- does NOT auto-cap
82
+ - `sendPayoutsOf()` auto-caps `amount` to the remaining payout limit -- does NOT revert. Use `minTokensPaidOut` to enforce a minimum.
83
83
  - `IJBTokens.claimTokensFor()` takes 4 args: `(holder, projectId, count, beneficiary)` -- NOT 3
84
84
  - `JBFeelessAddresses.setFeelessAddress()` NOT `setIsFeelessAddress()` -- the function name omits "Is"
85
85
  - Named returns auto-return (no explicit `return` statement needed in Solidity)
@@ -105,7 +105,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
105
105
  - `IJBDirectoryAccessControl` has `setControllerAllowed()` and `setTerminalsAllowed()` -- NOT `setControllerAllowedFor()`
106
106
  - Price feeds are immutable once set in `JBPrices` -- they cannot be replaced or removed
107
107
  - `JBFundAccessLimits` requires payout limits and surplus allowances to be in strictly increasing currency order to prevent duplicates
108
- - **Empty `fundAccessLimitGroups` = zero payouts, NOT unlimited.** If a ruleset's `fundAccessLimitGroups` array is empty (or has no entry for the terminal/token), `payoutLimitsOf()` returns an empty array → cumulative limit is 0 → `sendPayoutsOf()` reverts on any amount. To allow unlimited payouts, explicitly set a payout limit with `amount: type(uint224).max`.
108
+ - **Empty `fundAccessLimitGroups` = zero payouts, NOT unlimited.** If a ruleset's `fundAccessLimitGroups` array is empty (or has no entry for the terminal/token), `payoutLimitsOf()` returns an empty array → cumulative limit is 0 → `sendPayoutsOf()` caps to 0 and returns 0. To allow unlimited payouts, explicitly set a payout limit with `amount: type(uint224).max`.
109
109
  - **`groupId` (uint256) vs `currency` (uint32) are different types for the same address.** `JBSplitGroup.groupId` is `uint256(uint160(tokenAddress))` while `JBAccountingContext.currency` is `uint32(uint160(tokenAddress))`. These truncate differently — only `NATIVE_TOKEN` (0x000000000000000000000000000000000000EEEe) matches by coincidence. Don't confuse them.
110
110
  - **`JBAccountingContext.currency` is NOT `baseCurrency` — by design.** `baseCurrency` in ruleset metadata uses abstract real-world values (1 = ETH, 2 = USD) so rulesets are portable across chains — `baseCurrency=2` means "issue X tokens per USD" whether on Ethereum, Base, or Arbitrum. `JBAccountingContext.currency` uses token-derived values (`uint32(uint160(tokenAddress))`) because terminals track specific tokens at specific addresses — e.g. NATIVE_TOKEN = 61166, USDC on Ethereum = 909516616, USDC on Base = 3169378579. `JBPrices` mediates between the two: it converts token-derived currencies to/from abstract currencies (e.g. USDC token → USD concept, NATIVE_TOKEN → ETH concept) so that payout limits denominated in USD work correctly regardless of which token the terminal holds. The separation is what makes cross-chain consistency possible: same ruleset, different terminal accounting per chain.
111
111
  - **Don't queue multiple identical rulesets.** A ruleset with a `duration` automatically cycles — no need to queue copies. Queue multiple rulesets only when configuration actually changes between periods (e.g. different weight, splits, or limits).
@@ -166,7 +166,7 @@ Errors an agent is most likely to encounter. All are custom errors (revert with
166
166
  | `JBMultiTerminal_PermitAllowanceNotEnough` | `JBMultiTerminal` | Permit2 allowance insufficient for the payment amount. |
167
167
  | `JBTerminalStore_RulesetPaymentPaused` | `JBTerminalStore` | `pausePay` is set in the current ruleset. |
168
168
  | `JBTerminalStore_RulesetNotFound` | `JBTerminalStore` | No ruleset exists for the project (not launched). |
169
- | `JBTerminalStore_InadequateControllerPayoutLimit` | `JBTerminalStore` | `sendPayoutsOf` amount exceeds the payout limit for this cycle. |
169
+
170
170
  | `JBTerminalStore_InadequateControllerAllowance` | `JBTerminalStore` | `useAllowanceOf` amount exceeds the surplus allowance. |
171
171
  | `JBTerminalStore_InadequateTerminalStoreBalance` | `JBTerminalStore` | Withdrawal exceeds the terminal's recorded balance. |
172
172
  | `JBTerminalStore_InsufficientTokens` | `JBTerminalStore` | Cash out count exceeds the holder's token balance. |
@@ -1815,6 +1815,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1815
1815
  (ruleset, amountPaidOut) =
1816
1816
  STORE.recordPayoutFor({projectId: projectId, token: token, amount: amount, currency: currency});
1817
1817
 
1818
+ // If nothing to pay out (e.g. payout limit already used or not configured), return early.
1819
+ if (amountPaidOut == 0) return amountPaidOut;
1820
+
1818
1821
  // Get a reference to the project's owner.
1819
1822
  // The owner will receive tokens minted by paying the platform fee and receive any leftover funds not sent to
1820
1823
  // payout splits.
@@ -45,7 +45,7 @@ contract JBTerminalStore is IJBTerminalStore {
45
45
  );
46
46
  error JBTerminalStore_AddingAccountingContextNotAllowed(uint256 projectId, uint256 rulesetId, address terminal);
47
47
  error JBTerminalStore_InadequateControllerAllowance(uint256 amount, uint256 allowance);
48
- error JBTerminalStore_InadequateControllerPayoutLimit(uint256 amount, uint256 limit);
48
+
49
49
  error JBTerminalStore_InadequateTerminalStoreBalance(uint256 amount, uint256 balance);
50
50
  error JBTerminalStore_InsufficientTokens(uint256 count, uint256 totalSupply);
51
51
  error JBTerminalStore_InvalidAmountToForwardHook(uint256 amount, uint256 paidAmount);
@@ -392,6 +392,26 @@ contract JBTerminalStore is IJBTerminalStore {
392
392
  // Get a reference to the project's current ruleset.
393
393
  ruleset = RULESETS.currentOf(projectId);
394
394
 
395
+ // Get the payout limit for this currency.
396
+ uint256 payoutLimit = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
397
+ .payoutLimitOf({
398
+ projectId: projectId, rulesetId: ruleset.id, terminal: msg.sender, token: token, currency: currency
399
+ });
400
+
401
+ // Get the already-used payout limit for this cycle.
402
+ uint256 usedPayoutLimit = usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency];
403
+
404
+ // Cap the amount to the remaining payout limit instead of reverting.
405
+ uint256 remainingPayoutLimit = payoutLimit > usedPayoutLimit ? payoutLimit - usedPayoutLimit : 0;
406
+ if (amount > remainingPayoutLimit) {
407
+ amount = remainingPayoutLimit;
408
+ }
409
+
410
+ // If nothing can be paid out, return early with zero.
411
+ if (amount == 0) {
412
+ return (ruleset, 0);
413
+ }
414
+
395
415
  // Convert the amount to the balance's currency.
396
416
  amountPaidOut = (currency == accountingContext.currency)
397
417
  ? amount
@@ -415,29 +435,13 @@ contract JBTerminalStore is IJBTerminalStore {
415
435
  revert JBTerminalStore_InadequateTerminalStoreBalance({amount: amountPaidOut, balance: currentBalance});
416
436
  }
417
437
 
418
- // The new total amount which has been paid out during this ruleset.
419
- uint256 newUsedPayoutLimitOf =
420
- usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] + amount;
421
-
422
- // Amount must be within what is still available to pay out.
423
- // Validate BEFORE writing to storage to avoid wasting gas on SSTORE when the tx will revert.
424
- uint256 payoutLimit = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
425
- .payoutLimitOf({
426
- projectId: projectId, rulesetId: ruleset.id, terminal: msg.sender, token: token, currency: currency
427
- });
428
-
429
- // Make sure the new used amount is within the payout limit.
430
- if (newUsedPayoutLimitOf > payoutLimit || payoutLimit == 0) {
431
- revert JBTerminalStore_InadequateControllerPayoutLimit({amount: newUsedPayoutLimitOf, limit: payoutLimit});
432
- }
433
-
434
438
  // Removed the paid out funds from the project's token balance.
435
439
  unchecked {
436
440
  balanceOf[msg.sender][projectId][token] = currentBalance - amountPaidOut;
437
441
  }
438
442
 
439
443
  // Store the new used payout limit.
440
- usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] = newUsedPayoutLimitOf;
444
+ usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] = usedPayoutLimit + amount;
441
445
  }
442
446
 
443
447
  /// @notice Records a terminal migration — zeros out the project's balance and returns the amount moved to
@@ -11,13 +11,8 @@ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Recei
11
11
  /// @dev Attempts to re-enter sendPayoutsOf after receiving control-flow.
12
12
  contract MaliciousPayoutBeneficiary is IERC721Receiver, Test {
13
13
  function reEnter(address _terminal) internal {
14
- vm.expectRevert(
15
- abi.encodeWithSelector(
16
- JBTerminalStore.JBTerminalStore_InadequateControllerPayoutLimit.selector, 1.5e19, 10 * 10 ** 18
17
- )
18
- );
19
-
20
- IJBMultiTerminal(_terminal)
14
+ // Reentrancy attempt — payout limit already used, so the cap returns 0 (no funds extracted).
15
+ uint256 _reentrantPayout = IJBMultiTerminal(_terminal)
21
16
  .sendPayoutsOf({
22
17
  projectId: 2,
23
18
  amount: 5 * 10 ** 18,
@@ -25,6 +20,7 @@ contract MaliciousPayoutBeneficiary is IERC721Receiver, Test {
25
20
  token: JBConstants.NATIVE_TOKEN,
26
21
  minTokensPaidOut: 0
27
22
  });
23
+ assertEq(_reentrantPayout, 0);
28
24
  }
29
25
 
30
26
  receive() external payable {