@bananapus/core-v6 0.0.42 → 0.0.43

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/CHANGELOG.md CHANGED
@@ -19,7 +19,7 @@ This file describes the verified change from `nana-core-v5` to the current `nana
19
19
  - Token metadata is more editable than in v5. The controller now exposes a dedicated token-metadata update path.
20
20
  - Approval-hook handling is safer. The v6 codebase and tests are built around preventing a bad approval hook from freezing project behavior.
21
21
  - Fee accounting is tighter than in v5, especially around fee-free surplus behavior and cross-flow bookkeeping.
22
- - The repo carries much broader test coverage than the v5 tree, including dedicated audit, invariant, fork, and formal-style suites.
22
+ - The repo carries much broader test coverage than the v5 tree, including dedicated review, invariant, fork, and formal-style suites.
23
23
  - The implementation baseline moved from the v5 `0.8.23` world to `0.8.28`.
24
24
 
25
25
  ## Verified deltas
package/README.md CHANGED
@@ -89,7 +89,7 @@ When a flow is unclear, read the contract that owns the state before the contrac
89
89
  2. `test/TestTerminalPreviewParity.sol`
90
90
  3. `test/invariants/TerminalStoreInvariant.t.sol`
91
91
  4. `test/invariants/RulesetsInvariant.t.sol`
92
- 5. `test/audit/CrossTerminalSurplusSpoof.t.sol`
92
+ 5. `test/regression/CrossTerminalSurplusSpoof.t.sol`
93
93
 
94
94
  ## Install
95
95
 
@@ -123,7 +123,7 @@ This repo contains the main core deployments and periphery deployment helpers. M
123
123
  src/
124
124
  core contracts, periphery helpers, interfaces, libraries, enums, structs, and abstract bases
125
125
  test/
126
- unit, integration, fork, invariant, audit, formal, and regression coverage
126
+ unit, integration, fork, invariant, review, formal, and regression coverage
127
127
  script/
128
128
  Deploy.s.sol
129
129
  DeployPeriphery.s.sol
@@ -135,7 +135,7 @@ script/
135
135
  - Hooks can materially change payment and cash-out behavior.
136
136
  - Permissions are flexible, which makes broad or wildcard grants risky.
137
137
  - Multi-terminal and multi-token accounting is powerful, but it is easy to misuse if an integration assumes a single-terminal model.
138
- - Fee, surplus, and reclaim logic stay high-priority audit areas.
138
+ - Fee, surplus, and reclaim logic stay high-priority review areas.
139
139
 
140
140
  The easiest way to misread V6 is to treat core like a simple crowdfunding terminal. It is closer to a configurable accounting and settlement layer.
141
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -182,7 +182,6 @@ Errors an agent is most likely to encounter. All are custom errors (revert with
182
182
  | `JBSplits_TotalPercentExceeds100` | `JBSplits` | Split percentages sum exceeds `SPLITS_TOTAL_PERCENT`. |
183
183
  | `JBPrices_PriceFeedAlreadyExists` | `JBPrices` | Feed already set for that currency pair (immutable). |
184
184
  | `JBPrices_PriceFeedNotFound` | `JBPrices` | No feed found for the requested currency pair. |
185
- | `JBPermissions_CantSetRootPermissionForWildcardProject` | `JBPermissions` | Tried to grant ROOT with `projectId = 0` (wildcard). |
186
185
  | `JBRulesets_InvalidWeight` | `JBRulesets` | Weight exceeds `uint112.max`. |
187
186
  | `JBRulesets_InvalidWeightCutPercent` | `JBRulesets` | `weightCutPercent` exceeds `MAX_WEIGHT_CUT_PERCENT`. |
188
187
  | `JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering` | `JBFundAccessLimits` | Payout limit currencies not in strictly increasing order. |
@@ -17,7 +17,7 @@ contract JBChainlinkV3PriceFeed is IJBPriceFeed {
17
17
  // --------------------------- custom errors ------------------------- //
18
18
  //*********************************************************************//
19
19
 
20
- error JBChainlinkV3PriceFeed_IncompleteRound();
20
+ error JBChainlinkV3PriceFeed_IncompleteRound(uint80 roundId, uint80 answeredInRound, uint256 updatedAt);
21
21
  error JBChainlinkV3PriceFeed_NegativePrice(int256 price);
22
22
  error JBChainlinkV3PriceFeed_StalePrice(uint256 timestamp, uint256 threshold, uint256 updatedAt);
23
23
 
@@ -51,20 +51,28 @@ contract JBChainlinkV3PriceFeed is IJBPriceFeed {
51
51
  /// @return The current unit price from the feed, as a fixed point number with the specified number of decimals.
52
52
  function currentUnitPrice(uint256 decimals) public view virtual override returns (uint256) {
53
53
  // Get the latest round information from the feed.
54
- // slither-disable-next-line unused-return
55
54
  (uint80 roundId, int256 price,, uint256 updatedAt, uint80 answeredInRound) = FEED.latestRoundData();
56
55
 
57
56
  // Make sure the round is finished (check before stale price to avoid false stale on incomplete rounds).
58
- // slither-disable-next-line incorrect-equality
59
- if (updatedAt == 0) revert JBChainlinkV3PriceFeed_IncompleteRound();
57
+ if (updatedAt == 0) {
58
+ revert JBChainlinkV3PriceFeed_IncompleteRound({
59
+ roundId: roundId, answeredInRound: answeredInRound, updatedAt: updatedAt
60
+ });
61
+ }
60
62
 
61
63
  // Make sure the answer was provided in the current round.
62
- if (answeredInRound < roundId) revert JBChainlinkV3PriceFeed_IncompleteRound();
64
+ if (answeredInRound < roundId) {
65
+ revert JBChainlinkV3PriceFeed_IncompleteRound({
66
+ roundId: roundId, answeredInRound: answeredInRound, updatedAt: updatedAt
67
+ });
68
+ }
63
69
 
64
70
  // Make sure the price's update threshold is met.
65
71
  // forge-lint: disable-next-line(block-timestamp)
66
72
  if (block.timestamp > THRESHOLD + updatedAt) {
67
- revert JBChainlinkV3PriceFeed_StalePrice(block.timestamp, THRESHOLD, updatedAt);
73
+ revert JBChainlinkV3PriceFeed_StalePrice({
74
+ timestamp: block.timestamp, threshold: THRESHOLD, updatedAt: updatedAt
75
+ });
68
76
  }
69
77
 
70
78
  // Make sure the price is positive.
@@ -14,7 +14,7 @@ contract JBChainlinkV3SequencerPriceFeed is JBChainlinkV3PriceFeed {
14
14
  // --------------------------- custom errors ------------------------- //
15
15
  //*********************************************************************//
16
16
 
17
- error JBChainlinkV3SequencerPriceFeed_InvalidRound();
17
+ error JBChainlinkV3SequencerPriceFeed_InvalidRound(uint256 startedAt);
18
18
  error JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting(
19
19
  uint256 timestamp, uint256 gracePeriodTime, uint256 startedAt
20
20
  );
@@ -58,18 +58,17 @@ contract JBChainlinkV3SequencerPriceFeed is JBChainlinkV3PriceFeed {
58
58
  /// @return The current unit price from the feed, as a fixed point number with the specified number of decimals.
59
59
  function currentUnitPrice(uint256 decimals) public view override returns (uint256) {
60
60
  // Fetch sequencer status.
61
- // slither-disable-next-line unused-return
62
61
  (, int256 answer, uint256 startedAt,,) = SEQUENCER_FEED.latestRoundData();
63
62
 
64
63
  // Check if round is valid to prevent an edge-case where Arbitrum uptime contract is not init.
65
- if (startedAt == 0) revert JBChainlinkV3SequencerPriceFeed_InvalidRound();
64
+ if (startedAt == 0) revert JBChainlinkV3SequencerPriceFeed_InvalidRound({startedAt: startedAt});
66
65
 
67
66
  // Revert if sequencer has too recently restarted or is currently down.
68
67
  // forge-lint: disable-next-line(block-timestamp)
69
68
  if (block.timestamp <= GRACE_PERIOD_TIME + startedAt || answer != 0) {
70
- revert JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting(
71
- block.timestamp, GRACE_PERIOD_TIME, startedAt
72
- );
69
+ revert JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting({
70
+ timestamp: block.timestamp, gracePeriodTime: GRACE_PERIOD_TIME, startedAt: startedAt
71
+ });
73
72
  }
74
73
 
75
74
  return super.currentUnitPrice(decimals);
@@ -60,19 +60,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
60
60
  //*********************************************************************//
61
61
 
62
62
  error JBController_AddingPriceFeedNotAllowed(uint256 projectId);
63
- error JBController_CreditTransfersPaused();
63
+ error JBController_CreditTransfersPaused(uint256 projectId, uint256 rulesetId);
64
64
  error JBController_InvalidCashOutTaxRate(uint256 rate, uint256 limit);
65
65
  error JBController_InvalidReservedPercent(uint256 percent, uint256 limit);
66
66
  error JBController_MintNotAllowedAndNotTerminalOrHook(address caller);
67
- error JBController_NoReservedTokens();
67
+ error JBController_NoReservedTokens(uint256 projectId);
68
68
  error JBController_OnlyDirectory(address sender, IJBDirectory directory);
69
69
  error JBController_PendingReservedTokens(uint256 pendingReservedTokenBalance);
70
70
  error JBController_RulesetsAlreadyLaunched(uint256 projectId);
71
- error JBController_RulesetsArrayEmpty();
71
+ error JBController_RulesetsArrayEmpty(uint256 projectId, uint256 rulesetConfigurationCount);
72
72
  error JBController_RulesetSetTokenNotAllowed(uint256 projectId);
73
- error JBController_TerminalTokensNotTransferred();
74
- error JBController_ZeroTokensToBurn();
75
- error JBController_ZeroTokensToMint();
73
+ error JBController_TerminalTokensNotTransferred(address terminal, address token, uint256 allowance);
74
+ error JBController_ZeroTokensToBurn(uint256 projectId, address holder);
75
+ error JBController_ZeroTokensToMint(uint256 projectId, address beneficiary);
76
76
 
77
77
  //*********************************************************************//
78
78
  // --------------- public immutable stored properties ---------------- //
@@ -163,7 +163,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
163
163
  RULESETS = rulesets;
164
164
  SPLITS = splits;
165
165
  TOKENS = tokens;
166
- // slither-disable-next-line missing-zero-check
167
166
  OMNICHAIN_RULESET_OPERATOR = omnichainRulesetOperator;
168
167
  }
169
168
 
@@ -235,7 +234,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
235
234
  from.supportsInterface(type(IJBController).interfaceId)
236
235
  && IJBController(address(from)).pendingReservedTokenBalanceOf(projectId) > 0
237
236
  ) {
238
- // slither-disable-next-line unused-return
239
237
  IJBController(address(from)).sendReservedTokensToSplitsOf(projectId);
240
238
  }
241
239
  }
@@ -261,11 +259,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
261
259
  account: holder,
262
260
  projectId: projectId,
263
261
  permissionId: JBPermissionIds.BURN_TOKENS,
264
- alsoGrantAccessIf: _isTerminalOf(projectId, _msgSender())
262
+ alsoGrantAccessIf: _isTerminalOf({projectId: projectId, terminal: _msgSender()})
265
263
  });
266
264
 
267
265
  // There must be tokens to burn.
268
- if (tokenCount == 0) revert JBController_ZeroTokensToBurn();
266
+ if (tokenCount == 0) revert JBController_ZeroTokensToBurn({projectId: projectId, holder: holder});
269
267
 
270
268
  emit BurnTokens({
271
269
  holder: holder, projectId: projectId, tokenCount: tokenCount, memo: memo, caller: _msgSender()
@@ -359,7 +357,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
359
357
  // Approve the tokens being paid.
360
358
  IERC20(address(token)).forceApprove({spender: address(terminal), value: splitTokenCount});
361
359
 
362
- // slither-disable-next-line unused-return
363
360
  terminal.pay({
364
361
  projectId: projectId,
365
362
  token: address(token),
@@ -371,8 +368,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
371
368
  });
372
369
 
373
370
  // Make sure that the terminal received the tokens.
374
- if (IERC20(address(token)).allowance({owner: address(this), spender: address(terminal)}) != 0) {
375
- revert JBController_TerminalTokensNotTransferred();
371
+ uint256 allowance = IERC20(address(token)).allowance({owner: address(this), spender: address(terminal)});
372
+ if (allowance != 0) {
373
+ revert JBController_TerminalTokensNotTransferred({
374
+ terminal: address(terminal), token: address(token), allowance: allowance
375
+ });
376
376
  }
377
377
  }
378
378
 
@@ -400,7 +400,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
400
400
  returns (uint256 projectId)
401
401
  {
402
402
  // Mint the project ERC-721 into the owner's wallet.
403
- // slither-disable-next-line reentrancy-benign
404
403
  projectId = PROJECTS.createFor(owner);
405
404
 
406
405
  // If provided, set the project's metadata URI.
@@ -415,7 +414,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
415
414
  _configureTerminals({projectId: projectId, terminalConfigurations: terminalConfigurations});
416
415
 
417
416
  // Queue the rulesets.
418
- // slither-disable-next-line reentrancy-events
419
417
  uint256 rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
420
418
 
421
419
  emit LaunchProject({
@@ -445,7 +443,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
445
443
  returns (uint256 rulesetId)
446
444
  {
447
445
  // Make sure there are rulesets being queued.
448
- if (rulesetConfigurations.length == 0) revert JBController_RulesetsArrayEmpty();
446
+ if (rulesetConfigurations.length == 0) {
447
+ revert JBController_RulesetsArrayEmpty({
448
+ projectId: projectId, rulesetConfigurationCount: rulesetConfigurations.length
449
+ });
450
+ }
449
451
 
450
452
  // Keep a reference to the sender.
451
453
  address sender = _msgSender();
@@ -477,7 +479,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
477
479
 
478
480
  // If the project has already had rulesets, use `queueRulesetsOf(...)` instead.
479
481
  if (RULESETS.latestRulesetIdOf(projectId) > 0) {
480
- revert JBController_RulesetsAlreadyLaunched(projectId);
482
+ revert JBController_RulesetsAlreadyLaunched({projectId: projectId});
481
483
  }
482
484
 
483
485
  // If provided, set the project's metadata URI.
@@ -492,7 +494,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
492
494
  _configureTerminals({projectId: projectId, terminalConfigurations: terminalConfigurations});
493
495
 
494
496
  // Queue the first ruleset.
495
- // slither-disable-next-line reentrancy-events
496
497
  rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
497
498
 
498
499
  emit LaunchRulesets({
@@ -539,7 +540,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
539
540
  returns (uint256 beneficiaryTokenCount)
540
541
  {
541
542
  // There should be tokens to mint.
542
- if (tokenCount == 0) revert JBController_ZeroTokensToMint();
543
+ if (tokenCount == 0) revert JBController_ZeroTokensToMint({projectId: projectId, beneficiary: beneficiary});
543
544
 
544
545
  // Keep a reference to the reserved percent.
545
546
  uint256 reservedPercent;
@@ -549,11 +550,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
549
550
 
550
551
  // Cache common values used in both permission checks.
551
552
  address sender = _msgSender();
552
- bool senderIsTerminal = _isTerminalOf(projectId, sender);
553
+ bool senderIsTerminal = _isTerminalOf({projectId: projectId, terminal: sender});
553
554
  bool senderIsTerminalOrDataHook = senderIsTerminal || sender == ruleset.dataHook();
554
555
  // Only query the data hook if the sender isn't already a terminal or the data hook itself.
555
- bool senderHasDataHookMintPermission =
556
- !senderIsTerminalOrDataHook && _hasDataHookMintPermissionFor(projectId, ruleset, sender);
556
+ bool senderHasDataHookMintPermission = !senderIsTerminalOrDataHook
557
+ && _hasDataHookMintPermissionFor({projectId: projectId, ruleset: ruleset, addr: sender});
557
558
 
558
559
  // Minting is restricted to: the project's owner, addresses with permission to `MINT_TOKENS`, the project's
559
560
  // terminals, and the project's data hook.
@@ -570,7 +571,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
570
571
  ruleset.id != 0 && !ruleset.allowOwnerMinting() && !senderIsTerminalOrDataHook
571
572
  && !senderHasDataHookMintPermission
572
573
  ) {
573
- revert JBController_MintNotAllowedAndNotTerminalOrHook(sender);
574
+ revert JBController_MintNotAllowedAndNotTerminalOrHook({caller: sender});
574
575
  }
575
576
 
576
577
  // Determine the reserved percent to use.
@@ -581,7 +582,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
581
582
 
582
583
  if (beneficiaryTokenCount != 0) {
583
584
  // Mint the tokens.
584
- // slither-disable-next-line reentrancy-benign,reentrancy-events,unused-return
585
585
  TOKENS.mintFor({holder: beneficiary, projectId: projectId, count: beneficiaryTokenCount});
586
586
  }
587
587
 
@@ -618,7 +618,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
618
618
  returns (uint256 rulesetId)
619
619
  {
620
620
  // Make sure there are rulesets being queued.
621
- if (rulesetConfigurations.length == 0) revert JBController_RulesetsArrayEmpty();
621
+ if (rulesetConfigurations.length == 0) {
622
+ revert JBController_RulesetsArrayEmpty({
623
+ projectId: projectId, rulesetConfigurationCount: rulesetConfigurations.length
624
+ });
625
+ }
622
626
 
623
627
  // Enforce permissions.
624
628
  _requirePermissionAllowingOverrideFrom({
@@ -629,7 +633,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
629
633
  });
630
634
 
631
635
  // Queue the rulesets.
632
- // slither-disable-next-line reentrancy-events
633
636
  rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
634
637
 
635
638
  emit QueueRulesets({rulesetId: rulesetId, projectId: projectId, memo: memo, caller: _msgSender()});
@@ -748,7 +751,9 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
748
751
  JBRuleset memory ruleset = _currentRulesetOf(projectId);
749
752
 
750
753
  // Credit transfers must not be paused.
751
- if (ruleset.pauseCreditTransfers()) revert JBController_CreditTransfersPaused();
754
+ if (ruleset.pauseCreditTransfers()) {
755
+ revert JBController_CreditTransfersPaused({projectId: projectId, rulesetId: ruleset.id});
756
+ }
752
757
 
753
758
  TOKENS.transferCreditsFrom({holder: holder, projectId: projectId, recipient: recipient, count: creditCount});
754
759
  }
@@ -939,7 +944,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
939
944
  JBTerminalConfig memory terminalConfig = terminalConfigurations[i];
940
945
 
941
946
  // Add the accounting contexts for the specified tokens.
942
- // slither-disable-next-line calls-loop
943
947
  terminalConfig.terminal
944
948
  .addAccountingContextsFor({
945
949
  projectId: projectId, accountingContexts: terminalConfig.accountingContextsToAccept
@@ -975,20 +979,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
975
979
 
976
980
  // Make sure its reserved percent is valid.
977
981
  if (rulesetConfig.metadata.reservedPercent > JBConstants.MAX_RESERVED_PERCENT) {
978
- revert JBController_InvalidReservedPercent(
979
- rulesetConfig.metadata.reservedPercent, JBConstants.MAX_RESERVED_PERCENT
980
- );
982
+ revert JBController_InvalidReservedPercent({
983
+ percent: rulesetConfig.metadata.reservedPercent, limit: JBConstants.MAX_RESERVED_PERCENT
984
+ });
981
985
  }
982
986
 
983
987
  // Make sure its cash out tax rate is valid.
984
988
  if (rulesetConfig.metadata.cashOutTaxRate > JBConstants.MAX_CASH_OUT_TAX_RATE) {
985
- revert JBController_InvalidCashOutTaxRate(
986
- rulesetConfig.metadata.cashOutTaxRate, JBConstants.MAX_CASH_OUT_TAX_RATE
987
- );
989
+ revert JBController_InvalidCashOutTaxRate({
990
+ rate: rulesetConfig.metadata.cashOutTaxRate, limit: JBConstants.MAX_CASH_OUT_TAX_RATE
991
+ });
988
992
  }
989
993
 
990
994
  // Queue its ruleset.
991
- // slither-disable-next-line calls-loop
992
995
  JBRuleset memory ruleset = RULESETS.queueFor({
993
996
  projectId: projectId,
994
997
  duration: rulesetConfig.duration,
@@ -1000,13 +1003,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1000
1003
  });
1001
1004
 
1002
1005
  // Set its split groups.
1003
- // slither-disable-next-line calls-loop
1004
1006
  SPLITS.setSplitGroupsOf({
1005
1007
  projectId: projectId, rulesetId: ruleset.id, splitGroups: rulesetConfig.splitGroups
1006
1008
  });
1007
1009
 
1008
1010
  // Set its fund access limits.
1009
- // slither-disable-next-line calls-loop
1010
1011
  FUND_ACCESS_LIMITS.setFundAccessLimitsFor({
1011
1012
  projectId: projectId, rulesetId: ruleset.id, fundAccessLimitGroups: rulesetConfig.fundAccessLimitGroups
1012
1013
  });
@@ -1069,13 +1070,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1069
1070
 
1070
1071
  // If the split has a hook, call its `processSplitWith` function.
1071
1072
  if (split.hook != IJBSplitHook(address(0))) {
1072
- // Send the tokens to the split hook.
1073
- // slither-disable-next-line reentrancy-events
1074
- _sendTokens({
1075
- projectId: projectId, tokenCount: splitTokenCount, recipient: address(split.hook), token: token
1076
- });
1073
+ if (token != IJBToken(address(0))) {
1074
+ // ERC-20: grant the hook an allowance and let it pull tokens via transferFrom.
1075
+ IERC20(address(token)).forceApprove({spender: address(split.hook), value: splitTokenCount});
1076
+ } else {
1077
+ // Credits: send directly — there is no allowance mechanism for credits.
1078
+ TOKENS.transferCreditsFrom({
1079
+ holder: address(this),
1080
+ projectId: projectId,
1081
+ recipient: address(split.hook),
1082
+ count: splitTokenCount
1083
+ });
1084
+ }
1077
1085
 
1078
- // slither-disable-next-line calls-loop,reentrancy-events
1079
1086
  try split.hook
1080
1087
  .processSplitWith(
1081
1088
  JBSplitHookContext({
@@ -1088,9 +1095,18 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1088
1095
  })
1089
1096
  ) {}
1090
1097
  catch (bytes memory reason) {
1091
- // If the hook reverts, the tokens already transferred to it stay with the hook.
1092
1098
  emit SplitHookReverted({projectId: projectId, hook: address(split.hook), reason: reason});
1093
1099
  }
1100
+
1101
+ // For ERC-20 tokens, burn any tokens the hook did not consume.
1102
+ if (token != IJBToken(address(0))) {
1103
+ uint256 remaining =
1104
+ IERC20(address(token)).allowance({owner: address(this), spender: address(split.hook)});
1105
+ if (remaining > 0) {
1106
+ IERC20(address(token)).forceApprove({spender: address(split.hook), value: 0});
1107
+ TOKENS.burnFrom({holder: address(this), projectId: projectId, count: remaining});
1108
+ }
1109
+ }
1094
1110
  // If the split has a project ID, try to pay the project. If that fails, pay the beneficiary.
1095
1111
  } else {
1096
1112
  // Pay the project using the split's beneficiary if one was provided. Otherwise, use the message
@@ -1099,7 +1115,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1099
1115
 
1100
1116
  if (split.projectId != 0) {
1101
1117
  // Get a reference to the receiving project's primary payment terminal for the token.
1102
- // slither-disable-next-line calls-loop
1103
1118
  IJBTerminal terminal = token == IJBToken(address(0))
1104
1119
  ? IJBTerminal(address(0))
1105
1120
  : DIRECTORY.primaryTerminalOf({projectId: split.projectId, token: address(token)});
@@ -1108,17 +1123,14 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1108
1123
  // which accepts the token, send the tokens to the beneficiary.
1109
1124
  if (address(token) == address(0) || address(terminal) == address(0)) {
1110
1125
  // Mint the tokens to the beneficiary.
1111
- // slither-disable-next-line reentrancy-events
1112
1126
  _sendTokens({
1113
1127
  projectId: projectId, tokenCount: splitTokenCount, recipient: beneficiary, token: token
1114
1128
  });
1115
1129
  } else {
1116
1130
  // Use the `projectId` in the pay metadata.
1117
- // slither-disable-next-line reentrancy-events
1118
1131
  bytes memory metadata = bytes(abi.encodePacked(projectId));
1119
1132
 
1120
1133
  // Try to fulfill the payment.
1121
- // slither-disable-next-line calls-loop
1122
1134
  try this.executePayReservedTokenToTerminal({
1123
1135
  projectId: split.projectId,
1124
1136
  terminal: terminal,
@@ -1142,7 +1154,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1142
1154
  }
1143
1155
  } else if (beneficiary == address(0xdead)) {
1144
1156
  // If the split has no project ID, and the beneficiary is 0xdead, burn.
1145
- // slither-disable-next-line calls-loop
1146
1157
  TOKENS.burnFrom({holder: address(this), projectId: projectId, count: splitTokenCount});
1147
1158
  } else {
1148
1159
  // If the split has no project Id, send to beneficiary.
@@ -1180,7 +1191,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1180
1191
  tokenCount = pendingReservedTokenBalanceOf[projectId];
1181
1192
 
1182
1193
  // Revert if there are no pending reserved tokens
1183
- if (tokenCount == 0) revert JBController_NoReservedTokens();
1194
+ if (tokenCount == 0) revert JBController_NoReservedTokens({projectId: projectId});
1184
1195
 
1185
1196
  // Get the ruleset to read the reserved percent from.
1186
1197
  JBRuleset memory ruleset = _currentRulesetOf(projectId);
@@ -121,7 +121,7 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
121
121
 
122
122
  // If setting the controller is not allowed, revert.
123
123
  if (!allowSetController) {
124
- revert JBDirectory_SetControllerNotAllowed(projectId);
124
+ revert JBDirectory_SetControllerNotAllowed({projectId: projectId});
125
125
  }
126
126
 
127
127
  // Prepare the new controller to receive the project.
@@ -140,10 +140,8 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
140
140
  }
141
141
 
142
142
  // Set the new controller after migration completes.
143
- // slither-disable-next-line reentrancy-no-eth
144
143
  controllerOf[projectId] = controller;
145
144
 
146
- // slither-disable-next-line reentrancy-events
147
145
  emit SetController({projectId: projectId, controller: controller, caller: msg.sender});
148
146
 
149
147
  // Notify the new controller that migration is complete and it is now the active controller.
@@ -185,7 +183,7 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
185
183
 
186
184
  // If the terminal doesn't accept the token, revert.
187
185
  if (terminal.accountingContextForTokenOf({projectId: projectId, token: token}).token == address(0)) {
188
- revert JBDirectory_TokenNotAccepted(projectId, token, terminal);
186
+ revert JBDirectory_TokenNotAccepted({projectId: projectId, token: token, terminal: terminal});
189
187
  }
190
188
 
191
189
  // If the terminal is not already in the project's terminal list, require ADD_TERMINALS permission.
@@ -229,7 +227,7 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
229
227
 
230
228
  // If the caller is not the project's controller, the project's ruleset must allow setting terminals.
231
229
  if (msg.sender != address(controller) && !allowSetTerminals) {
232
- revert JBDirectory_SetTerminalsNotAllowed(projectId);
230
+ revert JBDirectory_SetTerminalsNotAllowed({projectId: projectId});
233
231
  }
234
232
 
235
233
  // Set the stored terminals for the project.
@@ -286,7 +284,6 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
286
284
  IJBTerminal terminal = terminals[i];
287
285
 
288
286
  // If the terminal accepts the specified token, return it.
289
- // slither-disable-next-line calls-loop
290
287
  if (terminal.accountingContextForTokenOf({projectId: projectId, token: token}).token != address(0)) {
291
288
  return terminal;
292
289
  }
@@ -355,7 +352,7 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
355
352
 
356
353
  // The project's ruleset must allow setting terminals.
357
354
  if (!allowSetTerminals) {
358
- revert JBDirectory_SetTerminalsNotAllowed(projectId);
355
+ revert JBDirectory_SetTerminalsNotAllowed({projectId: projectId});
359
356
  }
360
357
 
361
358
  // Add the new terminal.
package/src/JBERC20.sol CHANGED
@@ -25,8 +25,8 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
25
25
  // --------------------------- custom errors ------------------------- //
26
26
  //*********************************************************************//
27
27
 
28
- error JBERC20_AlreadyInitialized();
29
- error JBERC20_Unauthorized();
28
+ error JBERC20_AlreadyInitialized(uint256 currentNameLength, uint256 newNameLength);
29
+ error JBERC20_Unauthorized(address caller, address tokens);
30
30
 
31
31
  //*********************************************************************//
32
32
  // --------------- public immutable stored properties ---------------- //
@@ -48,11 +48,9 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
48
48
  //*********************************************************************//
49
49
 
50
50
  /// @notice The token's name.
51
- // slither-disable-next-line shadowing-state
52
51
  string private _name;
53
52
 
54
53
  /// @notice The token's symbol.
55
- // slither-disable-next-line shadowing-state
56
54
  string private _symbol;
57
55
 
58
56
  //*********************************************************************//
@@ -82,7 +80,7 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
82
80
  /// @notice Only the JBTokens contract can call this function.
83
81
  // forge-lint: disable-next-line(unwrapped-modifier-logic)
84
82
  modifier onlyTokens() {
85
- if (msg.sender != address(TOKENS)) revert JBERC20_Unauthorized();
83
+ if (msg.sender != address(TOKENS)) revert JBERC20_Unauthorized({caller: msg.sender, tokens: address(TOKENS)});
86
84
  _;
87
85
  }
88
86
 
@@ -132,7 +130,6 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
132
130
  /// @return magicValue `0x1626ba7e` if the signature is valid, `0xffffffff` otherwise.
133
131
  function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4 magicValue) {
134
132
  // Recover the signer from the signature. Return invalid if recovery fails.
135
- // slither-disable-next-line unused-return
136
133
  (address signer, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signature);
137
134
  if (error != ECDSA.RecoverError.NoError) return 0xffffffff;
138
135
 
@@ -201,7 +198,11 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
201
198
  /// @param tokens The JBTokens contract that manages this token.
202
199
  function initialize(string memory name_, string memory symbol_, address tokens) public override {
203
200
  // Prevent re-initialization by reverting if a name is already set or if the provided name is empty.
204
- if (bytes(_name).length != 0 || bytes(name_).length == 0) revert JBERC20_AlreadyInitialized();
201
+ if (bytes(_name).length != 0 || bytes(name_).length == 0) {
202
+ revert JBERC20_AlreadyInitialized({
203
+ currentNameLength: bytes(_name).length, newNameLength: bytes(name_).length
204
+ });
205
+ }
205
206
 
206
207
  _name = name_;
207
208
  _symbol = symbol_;
@@ -18,8 +18,12 @@ contract JBFundAccessLimits is JBControlled, IJBFundAccessLimits {
18
18
  // --------------------------- custom errors ------------------------- //
19
19
  //*********************************************************************//
20
20
 
21
- error JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering();
22
- error JBFundAccessLimits_InvalidSurplusAllowanceCurrencyOrdering();
21
+ error JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering(
22
+ uint256 projectId, uint256 rulesetId, uint256 groupIndex, uint256 limitIndex
23
+ );
24
+ error JBFundAccessLimits_InvalidSurplusAllowanceCurrencyOrdering(
25
+ uint256 projectId, uint256 rulesetId, uint256 groupIndex, uint256 allowanceIndex
26
+ );
23
27
 
24
28
  //*********************************************************************//
25
29
  // --------------------- internal stored properties ------------------ //
@@ -102,7 +106,9 @@ contract JBFundAccessLimits is JBControlled, IJBFundAccessLimits {
102
106
  // Make sure the payout limits are passed in strictly increasing order (sorted by currency) to prevent
103
107
  // duplicates.
104
108
  if (j != 0 && payoutLimit.currency <= fundAccessLimitGroup.payoutLimits[j - 1].currency) {
105
- revert JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering();
109
+ revert JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering({
110
+ projectId: projectId, rulesetId: rulesetId, groupIndex: i, limitIndex: j
111
+ });
106
112
  }
107
113
 
108
114
  // Set the payout limit if there is one.
@@ -127,7 +133,9 @@ contract JBFundAccessLimits is JBControlled, IJBFundAccessLimits {
127
133
  // Make sure the surplus allowances are passed in strictly increasing order (sorted by currency) to
128
134
  // prevent duplicates.
129
135
  if (j != 0 && surplusAllowance.currency <= fundAccessLimitGroup.surplusAllowances[j - 1].currency) {
130
- revert JBFundAccessLimits_InvalidSurplusAllowanceCurrencyOrdering();
136
+ revert JBFundAccessLimits_InvalidSurplusAllowanceCurrencyOrdering({
137
+ projectId: projectId, rulesetId: rulesetId, groupIndex: i, allowanceIndex: j
138
+ });
131
139
  }
132
140
 
133
141
  // Set the surplus allowance if there is one.