@bananapus/core-v6 0.0.42 → 0.0.44

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +3 -3
  3. package/package.json +2 -2
  4. package/references/entrypoints.md +3 -1
  5. package/references/types-errors-events.md +2 -3
  6. package/src/JBChainlinkV3PriceFeed.sol +14 -6
  7. package/src/JBChainlinkV3SequencerPriceFeed.sol +5 -6
  8. package/src/JBController.sol +84 -68
  9. package/src/JBDirectory.sol +4 -7
  10. package/src/JBERC20.sol +8 -7
  11. package/src/JBFeelessAddresses.sol +32 -13
  12. package/src/JBFundAccessLimits.sol +12 -4
  13. package/src/JBMultiTerminal.sol +39 -61
  14. package/src/JBPermissions.sol +6 -3
  15. package/src/JBPrices.sol +18 -12
  16. package/src/JBRulesets.sol +4 -25
  17. package/src/JBSplits.sol +13 -5
  18. package/src/JBTerminalStore.sol +46 -82
  19. package/src/JBTokens.sol +11 -13
  20. package/src/abstract/JBControlled.sol +1 -2
  21. package/src/interfaces/IJBController.sol +0 -6
  22. package/src/interfaces/IJBFeelessAddresses.sol +17 -7
  23. package/src/libraries/JBCashOuts.sol +6 -2
  24. package/src/libraries/JBCurrencyIds.sol +3 -0
  25. package/src/libraries/JBMetadataResolver.sol +20 -12
  26. package/src/libraries/JBPayoutSplitGroupLib.sol +8 -3
  27. package/src/libraries/JBRulesetMetadataResolver.sol +4 -4
  28. package/src/libraries/JBSplitGroupIds.sol +1 -0
  29. package/src/periphery/JBDeadline1Day.sol +1 -0
  30. package/src/periphery/JBDeadline3Days.sol +1 -0
  31. package/src/periphery/JBDeadline3Hours.sol +1 -0
  32. package/src/periphery/JBDeadline7Days.sol +1 -0
  33. package/src/structs/JBBeforeCashOutRecordedContext.sol +3 -2
  34. package/src/structs/JBRulesetMetadata.sol +3 -3
  35. package/test/helpers/JBTest.sol +3 -3
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.44",
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.22",
42
42
  "@chainlink/contracts": "1.5.0",
43
43
  "@openzeppelin/contracts": "5.6.1",
44
44
  "@prb/math": "4.1.1",
@@ -155,6 +155,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
155
155
 
156
156
  | Function | What it does |
157
157
  |----------|--------------|
158
- | `setFeelessAddress(address addr, bool flag)` | Adds or removes an address from the fee exemption list. Owner-only. (`JBFeelessAddresses`) |
158
+ | `setFeelessAddress(address addr, bool flag)` | Adds or removes an address from the global (all-project) fee exemption list. Owner-only. (`JBFeelessAddresses`) |
159
+ | `setFeelessAddressFor(uint256 projectId, address addr, bool flag)` | Adds or removes an address from a project's fee exemption list. `projectId = 0` = global wildcard. Owner-only. (`JBFeelessAddresses`) |
160
+ | `isFeelessFor(address addr, uint256 projectId)` | Returns whether an address is feeless for a project (checks both wildcard and project-specific). (`JBFeelessAddresses`) |
159
161
  | `setControllerAllowed(uint256 projectId)` | Returns whether a project's controller can currently be set. (`IJBDirectoryAccessControl`) |
160
162
  | `setTerminalsAllowed(uint256 projectId)` | Returns whether a project's terminals can currently be set. (`IJBDirectoryAccessControl`) |
@@ -8,7 +8,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
8
8
  |-------------|------------|---------|
9
9
  | `JBRuleset` | `cycleNumber (uint48)`, `id (uint48)`, `basedOnId (uint48)`, `start (uint48)`, `duration (uint32)`, `weight (uint112)`, `weightCutPercent (uint32)`, `approvalHook`, `metadata (uint256)` | `currentOf()`, `recordPaymentFrom()`, `recordCashOutFor()` return values |
