@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 +1 -1
- package/script/helpers/CoreDeploymentLib.sol +2 -3
- package/src/JBController.sol +7 -3
- package/src/JBERC20.sol +7 -7
- package/src/JBFeelessAddresses.sol +50 -5
- package/src/JBMultiTerminal.sol +25 -11
- package/src/JBPrices.sol +1 -1
- package/src/JBRulesets.sol +6 -6
- package/src/JBTokens.sol +1 -1
- package/src/interfaces/IJBFeelessAddresses.sol +19 -3
- package/src/interfaces/IJBFeelessHook.sol +18 -0
- package/src/interfaces/IJBToken.sol +2 -2
- package/src/libraries/JBCashOuts.sol +9 -8
- package/src/libraries/JBFees.sol +4 -3
- package/src/libraries/JBPayoutSplitGroupLib.sol +3 -3
- package/test/mock/MockFeelessHook.sol +59 -0
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
46
|
+
// Match the current chain ID to the Sphinx network name used in deployment artifacts.
|
|
47
47
|
uint256 chainId = block.chainid;
|
|
48
48
|
|
|
49
|
-
//
|
|
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
|
|
package/src/JBController.sol
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
199
|
-
function initialize(string memory name_, string memory symbol_, address
|
|
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
|
-
|
|
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
|
-
//
|
|
87
|
+
// ------------------------- external views -------------------------- //
|
|
58
88
|
//*********************************************************************//
|
|
59
89
|
|
|
60
|
-
/// @notice Returns whether the specified address is feeless for a specific project, considering
|
|
61
|
-
/// (projectId 0)
|
|
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
|
|
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
|
-
|
|
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.
|
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -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.
|
package/src/JBRulesets.sol
CHANGED
|
@@ -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,
|
|
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
|
|
15
|
-
///
|
|
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
|
|
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
|
|
33
|
-
function initialize(string memory name, string memory symbol, address
|
|
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)
|
|
56
|
-
|
|
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
|
|
package/src/libraries/JBFees.sol
CHANGED
|
@@ -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)
|
|
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
|
+
}
|