@bananapus/core-v6 0.0.54 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/core-v6",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,11 +43,10 @@ library CoreDeploymentLib {
43
43
  string constant PROJECT_NAME = "nana-core-v6";
44
44
 
45
45
  function getDeployment(string memory path) internal returns (CoreDeployment memory deployment) {
46
- // get chainId for which we need to get the deployment.
46
+ // Match the current chain ID to the Sphinx network name used in deployment artifacts.
47
47
  uint256 chainId = block.chainid;
48
48
 
49
- // Deploy to get the constants.
50
- // TODO: get constants without deploy.
49
+ // `SphinxConstants` exposes Sphinx's supported chain ID to network name mapping.
51
50
  SphinxConstants sphinxConstants = new SphinxConstants();
52
51
  NetworkInfo[] memory networks = sphinxConstants.getNetworkInfoArray();
53
52
 
@@ -1061,7 +1061,8 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1061
1061
  JBSplit memory split = splits[i];
1062
1062
 
1063
1063
  // Calculate the amount to send to the split.
1064
- uint256 splitTokenCount = mulDiv(tokenCount, split.percent, JBConstants.SPLITS_TOTAL_PERCENT);
1064
+ uint256 splitTokenCount =
1065
+ mulDiv({x: tokenCount, y: split.percent, denominator: JBConstants.SPLITS_TOTAL_PERCENT});
1065
1066
 
1066
1067
  // Mints tokens for the split if needed.
1067
1068
  if (splitTokenCount > 0) {
@@ -1264,8 +1265,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
1264
1265
  returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
1265
1266
  {
1266
1267
  // Compute the beneficiary's portion after removing the reserved share.
1267
- beneficiaryTokenCount =
1268
- mulDiv(tokenCount, JBConstants.MAX_RESERVED_PERCENT - reservedPercent, JBConstants.MAX_RESERVED_PERCENT);
1268
+ beneficiaryTokenCount = mulDiv({
1269
+ x: tokenCount,
1270
+ y: JBConstants.MAX_RESERVED_PERCENT - reservedPercent,
1271
+ denominator: JBConstants.MAX_RESERVED_PERCENT
1272
+ });
1269
1273
 
1270
1274
  // The remaining tokens are reserved.
1271
1275
  reservedTokenCount = tokenCount - beneficiaryTokenCount;
package/src/JBERC20.sol CHANGED
@@ -41,7 +41,7 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
41
41
 
42
42
  /// @notice The JBTokens contract that owns this token.
43
43
  /// @dev Set via `initialize` because JBERC20 is deployed before JBTokens (circular dependency).
44
- IJBTokens public TOKENS;
44
+ IJBTokens public tokens;
45
45
 
46
46
  //*********************************************************************//
47
47
  // -------------------- private stored properties -------------------- //
@@ -80,7 +80,7 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
80
80
  /// @notice Only the JBTokens contract can call this function.
81
81
  // forge-lint: disable-next-line(unwrapped-modifier-logic)
82
82
  modifier onlyTokens() {
83
- if (msg.sender != address(TOKENS)) revert JBERC20_Unauthorized({caller: msg.sender, tokens: address(TOKENS)});
83
+ if (msg.sender != address(tokens)) revert JBERC20_Unauthorized({caller: msg.sender, tokens: address(tokens)});
84
84
  _;
85
85
  }
86
86
 
@@ -130,11 +130,11 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
130
130
  /// @return magicValue `0x1626ba7e` if the signature is valid, `0xffffffff` otherwise.
131
131
  function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4 magicValue) {
132
132
  // Recover the signer from the signature. Return invalid if recovery fails.
133
- (address signer, ECDSA.RecoverError error,) = ECDSA.tryRecover(hash, signature);
133
+ (address signer, ECDSA.RecoverError error,) = ECDSA.tryRecover({hash: hash, signature: signature});
134
134
  if (error != ECDSA.RecoverError.NoError) return 0xffffffff;
135
135
 
136
136
  // Get the project ID this token belongs to.
137
- uint256 projectId = TOKENS.projectIdOf(IJBToken(address(this)));
137
+ uint256 projectId = tokens.projectIdOf(IJBToken(address(this)));
138
138
 
139
139
  // Get the project owner (the NFT holder).
140
140
  address projectOwner = PROJECTS.ownerOf(projectId);
@@ -195,8 +195,8 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
195
195
  /// @notice Initialize a new project token with the given name, symbol, and owner.
196
196
  /// @param name_ The token's name.
197
197
  /// @param symbol_ The token's symbol.
198
- /// @param tokens The JBTokens contract that manages this token.
199
- function initialize(string memory name_, string memory symbol_, address tokens) public override {
198
+ /// @param tokensAddress The JBTokens contract that manages this token.
199
+ function initialize(string memory name_, string memory symbol_, address tokensAddress) public override {
200
200
  // Prevent re-initialization by reverting if a name is already set or if the provided name is empty.
201
201
  if (bytes(_name).length != 0 || bytes(name_).length == 0) {
202
202
  revert JBERC20_AlreadyInitialized({
@@ -206,7 +206,7 @@ contract JBERC20 is ERC20Votes, ERC20Permit, JBPermissioned, IERC1271, IJBToken
206
206
 
207
207
  _name = name_;
208
208
  _symbol = symbol_;
209
- TOKENS = IJBTokens(tokens);
209
+ tokens = IJBTokens(tokensAddress);
210
210
  }
211
211
 
212
212
  //*********************************************************************//
@@ -5,12 +5,28 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
5
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6
6
 
7
7
  import {IJBFeelessAddresses} from "./interfaces/IJBFeelessAddresses.sol";
8
+ import {IJBFeelessHook} from "./interfaces/IJBFeelessHook.sol";
8
9
 
9
10
  /// @notice A registry of addresses exempt from the protocol's 2.5% fee. Feeless addresses don't incur fees on
10
11
  /// payouts they receive, surplus allowance they use, or cash outs where they are the beneficiary.
11
12
  /// @dev All feeless status is managed by the contract owner (typically the protocol multisig).
12
13
  /// @dev `projectId = 0` is the wildcard — an address feeless for project 0 is feeless for ALL projects.
13
14
  contract JBFeelessAddresses is Ownable, IJBFeelessAddresses, IERC165 {
15
+ //*********************************************************************//
16
+ // --------------------------- custom errors ------------------------- //
17
+ //*********************************************************************//
18
+
19
+ error JBFeelessAddresses_InvalidFeelessHook(IJBFeelessHook hook);
20
+
21
+ //*********************************************************************//
22
+ // --------------------- public stored properties -------------------- //
23
+ //*********************************************************************//
24
+
25
+ /// @notice Optional hook consulted (in addition to the static mappings) when computing feeless status.
26
+ /// @dev OR'd with the mappings — the hook can only widen the feeless set, never shrink it. `address(0)` disables
27
+ /// hook consultation.
28
+ IJBFeelessHook public override feelessHook;
29
+
14
30
  //*********************************************************************//
15
31
  // -------------------- internal stored properties ------------------- //
16
32
  //*********************************************************************//
@@ -53,19 +69,48 @@ contract JBFeelessAddresses is Ownable, IJBFeelessAddresses, IERC165 {
53
69
  emit SetFeelessAddress({projectId: projectId, addr: addr, isFeeless: flag, caller: _msgSender()});
54
70
  }
55
71
 
72
+ /// @notice Sets (or clears) the feeless hook consulted by `isFeelessFor`.
73
+ /// @dev Can only be called by this contract's owner (typically the protocol multisig).
74
+ /// @dev If `hook` is non-zero, it must report ERC-165 support for `IJBFeelessHook` or this call reverts.
75
+ /// @param hook The new hook. Pass `address(0)` to disable hook consultation.
76
+ function setFeelessHook(IJBFeelessHook hook) external virtual override onlyOwner {
77
+ if (address(hook) != address(0) && !hook.supportsInterface(type(IJBFeelessHook).interfaceId)) {
78
+ revert JBFeelessAddresses_InvalidFeelessHook(hook);
79
+ }
80
+
81
+ feelessHook = hook;
82
+
83
+ emit SetFeelessHook({hook: hook, caller: _msgSender()});
84
+ }
85
+
56
86
  //*********************************************************************//
57
- // -------------------------- public views --------------------------- //
87
+ // ------------------------- external views -------------------------- //
58
88
  //*********************************************************************//
59
89
 
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.
90
+ /// @notice Returns whether the specified address is feeless for a specific project, considering the wildcard
91
+ /// (projectId 0) feeless status, the project-specific feeless status, and the feeless hook (if set).
92
+ /// @dev The hook is invoked via try/catch — a reverting or out-of-gas hook is treated as `false` so it cannot
93
+ /// brick the fee path in terminals.
62
94
  /// @param addr The address to check.
63
95
  /// @param projectId The ID of the project to check.
64
- /// @return A flag indicating whether the address is feeless (globally or for the project).
96
+ /// @return A flag indicating whether the address is feeless (globally, for the project, or per the hook).
65
97
  function isFeelessFor(address addr, uint256 projectId) external view override returns (bool) {
66
- return _isFeelessFor[0][addr] || _isFeelessFor[projectId][addr];
98
+ if (_isFeelessFor[0][addr] || _isFeelessFor[projectId][addr]) return true;
99
+
100
+ IJBFeelessHook hook = feelessHook;
101
+ if (address(hook) == address(0)) return false;
102
+
103
+ try hook.isFeeless({projectId: projectId, addr: addr}) returns (bool result) {
104
+ return result;
105
+ } catch {
106
+ return false;
107
+ }
67
108
  }
68
109
 
110
+ //*********************************************************************//
111
+ // -------------------------- public views --------------------------- //
112
+ //*********************************************************************//
113
+
69
114
  /// @notice Indicates whether this contract adheres to the specified interface.
70
115
  /// @dev See {IERC165-supportsInterface}.
71
116
  /// @param interfaceId The ID of the interface to check for adherence to.
@@ -367,6 +367,31 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
367
367
 
368
368
  // Track the fee-free payout amount. During cashout at zero tax rate, fees apply
369
369
  // only up to this accumulated amount, preventing round-trip fee bypass.
370
+ // Revert on any self-referencing payout (the source project paying itself via a split),
371
+ // regardless of which terminal receives the call or which branch (pay vs add-to-balance)
372
+ // is taken. Both shapes are disguised owner actions that the payout pipeline must not
373
+ // silently authorize:
374
+ // - pay branch: the destination terminal's `pay()` mints new project tokens against
375
+ // the project's own surplus, diluting holders out-of-cycle and bypassing the
376
+ // ruleset's `allowOwnerMinting=false` guarantee. This holds even when the
377
+ // destination terminal is a different instance owned by the same project, because
378
+ // every registered terminal can mint via the terminal-as-minter pathway.
379
+ // - addToBalance branch: a same-project add-balance split shuffles surplus between
380
+ // the project's own terminals through the payout pipeline. The same effect is
381
+ // available via `addToBalanceOf` directly without the side effects (locked-split
382
+ // consumption, payout-limit drawdown, fee-free-surplus accounting); routing it
383
+ // through `sendPayoutsOf` is never the right surface.
384
+ // The try-catch in the split group lib catches this revert and restores the balance.
385
+ if (split.projectId == projectId) {
386
+ revert JBMultiTerminal_MintNotAllowed({
387
+ projectId: projectId, splitProjectId: split.projectId, terminal: address(terminal)
388
+ });
389
+ }
390
+
391
+ // Track the fee-free payout amount. During cashout at zero tax rate, fees apply
392
+ // only up to this accumulated amount, preventing round-trip fee bypass. Same-project
393
+ // splits would inflate this counter against the project's own future zero-tax cashouts
394
+ // but are already excluded by the self-reference revert above.
370
395
  if (terminal == this) {
371
396
  _feeFreeSurplusOf[split.projectId][token] += netPayoutAmount;
372
397
  }
@@ -384,17 +409,6 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
384
409
  metadata: metadata
385
410
  });
386
411
  } else {
387
- // Revert if this is a self-referencing payout (project paying itself via a split).
388
- // Same-project pay splits would mint tokens against existing balance without new funds entering.
389
- // Projects that want to mint should do so explicitly via the controller.
390
- // Cross-project pay splits on the same terminal are allowed (different project receives the funds).
391
- // The try-catch in the split group lib catches this revert and restores the balance.
392
- if (terminal == this && split.projectId == projectId) {
393
- revert JBMultiTerminal_MintNotAllowed({
394
- projectId: projectId, splitProjectId: split.projectId, terminal: address(terminal)
395
- });
396
- }
397
-
398
412
  // Keep a reference to the beneficiary of the payment.
399
413
  address beneficiary = split.beneficiary != address(0) ? split.beneficiary : originalMessageSender;
400
414
 
package/src/JBPrices.sol CHANGED
@@ -202,7 +202,7 @@ contract JBPrices is JBControlled, JBPermissioned, ERC2771Context, Ownable, IJBP
202
202
  // is in the range of ~1e9 to ~1e27 (for 18 decimals). Extreme prices outside this range may lose
203
203
  // significant precision due to fixed-point division truncation.
204
204
  if (feed != IJBPriceFeed(address(0))) {
205
- return mulDiv(10 ** decimals, 10 ** decimals, feed.currentUnitPrice(decimals));
205
+ return mulDiv({x: 10 ** decimals, y: 10 ** decimals, denominator: feed.currentUnitPrice(decimals)});
206
206
  }
207
207
 
208
208
  // Check for a default feed (project ID 0) if not found.
@@ -623,11 +623,11 @@ contract JBRulesets is JBControlled, IJBRulesets {
623
623
  {
624
624
  // A subsequent ruleset to one with a duration of 0 should have the next possible weight.
625
625
  if (baseRulesetDuration == 0) {
626
- return mulDiv(
627
- baseRulesetWeight,
628
- JBConstants.MAX_WEIGHT_CUT_PERCENT - baseRulesetWeightCutPercent,
629
- JBConstants.MAX_WEIGHT_CUT_PERCENT
630
- );
626
+ return mulDiv({
627
+ x: baseRulesetWeight,
628
+ y: JBConstants.MAX_WEIGHT_CUT_PERCENT - baseRulesetWeightCutPercent,
629
+ denominator: JBConstants.MAX_WEIGHT_CUT_PERCENT
630
+ });
631
631
  }
632
632
 
633
633
  // The weight should be based off the base ruleset's weight.
@@ -673,7 +673,7 @@ contract JBRulesets is JBControlled, IJBRulesets {
673
673
 
674
674
  for (uint256 i; i < weightCutMultiple;) {
675
675
  // Base the new weight on the specified ruleset's weight.
676
- weight = mulDiv(weight, cutFactor, maxPercent);
676
+ weight = mulDiv({x: weight, y: cutFactor, denominator: maxPercent});
677
677
 
678
678
  // The calculation doesn't need to continue if the weight is 0.
679
679
  if (weight == 0) break;
package/src/JBTokens.sol CHANGED
@@ -230,7 +230,7 @@ contract JBTokens is JBControlled, IJBTokens {
230
230
  });
231
231
 
232
232
  // Initialize the token.
233
- token.initialize({name: name, symbol: symbol, tokens: address(this)});
233
+ token.initialize({name: name, symbol: symbol, tokensAddress: address(this)});
234
234
  }
235
235
 
236
236
  /// @notice Create new tokens for a holder. If the project has an ERC-20 deployed, tokens are minted directly to
@@ -1,6 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
+ import {IJBFeelessHook} from "./IJBFeelessHook.sol";
5
+
4
6
  /// @notice Tracks addresses that are exempt from fees, both globally and on a per-project basis.
5
7
  /// @dev `projectId = 0` is the wildcard — an address feeless for project 0 is feeless for ALL projects.
6
8
  interface IJBFeelessAddresses {
@@ -11,11 +13,21 @@ interface IJBFeelessAddresses {
11
13
  /// @param caller The address that set the feeless status.
12
14
  event SetFeelessAddress(uint256 indexed projectId, address indexed addr, bool indexed isFeeless, address caller);
13
15
 
14
- /// @notice Returns whether the specified address is feeless for a specific project, considering both the wildcard
15
- /// (projectId 0) and project-specific feeless status.
16
+ /// @notice The optional hook (set by the owner) that can grant feeless status with arbitrary logic. Set to the
17
+ /// zero address to disable.
18
+ /// @param hook The new feeless hook. The zero address disables hook consultation.
19
+ /// @param caller The address that set the hook.
20
+ event SetFeelessHook(IJBFeelessHook indexed hook, address caller);
21
+
22
+ /// @notice The optional hook consulted (in addition to the static mappings) when computing feeless status.
23
+ /// @dev `address(0)` means no hook is set.
24
+ function feelessHook() external view returns (IJBFeelessHook);
25
+
26
+ /// @notice Returns whether the specified address is feeless for a specific project, considering the wildcard
27
+ /// (projectId 0) feeless status, the project-specific feeless status, and the feeless hook (if set).
16
28
  /// @param addr The address to check.
17
29
  /// @param projectId The ID of the project to check.
18
- /// @return A flag indicating whether the address is feeless (globally or for the project).
30
+ /// @return A flag indicating whether the address is feeless (globally, for the project, or per the hook).
19
31
  function isFeelessFor(address addr, uint256 projectId) external view returns (bool);
20
32
 
21
33
  /// @notice Sets whether an address is feeless globally (for all projects).
@@ -28,4 +40,8 @@ interface IJBFeelessAddresses {
28
40
  /// @param addr The address to set the feeless status of.
29
41
  /// @param flag A flag indicating whether the address should be feeless for the project.
30
42
  function setFeelessAddressFor(uint256 projectId, address addr, bool flag) external;
43
+
44
+ /// @notice Sets (or clears) the feeless hook consulted by `isFeelessFor`.
45
+ /// @param hook The new hook. Pass `address(0)` to disable hook consultation.
46
+ function setFeelessHook(IJBFeelessHook hook) external;
31
47
  }
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
5
+
6
+ /// @notice Optional hook that can grant feeless status to addresses based on arbitrary off-chain or on-chain logic.
7
+ /// @dev Plugged into `JBFeelessAddresses` by the contract owner. The hook is OR'd with the static feeless mappings,
8
+ /// so it can only widen the feeless set, never shrink it.
9
+ /// @dev `JBFeelessAddresses` invokes this hook inside a try/catch — a reverting hook is treated as returning `false`,
10
+ /// so a broken hook cannot brick the fee path in terminals.
11
+ interface IJBFeelessHook is IERC165 {
12
+ /// @notice Returns whether the address should be treated as feeless for the project.
13
+ /// @param projectId The ID of the project the fee would be charged on behalf of.
14
+ /// @param addr The address being checked (typically a payout recipient, surplus allowance beneficiary, or
15
+ /// cash-out beneficiary).
16
+ /// @return A flag indicating whether the address is feeless for the project under the hook's custom logic.
17
+ function isFeeless(uint256 projectId, address addr) external view returns (bool);
18
+ }
@@ -29,8 +29,8 @@ interface IJBToken {
29
29
  /// @notice Initializes the token with a name, symbol, and the JBTokens contract.
30
30
  /// @param name The token's name.
31
31
  /// @param symbol The token's symbol.
32
- /// @param tokens The JBTokens contract that manages this token.
33
- function initialize(string memory name, string memory symbol, address tokens) external;
32
+ /// @param tokensAddress The JBTokens contract that manages this token.
33
+ function initialize(string memory name, string memory symbol, address tokensAddress) external;
34
34
 
35
35
  /// @notice Mints tokens to an account.
36
36
  /// @param account The address to mint tokens to.
@@ -42,7 +42,7 @@ library JBCashOuts {
42
42
  if (cashOutCount >= totalSupply) return surplus;
43
43
 
44
44
  // Get a reference to the linear proportion.
45
- uint256 base = mulDiv(surplus, cashOutCount, totalSupply);
45
+ uint256 base = mulDiv({x: surplus, y: cashOutCount, denominator: totalSupply});
46
46
 
47
47
  // These conditions are all part of the same curve.
48
48
  // Edge conditions are separated to minimize the operations performed in those cases.
@@ -50,11 +50,12 @@ library JBCashOuts {
50
50
  return base;
51
51
  }
52
52
 
53
- return mulDiv(
54
- base,
55
- (JBConstants.MAX_CASH_OUT_TAX_RATE - cashOutTaxRate) + mulDiv(cashOutTaxRate, cashOutCount, totalSupply),
56
- JBConstants.MAX_CASH_OUT_TAX_RATE
57
- );
53
+ return mulDiv({
54
+ x: base,
55
+ y: (JBConstants.MAX_CASH_OUT_TAX_RATE - cashOutTaxRate)
56
+ + mulDiv({x: cashOutTaxRate, y: cashOutCount, denominator: totalSupply}),
57
+ denominator: JBConstants.MAX_CASH_OUT_TAX_RATE
58
+ });
58
59
  }
59
60
 
60
61
  /// @notice Returns the minimum number of tokens that must be cashed out to receive at least `desiredOutput` of
@@ -93,9 +94,9 @@ library JBCashOuts {
93
94
 
94
95
  // Linear case (no tax): out = surplus * c / totalSupply, so c = ceil(out * totalSupply / surplus).
95
96
  if (cashOutTaxRate == 0) {
96
- uint256 count = mulDiv(desiredOutput, totalSupply, surplus);
97
+ uint256 count = mulDiv({x: desiredOutput, y: totalSupply, denominator: surplus});
97
98
  // Round up if the floor division undershoots.
98
- if (mulDiv(surplus, count, totalSupply) < desiredOutput) count++;
99
+ if (mulDiv({x: surplus, y: count, denominator: totalSupply}) < desiredOutput) count++;
99
100
  return count;
100
101
  }
101
102
 
@@ -15,7 +15,7 @@ library JBFees {
15
15
  /// @param feePercent The fee percent, out of `JBConstants.MAX_FEE`.
16
16
  /// @return The fee amount, as a fixed point number with the same number of decimals as `amountBeforeFee`.
17
17
  function feeAmountFrom(uint256 amountBeforeFee, uint256 feePercent) internal pure returns (uint256) {
18
- return mulDiv(amountBeforeFee, feePercent, JBConstants.MAX_FEE);
18
+ return mulDiv({x: amountBeforeFee, y: feePercent, denominator: JBConstants.MAX_FEE});
19
19
  }
20
20
 
21
21
  /// @notice Returns the fee amount that, when added to `amountAfterFee`, produces the gross amount needed to yield
@@ -25,7 +25,8 @@ library JBFees {
25
25
  /// @param feePercent The fee percent, out of `JBConstants.MAX_FEE`.
26
26
  /// @return The fee amount, as a fixed point number with the same number of decimals as `amountAfterFee`.
27
27
  function feeAmountResultingIn(uint256 amountAfterFee, uint256 feePercent) internal pure returns (uint256) {
28
- return mulDiv(amountAfterFee, JBConstants.MAX_FEE, JBConstants.MAX_FEE - feePercent) - amountAfterFee;
28
+ return mulDiv({x: amountAfterFee, y: JBConstants.MAX_FEE, denominator: JBConstants.MAX_FEE - feePercent})
29
+ - amountAfterFee;
29
30
  }
30
31
 
31
32
  /// @notice Returns the standard protocol fee taken from `amountBeforeFee`.
@@ -45,6 +46,6 @@ library JBFees {
45
46
  /// @return The fee amount that, when added to `amountAfterFee`, yields the gross pre-fee amount.
46
47
  function standardFeeAmountResultingIn(uint256 amountAfterFee) internal pure returns (uint256) {
47
48
  // Use `mulDiv` instead of `amountAfterFee * 40 / 39` to preserve overflow safety.
48
- return mulDiv(amountAfterFee, 40, 39) - amountAfterFee;
49
+ return mulDiv({x: amountAfterFee, y: 40, denominator: 39}) - amountAfterFee;
49
50
  }
50
51
  }
@@ -141,7 +141,7 @@ library JBPayoutSplitGroupLib {
141
141
  // refund = amount * (netPayoutAmount - sent) / netPayoutAmount. For full consumption this branch is
142
142
  // skipped. For zero consumption this refunds the full `amount` (i.e. the gross, fee allocation included).
143
143
  if (sent < netPayoutAmount) {
144
- uint256 refund = mulDiv(amount, netPayoutAmount - sent, netPayoutAmount);
144
+ uint256 refund = mulDiv({x: amount, y: netPayoutAmount - sent, denominator: netPayoutAmount});
145
145
  if (refund != 0) {
146
146
  store.recordAddedBalanceFor({projectId: projectId, token: token, amount: refund});
147
147
  }
@@ -151,7 +151,7 @@ library JBPayoutSplitGroupLib {
151
151
  // gross-equivalent of what the hook actually consumed so the held fee scales with consumption rather
152
152
  // than with the project's original payout intent.
153
153
  if (netPayoutAmount < amount && sent != 0) {
154
- feeEligibleAmount = mulDiv(amount, sent, netPayoutAmount);
154
+ feeEligibleAmount = mulDiv({x: amount, y: sent, denominator: netPayoutAmount});
155
155
  }
156
156
  }
157
157
 
@@ -192,7 +192,7 @@ library JBPayoutSplitGroupLib {
192
192
  JBSplit memory split = payoutSplits[i];
193
193
 
194
194
  // The amount to send to the split.
195
- uint256 payoutAmount = mulDiv(leftoverAmount, split.percent, leftoverPercentage);
195
+ uint256 payoutAmount = mulDiv({x: leftoverAmount, y: split.percent, denominator: leftoverPercentage});
196
196
 
197
197
  // Send the payout (inlined to keep stack pressure manageable with the tuple return).
198
198
  // Returns (netPayoutAmount sent, feeEligible gross-equivalent). For non-hook splits and fully-consumed
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {IJBFeelessHook} from "../../src/interfaces/IJBFeelessHook.sol";
5
+
6
+ import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
7
+
8
+ /// @notice Configurable `IJBFeelessHook` mock for unit tests.
9
+ /// @dev `mode` controls behavior of `isFeeless`:
10
+ /// - 0: returns `defaultResult` for any (projectId, addr)
11
+ /// - 1: reverts with `Nope()`
12
+ /// - 2: reverts via `require(false, "nope")`
13
+ /// - 3: returns `true` iff `(projectId, addr)` has been allowlisted via `setAllowed`
14
+ contract MockFeelessHook is ERC165, IJBFeelessHook {
15
+ error Nope();
16
+
17
+ uint256 public mode;
18
+ bool public defaultResult;
19
+ mapping(uint256 => mapping(address => bool)) internal _allowed;
20
+
21
+ function setMode(uint256 newMode) external {
22
+ mode = newMode;
23
+ }
24
+
25
+ function setDefaultResult(bool result) external {
26
+ defaultResult = result;
27
+ }
28
+
29
+ function setAllowed(uint256 projectId, address addr, bool flag) external {
30
+ _allowed[projectId][addr] = flag;
31
+ }
32
+
33
+ function isFeeless(uint256 projectId, address addr) external view override returns (bool) {
34
+ if (mode == 1) revert Nope();
35
+ if (mode == 2) require(false, "nope");
36
+ if (mode == 3) return _allowed[projectId][addr];
37
+ return defaultResult;
38
+ }
39
+
40
+ function supportsInterface(bytes4 interfaceId) public view override(IERC165, ERC165) returns (bool) {
41
+ return interfaceId == type(IJBFeelessHook).interfaceId || super.supportsInterface(interfaceId);
42
+ }
43
+ }
44
+
45
+ /// @notice Non-conforming "hook" that returns false for the IJBFeelessHook interfaceId in ERC-165.
46
+ /// @dev Used to test the `setFeelessHook` validation guard. Has the `isFeeless` selector so the
47
+ /// call shape matches, but its `supportsInterface` advertises only IERC165, not IJBFeelessHook.
48
+ contract MockNonConformingFeelessHook is ERC165 {
49
+ function isFeeless(uint256, address) external pure returns (bool) {
50
+ return true;
51
+ }
52
+ }
53
+
54
+ /// @notice "Hook" with no `supportsInterface` at all — `setFeelessHook` should revert when this is passed.
55
+ contract MockEoaLikeFeelessHook {
56
+ function isFeeless(uint256, address) external pure returns (bool) {
57
+ return true;
58
+ }
59
+ }