10
10
  | `JBRulesetConfig` | `mustStartAtOrAfter (uint48)`, `duration (uint32)`, `weight (uint112)`, `weightCutPercent (uint32)`, `approvalHook`, `metadata (JBRulesetMetadata)`, `splitGroups[]`, `fundAccessLimitGroups[]` | `launchProjectFor()`, `queueRulesetsOf()` input |
11
- | `JBRulesetMetadata` | `reservedPercent (uint16)`, `cashOutTaxRate (uint16)`, `baseCurrency (uint32)`, `pausePay`, `pauseCreditTransfers`, `allowOwnerMinting`, `allowSetCustomToken`, `allowTerminalMigration`, `allowSetTerminals`, `allowSetController`, `allowAddAccountingContext`, `allowAddPriceFeed`, `ownerMustSendPayouts`, `holdFees`, `useTotalSurplusForCashOuts`, `useDataHookForPay`, `useDataHookForCashOut`, `dataHook (address)`, `metadata (uint16)` | Packed into `JBRuleset.metadata` |
11
+ | `JBRulesetMetadata` | `reservedPercent (uint16)`, `cashOutTaxRate (uint16)`, `baseCurrency (uint32)`, `pausePay`, `pauseCreditTransfers`, `allowOwnerMinting`, `allowSetCustomToken`, `allowTerminalMigration`, `allowSetTerminals`, `allowSetController`, `allowAddAccountingContext`, `allowAddPriceFeed`, `ownerMustSendPayouts`, `holdFees`, `scopeCashOutsToLocalBalances`, `useDataHookForPay`, `useDataHookForCashOut`, `dataHook (address)`, `metadata (uint16)` | Packed into `JBRuleset.metadata` |
12
12
  | `JBSplit` | `percent (uint32)`, `projectId (uint64)`, `beneficiary (address payable)`, `preferAddToBalance`, `lockedUntil (uint48)`, `hook (IJBSplitHook)` | `splitsOf()`, `setSplitGroupsOf()` |
13
13
  | `JBSplitGroup` | `groupId (uint256)`, `splits (JBSplit[])` | `JBRulesetConfig.splitGroups`, `setSplitGroupsOf()` |
14
14
  | `JBAccountingContext` | `token (address)`, `decimals (uint8)`, `currency (uint32)` | Terminal token accounting, surplus/reclaim calculations |
@@ -28,7 +28,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
28
28
  | Struct | Key Fields | Used In |
29
29
  |--------|------------|---------|
30
30
  | `JBBeforePayRecordedContext` | `terminal`, `payer`, `amount (JBTokenAmount)`, `projectId`, `rulesetId`, `beneficiary`, `weight`, `reservedPercent`, `metadata` | `IJBRulesetDataHook.beforePayRecordedWith()` input |
31
- | `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `useTotalSurplus`, `cashOutTaxRate`, `beneficiaryIsFeeless`, `metadata` | `IJBRulesetDataHook.beforeCashOutRecordedWith()` input |
31
+ | `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `scopeCashOutsToLocalBalances`, `cashOutTaxRate`, `beneficiaryIsFeeless`, `metadata` | `IJBRulesetDataHook.beforeCashOutRecordedWith()` input |
32
32
  | `JBAfterPayRecordedContext` | `payer`, `projectId`, `rulesetId`, `amount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `weight`, `newlyIssuedTokenCount`, `beneficiary`, `hookMetadata`, `payerMetadata` | `IJBPayHook.afterPayRecordedWith()` input |
33
33
  | `JBAfterCashOutRecordedContext` | `holder`, `projectId`, `rulesetId`, `cashOutCount`, `reclaimedAmount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `cashOutTaxRate`, `beneficiary`, `hookMetadata`, `cashOutMetadata` | `IJBCashOutHook.afterCashOutRecordedWith()` input |
34
34
  | `JBPayHookSpecification` | `hook (IJBPayHook)`, `noop (bool)`, `amount`, `metadata` | Returned by data hook; specifies which pay hooks to call and how much to forward. `noop = true` means informational-only (callback skipped, amount must be 0). |
@@ -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
 
