@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
@@ -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_;
@@ -7,19 +7,17 @@ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
7
7
  import {IJBFeelessAddresses} from "./interfaces/IJBFeelessAddresses.sol";
8
8
 
9
9
  /// @notice A registry of addresses exempt from the protocol's 2.5% fee. Feeless addresses don't incur fees on
10
- /// payouts they receive, surplus allowance they use, or cash outs where they are the beneficiary. Managed by the
11
- /// contract owner (typically the protocol multisig).
10
+ /// payouts they receive, surplus allowance they use, or cash outs where they are the beneficiary.
11
+ /// @dev All feeless status is managed by the contract owner (typically the protocol multisig).
12
+ /// @dev `projectId = 0` is the wildcard — an address feeless for project 0 is feeless for ALL projects.
12
13
  contract JBFeelessAddresses is Ownable, IJBFeelessAddresses, IERC165 {
13
14
  //*********************************************************************//
14
- // --------------------- public stored properties -------------------- //
15
+ // -------------------- internal stored properties ------------------- //
15
16
  //*********************************************************************//
16
17
 
17
- /// @notice Check if the specified address is feeless.
18
- /// @dev Feeless addresses can receive payouts without incurring a fee.
19
- /// @dev Feeless addresses can use the surplus allowance without incurring a fee.
20
- /// @dev Feeless addresses can be the beneficiary of cash outs without incurring a fee.
21
- /// @custom:param addr The address to check.
22
- mapping(address addr => bool) public override isFeeless;
18
+ /// @notice Raw feeless status per project per address.
19
+ /// @dev `projectId = 0` stores the global (all-project) feeless status.
20
+ mapping(uint256 projectId => mapping(address addr => bool)) internal _isFeelessFor;
23
21
 
24
22
  //*********************************************************************//
25
23
  // -------------------------- constructor ---------------------------- //
@@ -32,21 +30,42 @@ contract JBFeelessAddresses is Ownable, IJBFeelessAddresses, IERC165 {
32
30
  // ---------------------- external transactions ---------------------- //
33
31
  //*********************************************************************//
34
32
 
35
- /// @notice Add or remove an address from the fee-exempt list. Feeless addresses don't pay the 2.5% protocol fee
36
- /// on payouts received, surplus allowance used, or cash outs where they're the beneficiary.
33
+ /// @notice Add or remove an address from the global (all-project) fee-exempt list.
34
+ /// @dev Equivalent to `setFeelessAddressFor(0, addr, flag)`.
37
35
  /// @dev Can only be called by this contract's owner (typically the protocol multisig).
38
36
  /// @param addr The address to set as feeless or not feeless.
39
37
  /// @param flag Whether the address should be feeless (`true`) or not feeless (`false`).
40
38
  function setFeelessAddress(address addr, bool flag) external virtual override onlyOwner {
41
- isFeeless[addr] = flag;
39
+ _isFeelessFor[0][addr] = flag;
42
40
 
43
- emit SetFeelessAddress({addr: addr, isFeeless: flag, caller: _msgSender()});
41
+ emit SetFeelessAddress({projectId: 0, addr: addr, isFeeless: flag, caller: _msgSender()});
42
+ }
43
+
44
+ /// @notice Add or remove an address from a project's fee-exempt list.
45
+ /// @dev Can only be called by this contract's owner (typically the protocol multisig).
46
+ /// @dev Use `projectId = 0` to set the global (all-project) feeless status.
47
+ /// @param projectId The ID of the project. 0 means all projects.
48
+ /// @param addr The address to set as feeless or not feeless for the project.
49
+ /// @param flag Whether the address should be feeless for the project (`true`) or not (`false`).
50
+ function setFeelessAddressFor(uint256 projectId, address addr, bool flag) external virtual override onlyOwner {
51
+ _isFeelessFor[projectId][addr] = flag;
52
+
53
+ emit SetFeelessAddress({projectId: projectId, addr: addr, isFeeless: flag, caller: _msgSender()});
44
54
  }
45
55
 
46
56
  //*********************************************************************//
47
57
  // -------------------------- public views --------------------------- //
48
58
  //*********************************************************************//
49
59
 
60
+ /// @notice Returns whether the specified address is feeless for a specific project, considering both the wildcard
61
+ /// (projectId 0) and project-specific feeless status.
62
+ /// @param addr The address to check.
63
+ /// @param projectId The ID of the project to check.
64
+ /// @return A flag indicating whether the address is feeless (globally or for the project).
65
+ function isFeelessFor(address addr, uint256 projectId) external view override returns (bool) {
66
+ return _isFeelessFor[0][addr] || _isFeelessFor[projectId][addr];
67
+ }
68
+
50
69
  /// @notice Indicates whether this contract adheres to the specified interface.
51
70
  /// @dev See {IERC165-supportsInterface}.
52
71
  /// @param interfaceId The ID of the interface to check for adherence to.
@@ -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.
@@ -64,7 +64,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
64
64
  //*********************************************************************//
65
65
 
66
66
  error JBMultiTerminal_FeeTerminalNotFound(address token);
67
- error JBMultiTerminal_MintNotAllowed();
67
+ error JBMultiTerminal_MintNotAllowed(uint256 projectId, uint256 splitProjectId, address terminal);
68
68
  error JBMultiTerminal_NoMsgValueAllowed(uint256 value);
69
69
  error JBMultiTerminal_OverflowAlert(uint256 value, uint256 limit);
70
70
  error JBMultiTerminal_PermitAllowanceNotEnough(uint256 amount, uint256 allowance);
@@ -213,7 +213,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
213
213
 
214
214
  // Emit an event for each accounting context.
215
215
  for (uint256 i; i < accountingContexts.length;) {
216
- // slither-disable-next-line reentrancy-events
217
216
  emit SetAccountingContext({projectId: projectId, context: accountingContexts[i], caller: _msgSender()});
218
217
  unchecked {
219
218
  ++i;
@@ -248,15 +247,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
248
247
  _addToBalanceOf({
249
248
  projectId: projectId,
250
249
  token: token,
251
- amount: _acceptFundsFor(projectId, token, amount, metadata),
250
+ amount: _acceptFundsFor({projectId: projectId, token: token, amount: amount, metadata: metadata}),
252
251
  shouldReturnHeldFees: shouldReturnHeldFees,
253
252
  memo: memo,
254
253
  metadata: metadata
255
254
  });
256
255
  }
257
256
 
258
- /// @notice Burns project tokens to reclaim a share of the project's surplus (determined by the bonding curve) or
259
- /// to trigger custom logic through the ruleset's data hook and cash out hook.
257
+ /// @notice Cash out project tokens. The project's current ruleset determines the reclaimed surplus and any data
258
+ /// hook or cash out hook behavior.
260
259
  /// @dev Only the token holder or an operator with `CASH_OUT_TOKENS` permission from that holder can call this.
261
260
  /// @param holder The account cashing out tokens.
262
261
  /// @param projectId The ID of the project the project tokens belong to.
@@ -330,12 +329,12 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
330
329
  if (split.hook != IJBSplitHook(address(0))) {
331
330
  // Make sure that the address supports the split hook interface.
332
331
  if (!split.hook.supportsInterface(type(IJBSplitHook).interfaceId)) {
333
- revert JBMultiTerminal_SplitHookInvalid(split.hook);
332
+ revert JBMultiTerminal_SplitHookInvalid({hook: split.hook});
334
333
  }
335
334
 
336
335
  // This payout is eligible for a fee since the funds are leaving this contract and the split hook isn't a
337
336
  // feeless address.
338
- if (!_isFeeless(address(split.hook))) {
337
+ if (!_isFeeless({addr: address(split.hook), projectId: projectId})) {
339
338
  unchecked {
340
339
  netPayoutAmount -= _feeAmountFrom(amount);
341
340
  }
@@ -370,7 +369,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
370
369
 
371
370
  // The project must have a terminal to send funds to.
372
371
  if (terminal == IJBTerminal(address(0))) {
373
- revert JBMultiTerminal_RecipientProjectTerminalNotFound(split.projectId, token);
372
+ revert JBMultiTerminal_RecipientProjectTerminalNotFound({projectId: split.projectId, token: token});
374
373
  }
375
374
 
376
375
  // Fees apply to fund egress, not intra-terminal accounting. When both projects share this terminal,
@@ -378,7 +377,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
378
377
  // the fee model taxes value leaving the protocol ecosystem, not internal rebalancing.
379
378
  // This payout is eligible for a fee if the funds are leaving this contract and the receiving terminal isn't
380
379
  // a feeless address.
381
- if (terminal != this && !_isFeeless(address(terminal))) {
380
+ if (terminal != this && !_isFeeless({addr: address(terminal), projectId: projectId})) {
382
381
  unchecked {
383
382
  netPayoutAmount -= _feeAmountFrom(amount);
384
383
  }
@@ -409,7 +408,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
409
408
  // Cross-project pay splits on the same terminal are allowed (different project receives the funds).
410
409
  // The try-catch in the split group lib catches this revert and restores the balance.
411
410
  if (terminal == this && split.projectId == projectId) {
412
- revert JBMultiTerminal_MintNotAllowed();
411
+ revert JBMultiTerminal_MintNotAllowed({
412
+ projectId: projectId, splitProjectId: split.projectId, terminal: address(terminal)
413
+ });
413
414
  }
414
415
 
415
416
  // Keep a reference to the beneficiary of the payment.
@@ -429,7 +430,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
429
430
  // destination project's data hook forwarded part of the payment to pay hooks, the store
430
431
  // only recorded a partial balance increase. Without this cap, _feeFreeSurplusOf can exceed
431
432
  // STORE.balanceOf, causing users to be overcharged fees on zero-tax cashouts.
432
- // slither-disable-next-line reentrancy-eth
433
433
  _capFeeFreeSurplus({projectId: split.projectId, token: token});
434
434
  }
435
435
  } else {
@@ -440,7 +440,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
440
440
 
441
441
  // This payout is eligible for a fee since the funds are leaving this contract and the recipient isn't a
442
442
  // feeless address.
443
- if (!_isFeeless(recipient)) {
443
+ if (!_isFeeless({addr: recipient, projectId: projectId})) {
444
444
  unchecked {
445
445
  netPayoutAmount -= _feeAmountFrom(amount);
446
446
  }
@@ -473,7 +473,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
473
473
  require(msg.sender == address(this));
474
474
 
475
475
  if (address(feeTerminal) == address(0)) {
476
- revert JBMultiTerminal_FeeTerminalNotFound(token);
476
+ revert JBMultiTerminal_FeeTerminalNotFound({token: token});
477
477
  }
478
478
 
479
479
  // Send the projectId in the metadata.
@@ -537,7 +537,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
537
537
  // sends fees to the fee project terminal. The migrated project's balance on this terminal is zero, but held
538
538
  // fees are backed by the terminal's own token balance (not the project's recorded balance).
539
539
  // Record the migration in the store.
540
- // slither-disable-next-line reentrancy-events
541
540
  balance = STORE.recordTerminalMigration({projectId: projectId, token: token});
542
541
 
543
542
  emit MigrateTerminal({projectId: projectId, token: token, to: to, amount: balance, caller: _msgSender()});
@@ -547,7 +546,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
547
546
  // Migration to a non-feeless terminal incurs the standard 2.5% fee, same as any other fund egress.
548
547
  // This also settles any fee-free surplus liability that would otherwise be lost on the new terminal.
549
548
  uint256 feeAmount;
550
- if (!_isFeeless(address(to)) && projectId != _FEE_BENEFICIARY_PROJECT_ID) {
549
+ if (!_isFeeless({addr: address(to), projectId: projectId}) && projectId != _FEE_BENEFICIARY_PROJECT_ID) {
551
550
  feeAmount = _takeFeeFrom({
552
551
  projectId: projectId,
553
552
  token: token,
@@ -566,8 +565,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
566
565
  }
567
566
  }
568
567
 
569
- /// @notice Pay a project with tokens. The project's current ruleset determines how many project tokens the
570
- /// beneficiary will receive.
568
+ /// @notice Pay a project. The project's current ruleset determines how many project tokens the beneficiary receives
569
+ /// and any data hook or pay hook behavior.
571
570
  /// @param projectId The ID of the project to pay.
572
571
  /// @param amount The amount of tokens to send, as a fixed point number with the same number of
573
572
  /// decimals as the token's accounting context. If this terminal's token is native, this is ignored and `msg.value`
@@ -669,7 +668,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
669
668
  _nextHeldFeeIndexOf[projectId][token] = currentIndex + 1;
670
669
 
671
670
  // Process the fee.
672
- // slither-disable-next-line reentrancy-no-eth
673
671
  _processFee({
674
672
  projectId: projectId,
675
673
  token: token,
@@ -906,16 +904,16 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
906
904
  JBCashOutHookSpecification[] memory hookSpecifications
907
905
  )
908
906
  {
909
- (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) =
910
- STORE.previewCashOutFrom({
911
- terminal: address(this),
912
- holder: holder,
913
- projectId: projectId,
914
- cashOutCount: cashOutCount,
915
- tokenToReclaim: tokenToReclaim,
916
- beneficiaryIsFeeless: _isFeeless(beneficiary),
917
- metadata: metadata
918
- });
907
+ bool feeless = _isFeeless({addr: beneficiary, projectId: projectId});
908
+ (ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.previewCashOutFrom({
909
+ terminal: address(this),
910
+ holder: holder,
911
+ projectId: projectId,
912
+ cashOutCount: cashOutCount,
913
+ tokenToReclaim: tokenToReclaim,
914
+ beneficiaryIsFeeless: feeless,
915
+ metadata: metadata
916
+ });
919
917
  }
920
918
 
921
919
  /// @notice Simulates a payment without modifying state — use this to preview how many project tokens a payer
@@ -1019,7 +1017,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1019
1017
 
1020
1018
  // Make sure the permit allowance is enough for this payment. If not we revert early.
1021
1019
  if (amount > allowance.amount) {
1022
- revert JBMultiTerminal_PermitAllowanceNotEnough(amount, allowance.amount);
1020
+ revert JBMultiTerminal_PermitAllowanceNotEnough({amount: amount, allowance: allowance.amount});
1023
1021
  }
1024
1022
 
1025
1023
  // Keep a reference to the permit rules.
@@ -1034,8 +1032,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1034
1032
  // Set the allowance to `spend` tokens for the user.
1035
1033
  try PERMIT2.permit({owner: _msgSender(), permitSingle: permitSingle, signature: allowance.signature}) {}
1036
1034
  catch (bytes memory reason) {
1037
- // slither-disable-next-line reentrancy-events
1038
- emit Permit2AllowanceFailed(token, _msgSender(), reason);
1035
+ emit Permit2AllowanceFailed({token: token, owner: _msgSender(), reason: reason});
1039
1036
  }
1040
1037
  }
1041
1038
 
@@ -1118,7 +1115,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1118
1115
 
1119
1116
  // Cap fee-free surplus at the remaining balance.
1120
1117
  if (feeFreeSurplus > remainingBalance) {
1121
- // slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
1122
1118
  _feeFreeSurplusOf[projectId][token] = remainingBalance;
1123
1119
  }
1124
1120
  }
@@ -1159,7 +1155,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1159
1155
  uint256 cashOutTaxRate;
1160
1156
 
1161
1157
  // Cache whether the beneficiary is feeless.
1162
- bool beneficiaryIsFeeless = _isFeeless(beneficiary);
1158
+ bool beneficiaryIsFeeless = _isFeeless({addr: beneficiary, projectId: projectId});
1163
1159
 
1164
1160
  {
1165
1161
  // Cache the controller to avoid a redundant external call (also used inside STORE.recordCashOutFor).
@@ -1199,7 +1195,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1199
1195
  uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][tokenToReclaim];
1200
1196
  if (feeFreeSurplus != 0) {
1201
1197
  uint256 feeableAmount = reclaimAmount < feeFreeSurplus ? reclaimAmount : feeFreeSurplus;
1202
- // slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
1203
1198
  unchecked {
1204
1199
  _feeFreeSurplusOf[projectId][tokenToReclaim] = feeFreeSurplus - feeableAmount;
1205
1200
  }
@@ -1242,7 +1237,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1242
1237
  // balance during a prior inbound payout), overcharging fees on round-trip prevention.
1243
1238
  // Placed after hook fulfillment so any further balance reductions from cashout hooks are
1244
1239
  // also accounted for.
1245
- // slither-disable-next-line reentrancy-eth
1246
1240
  _capFeeFreeSurplus({projectId: projectId, token: tokenToReclaim});
1247
1241
 
1248
1242
  // Take the fee from all outbound reclaimings.
@@ -1344,12 +1338,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1344
1338
  } else {
1345
1339
  // Trigger any inherited pre-transfer logic.
1346
1340
  // Keep a reference to the amount that'll be paid as a `msg.value`.
1347
- // slither-disable-next-line reentrancy-events
1348
1341
  uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
1349
1342
 
1350
1343
  // Send the fee.
1351
1344
  // If this terminal's token is ETH, send it in msg.value.
1352
- // slither-disable-next-line unused-return
1353
1345
  terminal.pay{value: payValue}({
1354
1346
  projectId: projectId,
1355
1347
  token: token,
@@ -1382,7 +1374,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1382
1374
  {
1383
1375
  // Trigger any inherited pre-transfer logic.
1384
1376
  // Keep a reference to the amount that'll be paid as a `msg.value`.
1385
- // slither-disable-next-line reentrancy-events
1386
1377
  uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
1387
1378
 
1388
1379
  // Add to balance on the recipient terminal.
@@ -1441,7 +1432,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1441
1432
  cashOutMetadata: metadata
1442
1433
  });
1443
1434
 
1444
- // slither-disable-next-line calls-loop
1445
1435
  for (uint256 i; i < specifications.length;) {
1446
1436
  // Set the specification being iterated on.
1447
1437
  JBCashOutHookSpecification memory specification = specifications[i];
@@ -1455,8 +1445,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1455
1445
  }
1456
1446
 
1457
1447
  // Get the fee for the specified amount.
1458
- uint256 specificationAmountFee =
1459
- _isFeeless(address(specification.hook)) ? 0 : _feeAmountFrom(specification.amount);
1448
+ uint256 specificationAmountFee = _isFeeless({addr: address(specification.hook), projectId: projectId})
1449
+ ? 0
1450
+ : _feeAmountFrom(specification.amount);
1460
1451
 
1461
1452
  // Add the specification's amount to the amount eligible for fees.
1462
1453
  if (specificationAmountFee != 0) {
@@ -1477,13 +1468,11 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1477
1468
 
1478
1469
  // Trigger any inherited pre-transfer logic.
1479
1470
  // Keep a reference to the amount that'll be paid as a `msg.value`.
1480
- // slither-disable-next-line reentrancy-events
1481
1471
  uint256 payValue = _beforeTransferTo({
1482
1472
  to: address(specification.hook), token: beneficiaryReclaimAmount.token, amount: specification.amount
1483
1473
  });
1484
1474
 
1485
1475
  // Fulfill the specification.
1486
- // slither-disable-next-line reentrancy-events,calls-loop
1487
1476
  specification.hook.afterCashOutRecordedWith{value: payValue}(context);
1488
1477
 
1489
1478
  // Revoke the temporary pull allowance now that the hook call has finished.
@@ -1538,7 +1527,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1538
1527
  });
1539
1528
 
1540
1529
  // Fulfill each specification through their pay hooks.
1541
- // slither-disable-next-line calls-loop
1542
1530
  for (uint256 i; i < specifications.length;) {
1543
1531
  // Set the specification being iterated on.
1544
1532
  JBPayHookSpecification memory specification = specifications[i];
@@ -1564,13 +1552,11 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1564
1552
 
1565
1553
  // Trigger any inherited pre-transfer logic.
1566
1554
  // Keep a reference to the amount that'll be paid as a `msg.value`.
1567
- // slither-disable-next-line reentrancy-events
1568
1555
  uint256 payValue = _beforeTransferTo({
1569
1556
  to: address(specification.hook), token: tokenAmount.token, amount: specification.amount
1570
1557
  });
1571
1558
 
1572
1559
  // Fulfill the specification.
1573
- // slither-disable-next-line reentrancy-events,calls-loop
1574
1560
  specification.hook.afterPayRecordedWith{value: payValue}(context);
1575
1561
 
1576
1562
  // Revoke the temporary pull allowance now that the hook call has finished.
@@ -1618,7 +1604,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1618
1604
  // Keep a reference to the ruleset the payment is being made during.
1619
1605
  // Keep a reference to the pay hook specifications.
1620
1606
  // Keep a reference to the token count that'll be minted as a result of the payment.
1621
- // slither-disable-next-line reentrancy-events
1622
1607
  (JBRuleset memory ruleset, uint256 tokenCount, JBPayHookSpecification[] memory hookSpecifications) = STORE.recordPaymentFrom({
1623
1608
  payer: payer, amount: tokenAmount, projectId: projectId, beneficiary: beneficiary, metadata: metadata
1624
1609
  });
@@ -1630,7 +1615,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1630
1615
  if (tokenCount != 0) {
1631
1616
  // Set the token count to be the number of tokens minted for the beneficiary instead of the total
1632
1617
  // amount.
1633
- // slither-disable-next-line reentrancy-events
1634
1618
  newlyIssuedTokenCount = _controllerOf(projectId)
1635
1619
  .mintTokensOf({
1636
1620
  projectId: projectId,
@@ -1686,7 +1670,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1686
1670
  )
1687
1671
  internal
1688
1672
  {
1689
- // slither-disable-next-line reentrancy-events,calls-loop
1690
1673
  try this.executeProcessFee({
1691
1674
  projectId: projectId, token: token, amount: amount, beneficiary: beneficiary, feeTerminal: feeTerminal
1692
1675
  }) {
@@ -1723,7 +1706,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1723
1706
  /// this
1724
1707
  /// terminal.
1725
1708
  function _recordAddedBalanceFor(uint256 projectId, address token, uint256 amount) internal {
1726
- // slither-disable-next-line calls-loop
1727
1709
  STORE.recordAddedBalanceFor({projectId: projectId, token: token, amount: amount});
1728
1710
  }
1729
1711
 
@@ -1760,7 +1742,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1760
1742
  // Save the fee being iterated on.
1761
1743
  JBFee memory heldFee = _heldFeesOf[projectId][token][startIndex + i];
1762
1744
 
1763
- // slither-disable-next-line incorrect-equality
1764
1745
  if (leftoverAmount == 0) {
1765
1746
  break;
1766
1747
  } else {
@@ -1862,7 +1843,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1862
1843
  // Send any leftover funds to the project owner and update the fee tracking accordingly.
1863
1844
  if (leftoverPayoutAmount != 0) {
1864
1845
  // Keep a reference to the fee for the leftover payout amount.
1865
- uint256 fee = _isFeeless(projectOwner) ? 0 : _feeAmountFrom(leftoverPayoutAmount);
1846
+ uint256 fee =
1847
+ _isFeeless({addr: projectOwner, projectId: projectId}) ? 0 : _feeAmountFrom(leftoverPayoutAmount);
1866
1848
 
1867
1849
  uint256 netLeftoverPayoutAmount;
1868
1850
  unchecked {
@@ -1878,7 +1860,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1878
1860
  leftoverPayoutAmount = netLeftoverPayoutAmount;
1879
1861
  }
1880
1862
  } catch (bytes memory reason) {
1881
- // slither-disable-next-line reentrancy-events
1882
1863
  emit PayoutTransferReverted({
1883
1864
  projectId: projectId,
1884
1865
  addr: projectOwner,
@@ -1896,7 +1877,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
1896
1877
 
1897
1878
  // Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
1898
1879
  // Placed after all payouts settle so the cap reflects post-payout state.
1899
- // slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
1900
1880
  _capFeeFreeSurplus({projectId: projectId, token: token});
1901
1881
 
1902
1882
  // Take the fee.
@@ -2042,14 +2022,12 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
2042
2022
  STORE.recordUsedAllowanceOf({projectId: projectId, token: token, amount: amount, currency: currency});
2043
2023
 
2044
2024
  // Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
2045
- // slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
2046
2025
  _capFeeFreeSurplus({projectId: projectId, token: token});
2047
2026
 
2048
2027
  // Take a fee from the `amountPaidOut`, if needed.
2049
2028
  // The net amount is the final amount withdrawn after the fee has been taken.
2050
- // slither-disable-next-line reentrancy-events
2051
2029
  netAmountPaidOut = amountPaidOut
2052
- - (_isFeeless(owner) || _isFeeless(beneficiary)
2030
+ - (_isFeeless({addr: owner, projectId: projectId}) || _isFeeless({addr: beneficiary, projectId: projectId})
2053
2031
  ? 0
2054
2032
  : _takeFeeFrom({
2055
2033
  projectId: projectId,
@@ -2092,7 +2070,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
2092
2070
  if (token == JBConstants.NATIVE_TOKEN) return;
2093
2071
 
2094
2072
  // Revert if the callee returned without consuming the full forwarded ERC-20 amount.
2095
- // slither-disable-next-line calls-loop
2096
2073
  uint256 allowance = IERC20(token).allowance({owner: address(this), spender: to});
2097
2074
  if (allowance != 0) revert JBMultiTerminal_TemporaryAllowanceNotConsumed(token, to, allowance);
2098
2075
  }
@@ -2138,9 +2115,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
2138
2115
 
2139
2116
  /// @notice Returns a flag indicating if interacting with an address should not incur fees.
2140
2117
  /// @param addr The address to check.
2141
- /// @return A flag indicating if the address should not incur fees.
2142
- function _isFeeless(address addr) internal view returns (bool) {
2143
- return FEELESS_ADDRESSES.isFeeless(addr);
2118
+ /// @param projectId The ID of the project to check the per-project feeless status for.
2119
+ /// @return A flag indicating if the address should not incur fees (globally or for the project).
2120
+ function _isFeeless(address addr, uint256 projectId) internal view returns (bool) {
2121
+ return FEELESS_ADDRESSES.isFeelessFor({addr: addr, projectId: projectId});
2144
2122
  }
2145
2123
 
2146
2124
  /// @notice The calldata. Preferred to use over `msg.data`.
@@ -17,8 +17,7 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
17
17
  // --------------------------- custom errors ------------------------- //
18
18
  //*********************************************************************//
19
19
 
20
- error JBPermissions_CantSetRootPermissionForWildcardProject();
21
- error JBPermissions_NoZeroPermission();
20
+ error JBPermissions_NoZeroPermission(address account, address operator, uint256 projectId);
22
21
  error JBPermissions_PermissionIdOutOfBounds(uint256 permissionId);
23
22
  error JBPermissions_Unauthorized(address account, address operator, uint256 projectId, uint256 permissionId);
24
23
 
@@ -69,7 +68,11 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
69
68
  uint256 packed = _packedPermissions(permissionsData.permissionIds);
70
69
 
71
70
  // Make sure the 0 permission is not set.
72
- if (_includesPermission({permissions: packed, permissionId: 0})) revert JBPermissions_NoZeroPermission();
71
+ if (_includesPermission({permissions: packed, permissionId: 0})) {
72
+ revert JBPermissions_NoZeroPermission({
73
+ account: account, operator: permissionsData.operator, projectId: permissionsData.projectId
74
+ });
75
+ }
73
76
 
74
77
  // Cache the sender.
75
78
  address msgSender = _msgSender();