@@ -189,7 +188,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
189
188
  {
190
189
  // Enforce permissions.
191
190
  _requirePermissionFrom({
192
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.ADD_PRICE_FEED
191
+ account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.ADD_PRICE_FEED
193
192
  });
194
193
 
195
194
  JBRuleset memory ruleset = _currentRulesetOf(projectId);
@@ -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()
@@ -319,7 +317,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
319
317
  {
320
318
  // Enforce permissions.
321
319
  _requirePermissionFrom({
322
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.DEPLOY_ERC20
320
+ account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.DEPLOY_ERC20
323
321
  });
324
322
 
325
323
  // If a salt is provided, use it.
@@ -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,39 +443,41 @@ 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();
452
454
 
453
455
  // Enforce permissions.
456
+ bool isOmnichainOperator = sender == OMNICHAIN_RULESET_OPERATOR;
454
457
  _requirePermissionAllowingOverrideFrom({
455
- account: PROJECTS.ownerOf(projectId),
458
+ account: _ownerOf(projectId),
456
459
  projectId: projectId,
457
460
  permissionId: JBPermissionIds.LAUNCH_RULESETS,
458
- alsoGrantAccessIf: sender == OMNICHAIN_RULESET_OPERATOR
461
+ alsoGrantAccessIf: isOmnichainOperator
459
462
  });
460
-
461
- // Enforce permissions.
462
463
  _requirePermissionAllowingOverrideFrom({
463
- account: PROJECTS.ownerOf(projectId),
464
+ account: _ownerOf(projectId),
464
465
  projectId: projectId,
465
466
  permissionId: JBPermissionIds.SET_TERMINALS,
466
- alsoGrantAccessIf: sender == OMNICHAIN_RULESET_OPERATOR
467
+ alsoGrantAccessIf: isOmnichainOperator
467
468
  });
468
-
469
469
  if (bytes(projectUri).length > 0) {
470
470
  _requirePermissionAllowingOverrideFrom({
471
- account: PROJECTS.ownerOf(projectId),
471
+ account: _ownerOf(projectId),
472
472
  projectId: projectId,
473
473
  permissionId: JBPermissionIds.SET_PROJECT_URI,
474
- alsoGrantAccessIf: sender == OMNICHAIN_RULESET_OPERATOR
474
+ alsoGrantAccessIf: isOmnichainOperator
475
475
  });
476
476
  }
477
477
 
478
478
  // If the project has already had rulesets, use `queueRulesetsOf(...)` instead.
479
479
  if (RULESETS.latestRulesetIdOf(projectId) > 0) {
480
- revert JBController_RulesetsAlreadyLaunched(projectId);
480
+ revert JBController_RulesetsAlreadyLaunched({projectId: projectId});
481
481
  }
482
482
 
483
483
  // If provided, set the project's metadata URI.
@@ -492,7 +492,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
492
492
  _configureTerminals({projectId: projectId, terminalConfigurations: terminalConfigurations});
493
493
 
494
494
  // Queue the first ruleset.
495
- // slither-disable-next-line reentrancy-events
496
495
  rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
497
496
 
498
497
  emit LaunchRulesets({
@@ -539,7 +538,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
539
538
  returns (uint256 beneficiaryTokenCount)
540
539
  {
541
540
  // There should be tokens to mint.
542
- if (tokenCount == 0) revert JBController_ZeroTokensToMint();
541
+ if (tokenCount == 0) revert JBController_ZeroTokensToMint({projectId: projectId, beneficiary: beneficiary});
543
542
 
544
543
  // Keep a reference to the reserved percent.
545
544
  uint256 reservedPercent;
@@ -549,16 +548,16 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
549
548
 
550
549
  // Cache common values used in both permission checks.
551
550
  address sender = _msgSender();
552
- bool senderIsTerminal = _isTerminalOf(projectId, sender);
551
+ bool senderIsTerminal = _isTerminalOf({projectId: projectId, terminal: sender});
553
552
  bool senderIsTerminalOrDataHook = senderIsTerminal || sender == ruleset.dataHook();
554
553
  // 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);
554
+ bool senderHasDataHookMintPermission = !senderIsTerminalOrDataHook
555
+ && _hasDataHookMintPermissionFor({projectId: projectId, ruleset: ruleset, addr: sender});
557
556
 
558
557
  // Minting is restricted to: the project's owner, addresses with permission to `MINT_TOKENS`, the project's
559
558
  // terminals, and the project's data hook.
560
559
  _requirePermissionAllowingOverrideFrom({
561
- account: PROJECTS.ownerOf(projectId),
560
+ account: _ownerOf(projectId),
562
561
  projectId: projectId,
563
562
  permissionId: JBPermissionIds.MINT_TOKENS,
564
563
  alsoGrantAccessIf: senderIsTerminalOrDataHook || senderHasDataHookMintPermission
@@ -570,7 +569,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
570
569
  ruleset.id != 0 && !ruleset.allowOwnerMinting() && !senderIsTerminalOrDataHook
571
570
  && !senderHasDataHookMintPermission
572
571
  ) {
573
- revert JBController_MintNotAllowedAndNotTerminalOrHook(sender);
572
+ revert JBController_MintNotAllowedAndNotTerminalOrHook({caller: sender});
574
573
  }
575
574
 
576
575
  // Determine the reserved percent to use.
@@ -581,7 +580,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
581
580
 
582
581
  if (beneficiaryTokenCount != 0) {
583
582
  // Mint the tokens.
584
- // slither-disable-next-line reentrancy-benign,reentrancy-events,unused-return
585
583
  TOKENS.mintFor({holder: beneficiary, projectId: projectId, count: beneficiaryTokenCount});
586
584
  }
587
585
 
@@ -618,18 +616,21 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
618
616
  returns (uint256 rulesetId)
619
617
  {
620
618
  // Make sure there are rulesets being queued.
621
- if (rulesetConfigurations.length == 0) revert JBController_RulesetsArrayEmpty();
619
+ if (rulesetConfigurations.length == 0) {
620
+ revert JBController_RulesetsArrayEmpty({
621
+ projectId: projectId, rulesetConfigurationCount: rulesetConfigurations.length
622
+ });
623
+ }
622
624
 
623
625
  // Enforce permissions.
624
626
  _requirePermissionAllowingOverrideFrom({
625
- account: PROJECTS.ownerOf(projectId),
627
+ account: _ownerOf(projectId),
626
628
  projectId: projectId,
627
629
  permissionId: JBPermissionIds.QUEUE_RULESETS,
628
630
  alsoGrantAccessIf: _msgSender() == OMNICHAIN_RULESET_OPERATOR
629
631
  });
630
632
 
631
633
  // Queue the rulesets.
632
- // slither-disable-next-line reentrancy-events
633
634
  rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
634
635
 
635
636
  emit QueueRulesets({rulesetId: rulesetId, projectId: projectId, memo: memo, caller: _msgSender()});
@@ -662,7 +663,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
662
663
  {
663
664
  // Enforce permissions.
664
665
  _requirePermissionFrom({
665
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_SPLIT_GROUPS
666
+ account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_SPLIT_GROUPS
666
667
  });
667
668
 
668
669
  // Set the split groups.
@@ -676,7 +677,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
676
677
  function setTokenFor(uint256 projectId, IJBToken token) external override {
677
678
  // Enforce permissions.
678
679
  _requirePermissionFrom({
679
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN
680
+ account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN
680
681
  });
681
682
 
682
683
  // Get a reference to the current ruleset.
@@ -700,7 +701,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
700
701
  function setTokenMetadataOf(uint256 projectId, string calldata name, string calldata symbol) external override {
701
702
  // Enforce permissions.
702
703
  _requirePermissionFrom({
703
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN_METADATA
704
+ account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN_METADATA
704
705
  });
705
706
 
706
707
  TOKENS.setTokenMetadataFor({projectId: projectId, name: name, symbol: symbol});
@@ -715,7 +716,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
715
716
  function setUriOf(uint256 projectId, string calldata uri) external override {
716
717
  // Enforce permissions.
717
718
  _requirePermissionFrom({
718
- account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_PROJECT_URI
719
+ account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_PROJECT_URI
719
720
  });
720
721
 
721
722
  // Set the project's metadata URI.
@@ -748,7 +749,9 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
748
749
  JBRuleset memory ruleset = _currentRulesetOf(projectId);
749
750
 
750
751
  // Credit transfers must not be paused.
751
- if (ruleset.pauseCreditTransfers()) revert JBController_CreditTransfersPaused();
752
+ if (ruleset.pauseCreditTransfers()) {
753
+ revert JBController_CreditTransfersPaused({projectId: projectId, rulesetId: ruleset.id});
754
+ }
752
755
 
753
756
  TOKENS.transferCreditsFrom({holder: holder, projectId: projectId, recipient: recipient, count: creditCount});
754
757
  }
@@ -939,7 +942,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
939
942
  JBTerminalConfig memory terminalConfig = terminalConfigurations[i];
940
943
 
941
944
  // Add the accounting contexts for the specified tokens.
942
- // slither-disable-next-line calls-loop
943
945
  terminalConfig.terminal
944
946
  .addAccountingContextsFor({
945
947
  projectId: projectId, accountingContexts: terminalConfig.accountingContextsToAccept
@@ -975,20 +977,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
975
977
 
976
978
  // Make sure its reserved percent is valid.
977
979
  if (rulesetConfig.metadata.reservedPercent > JBConstants.MAX_RESERVED_PERCENT) {
978
- revert JBController_InvalidReservedPercent(
979
- rulesetConfig.metadata.reservedPercent, JBConstants.MAX_RESERVED_PERCENT
980
- );
980
+ revert JBController_InvalidReservedPercent({
981
+ percent: rulesetConfig.metadata.reservedPercent, limit: JBConstants.MAX_RESERVED_PERCENT
982
+ });
981
983
  }
982
984
 
983
985
  // Make sure its cash out tax rate is valid.
984
986
  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
- );
987
+ revert JBController_InvalidCashOutTaxRate({
988
+ rate: rulesetConfig.metadata.cashOutTaxRate, limit: JBConstants.MAX_CASH_OUT_TAX_RATE
989
+ });
988
990
  }
989
991
 
990
992
  // Queue its ruleset.
991
- // slither-disable-next-line calls-loop
992
993
  JBRuleset memory ruleset = RULESETS.queueFor({
993
994
  projectId: projectId,
994
995
  duration: rulesetConfig.duration,
@@ -1000,13 +1001,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1000
1001
  });
1001
1002
 
1002
1003
  // Set its split groups.
1003
- // slither-disable-next-line calls-loop
1004
1004
  SPLITS.setSplitGroupsOf({
1005
1005
  projectId: projectId, rulesetId: ruleset.id, splitGroups: rulesetConfig.splitGroups
1006
1006
  });
1007
1007
 
1008
1008
  // Set its fund access limits.
1009
- // slither-disable-next-line calls-loop
1010
1009
  FUND_ACCESS_LIMITS.setFundAccessLimitsFor({
1011
1010
  projectId: projectId, rulesetId: ruleset.id, fundAccessLimitGroups: rulesetConfig.fundAccessLimitGroups
1012
1011
  });
@@ -1069,13 +1068,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1069
1068
 
1070
1069
  // If the split has a hook, call its `processSplitWith` function.
1071
1070
  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
- });
1071
+ if (token != IJBToken(address(0))) {
1072
+ // ERC-20: grant the hook an allowance and let it pull tokens via transferFrom.
1073
+ IERC20(address(token)).forceApprove({spender: address(split.hook), value: splitTokenCount});
1074
+ } else {
1075
+ // Credits: send directly — there is no allowance mechanism for credits.
1076
+ TOKENS.transferCreditsFrom({
1077
+ holder: address(this),
1078
+ projectId: projectId,
1079
+ recipient: address(split.hook),
1080
+ count: splitTokenCount
1081
+ });
1082
+ }
1077
1083
 
1078
- // slither-disable-next-line calls-loop,reentrancy-events
1079
1084
  try split.hook
1080
1085
  .processSplitWith(
1081
1086
  JBSplitHookContext({
@@ -1088,9 +1093,18 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1088
1093
  })
1089
1094
  ) {}
1090
1095
  catch (bytes memory reason) {
1091
- // If the hook reverts, the tokens already transferred to it stay with the hook.
1092
1096
  emit SplitHookReverted({projectId: projectId, hook: address(split.hook), reason: reason});
1093
1097
  }
1098
+
1099
+ // For ERC-20 tokens, burn any tokens the hook did not consume.
1100
+ if (token != IJBToken(address(0))) {
1101
+ uint256 remaining =
1102
+ IERC20(address(token)).allowance({owner: address(this), spender: address(split.hook)});
1103
+ if (remaining > 0) {
1104
+ IERC20(address(token)).forceApprove({spender: address(split.hook), value: 0});
1105
+ TOKENS.burnFrom({holder: address(this), projectId: projectId, count: remaining});
1106
+ }
1107
+ }
1094
1108
  // If the split has a project ID, try to pay the project. If that fails, pay the beneficiary.
1095
1109
  } else {
1096
1110
  // Pay the project using the split's beneficiary if one was provided. Otherwise, use the message
@@ -1099,7 +1113,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1099
1113
 
1100
1114
  if (split.projectId != 0) {
1101
1115
  // Get a reference to the receiving project's primary payment terminal for the token.
1102
- // slither-disable-next-line calls-loop
1103
1116
  IJBTerminal terminal = token == IJBToken(address(0))
1104
1117
  ? IJBTerminal(address(0))
1105
1118
  : DIRECTORY.primaryTerminalOf({projectId: split.projectId, token: address(token)});
@@ -1108,17 +1121,14 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1108
1121
  // which accepts the token, send the tokens to the beneficiary.
1109
1122
  if (address(token) == address(0) || address(terminal) == address(0)) {
1110
1123
  // Mint the tokens to the beneficiary.
1111
- // slither-disable-next-line reentrancy-events
1112
1124
  _sendTokens({
1113
1125
  projectId: projectId, tokenCount: splitTokenCount, recipient: beneficiary, token: token
1114
1126
  });
1115
1127
  } else {
1116
1128
  // Use the `projectId` in the pay metadata.
1117
- // slither-disable-next-line reentrancy-events
1118
1129
  bytes memory metadata = bytes(abi.encodePacked(projectId));
1119
1130
 
1120
1131
  // Try to fulfill the payment.
1121
- // slither-disable-next-line calls-loop
1122
1132
  try this.executePayReservedTokenToTerminal({
1123
1133
  projectId: split.projectId,
1124
1134
  terminal: terminal,
@@ -1142,7 +1152,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1142
1152
  }
1143
1153
  } else if (beneficiary == address(0xdead)) {
1144
1154
  // If the split has no project ID, and the beneficiary is 0xdead, burn.
1145
- // slither-disable-next-line calls-loop
1146
1155
  TOKENS.burnFrom({holder: address(this), projectId: projectId, count: splitTokenCount});
1147
1156
  } else {
1148
1157
  // If the split has no project Id, send to beneficiary.
@@ -1180,13 +1189,13 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1180
1189
  tokenCount = pendingReservedTokenBalanceOf[projectId];
1181
1190
 
1182
1191
  // Revert if there are no pending reserved tokens
1183
- if (tokenCount == 0) revert JBController_NoReservedTokens();
1192
+ if (tokenCount == 0) revert JBController_NoReservedTokens({projectId: projectId});
1184
1193
 
1185
1194
  // Get the ruleset to read the reserved percent from.
1186
1195
  JBRuleset memory ruleset = _currentRulesetOf(projectId);
1187
1196
 
1188
1197
  // Get a reference to the project's owner.
1189
- address owner = PROJECTS.ownerOf(projectId);
1198
+ address owner = _ownerOf(projectId);
1190
1199
 
1191
1200
  // Reset the pending reserved token balance.
1192
1201
  pendingReservedTokenBalanceOf[projectId] = 0;
@@ -1313,6 +1322,13 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1313
1322
  return ERC2771Context._msgSender();
1314
1323
  }
1315
1324
 
1325
+ /// @notice The owner of a project.
1326
+ /// @param projectId The ID of the project to get the owner of.
1327
+ /// @return The owner of the project.
1328
+ function _ownerOf(uint256 projectId) internal view returns (address) {
1329
+ return PROJECTS.ownerOf(projectId);
1330
+ }
1331
+
1316
1332
  /// @notice The project's upcoming ruleset.
1317
1333
  /// @param projectId The ID of the project to check.
1318
1334
  /// @return The project's upcoming ruleset.