@bananapus/core-v6 0.0.43 → 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.
- package/package.json +2 -2
- package/references/entrypoints.md +3 -1
- package/references/types-errors-events.md +2 -2
- package/src/JBController.sol +23 -18
- package/src/JBFeelessAddresses.sol +32 -13
- package/src/JBMultiTerminal.sol +25 -22
- package/src/JBTerminalStore.sol +10 -57
- package/src/interfaces/IJBFeelessAddresses.sol +17 -7
- package/src/libraries/JBRulesetMetadataResolver.sol +4 -4
- package/src/structs/JBBeforeCashOutRecordedContext.sol +3 -2
- package/src/structs/JBRulesetMetadata.sol +3 -3
- package/test/helpers/JBTest.sol +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@bananapus/permission-ids-v6": "0.0.22",
|
|
41
|
+
"@bananapus/permission-ids-v6": "^0.0.22",
|
|
42
42
|
"@chainlink/contracts": "1.5.0",
|
|
43
43
|
"@openzeppelin/contracts": "5.6.1",
|
|
44
44
|
"@prb/math": "4.1.1",
|
|
@@ -155,6 +155,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
155
155
|
|
|
156
156
|
| Function | What it does |
|
|
157
157
|
|----------|--------------|
|
|
158
|
-
| `setFeelessAddress(address addr, bool flag)` | Adds or removes an address from the fee exemption list. Owner-only. (`JBFeelessAddresses`) |
|
|
158
|
+
| `setFeelessAddress(address addr, bool flag)` | Adds or removes an address from the global (all-project) fee exemption list. Owner-only. (`JBFeelessAddresses`) |
|
|
159
|
+
| `setFeelessAddressFor(uint256 projectId, address addr, bool flag)` | Adds or removes an address from a project's fee exemption list. `projectId = 0` = global wildcard. Owner-only. (`JBFeelessAddresses`) |
|
|
160
|
+
| `isFeelessFor(address addr, uint256 projectId)` | Returns whether an address is feeless for a project (checks both wildcard and project-specific). (`JBFeelessAddresses`) |
|
|
159
161
|
| `setControllerAllowed(uint256 projectId)` | Returns whether a project's controller can currently be set. (`IJBDirectoryAccessControl`) |
|
|
160
162
|
| `setTerminalsAllowed(uint256 projectId)` | Returns whether a project's terminals can currently be set. (`IJBDirectoryAccessControl`) |
|
|
@@ -8,7 +8,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
|
|
|
8
8
|
|-------------|------------|---------|
|
|
9
9
|
| `JBRuleset` | `cycleNumber (uint48)`, `id (uint48)`, `basedOnId (uint48)`, `start (uint48)`, `duration (uint32)`, `weight (uint112)`, `weightCutPercent (uint32)`, `approvalHook`, `metadata (uint256)` | `currentOf()`, `recordPaymentFrom()`, `recordCashOutFor()` return values |
|
|
10
10
|
| `JBRulesetConfig` | `mustStartAtOrAfter (uint48)`, `duration (uint32)`, `weight (uint112)`, `weightCutPercent (uint32)`, `approvalHook`, `metadata (JBRulesetMetadata)`, `splitGroups[]`, `fundAccessLimitGroups[]` | `launchProjectFor()`, `queueRulesetsOf()` input |
|
|
11
|
-
| `JBRulesetMetadata` | `reservedPercent (uint16)`, `cashOutTaxRate (uint16)`, `baseCurrency (uint32)`, `pausePay`, `pauseCreditTransfers`, `allowOwnerMinting`, `allowSetCustomToken`, `allowTerminalMigration`, `allowSetTerminals`, `allowSetController`, `allowAddAccountingContext`, `allowAddPriceFeed`, `ownerMustSendPayouts`, `holdFees`, `
|
|
11
|
+
| `JBRulesetMetadata` | `reservedPercent (uint16)`, `cashOutTaxRate (uint16)`, `baseCurrency (uint32)`, `pausePay`, `pauseCreditTransfers`, `allowOwnerMinting`, `allowSetCustomToken`, `allowTerminalMigration`, `allowSetTerminals`, `allowSetController`, `allowAddAccountingContext`, `allowAddPriceFeed`, `ownerMustSendPayouts`, `holdFees`, `scopeCashOutsToLocalBalances`, `useDataHookForPay`, `useDataHookForCashOut`, `dataHook (address)`, `metadata (uint16)` | Packed into `JBRuleset.metadata` |
|
|
12
12
|
| `JBSplit` | `percent (uint32)`, `projectId (uint64)`, `beneficiary (address payable)`, `preferAddToBalance`, `lockedUntil (uint48)`, `hook (IJBSplitHook)` | `splitsOf()`, `setSplitGroupsOf()` |
|
|
13
13
|
| `JBSplitGroup` | `groupId (uint256)`, `splits (JBSplit[])` | `JBRulesetConfig.splitGroups`, `setSplitGroupsOf()` |
|
|
14
14
|
| `JBAccountingContext` | `token (address)`, `decimals (uint8)`, `currency (uint32)` | Terminal token accounting, surplus/reclaim calculations |
|
|
@@ -28,7 +28,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
|
|
|
28
28
|
| Struct | Key Fields | Used In |
|
|
29
29
|
|--------|------------|---------|
|
|
30
30
|
| `JBBeforePayRecordedContext` | `terminal`, `payer`, `amount (JBTokenAmount)`, `projectId`, `rulesetId`, `beneficiary`, `weight`, `reservedPercent`, `metadata` | `IJBRulesetDataHook.beforePayRecordedWith()` input |
|
|
31
|
-
| `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `
|
|
31
|
+
| `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `scopeCashOutsToLocalBalances`, `cashOutTaxRate`, `beneficiaryIsFeeless`, `metadata` | `IJBRulesetDataHook.beforeCashOutRecordedWith()` input |
|
|
32
32
|
| `JBAfterPayRecordedContext` | `payer`, `projectId`, `rulesetId`, `amount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `weight`, `newlyIssuedTokenCount`, `beneficiary`, `hookMetadata`, `payerMetadata` | `IJBPayHook.afterPayRecordedWith()` input |
|
|
33
33
|
| `JBAfterCashOutRecordedContext` | `holder`, `projectId`, `rulesetId`, `cashOutCount`, `reclaimedAmount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `cashOutTaxRate`, `beneficiary`, `hookMetadata`, `cashOutMetadata` | `IJBCashOutHook.afterCashOutRecordedWith()` input |
|
|
34
34
|
| `JBPayHookSpecification` | `hook (IJBPayHook)`, `noop (bool)`, `amount`, `metadata` | Returned by data hook; specifies which pay hooks to call and how much to forward. `noop = true` means informational-only (callback skipped, amount must be 0). |
|
package/src/JBController.sol
CHANGED
|
@@ -188,7 +188,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
188
188
|
{
|
|
189
189
|
// Enforce permissions.
|
|
190
190
|
_requirePermissionFrom({
|
|
191
|
-
account:
|
|
191
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.ADD_PRICE_FEED
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
JBRuleset memory ruleset = _currentRulesetOf(projectId);
|
|
@@ -317,7 +317,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
317
317
|
{
|
|
318
318
|
// Enforce permissions.
|
|
319
319
|
_requirePermissionFrom({
|
|
320
|
-
account:
|
|
320
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.DEPLOY_ERC20
|
|
321
321
|
});
|
|
322
322
|
|
|
323
323
|
// If a salt is provided, use it.
|
|
@@ -453,27 +453,25 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
453
453
|
address sender = _msgSender();
|
|
454
454
|
|
|
455
455
|
// Enforce permissions.
|
|
456
|
+
bool isOmnichainOperator = sender == OMNICHAIN_RULESET_OPERATOR;
|
|
456
457
|
_requirePermissionAllowingOverrideFrom({
|
|
457
|
-
account:
|
|
458
|
+
account: _ownerOf(projectId),
|
|
458
459
|
projectId: projectId,
|
|
459
460
|
permissionId: JBPermissionIds.LAUNCH_RULESETS,
|
|
460
|
-
alsoGrantAccessIf:
|
|
461
|
+
alsoGrantAccessIf: isOmnichainOperator
|
|
461
462
|
});
|
|
462
|
-
|
|
463
|
-
// Enforce permissions.
|
|
464
463
|
_requirePermissionAllowingOverrideFrom({
|
|
465
|
-
account:
|
|
464
|
+
account: _ownerOf(projectId),
|
|
466
465
|
projectId: projectId,
|
|
467
466
|
permissionId: JBPermissionIds.SET_TERMINALS,
|
|
468
|
-
alsoGrantAccessIf:
|
|
467
|
+
alsoGrantAccessIf: isOmnichainOperator
|
|
469
468
|
});
|
|
470
|
-
|
|
471
469
|
if (bytes(projectUri).length > 0) {
|
|
472
470
|
_requirePermissionAllowingOverrideFrom({
|
|
473
|
-
account:
|
|
471
|
+
account: _ownerOf(projectId),
|
|
474
472
|
projectId: projectId,
|
|
475
473
|
permissionId: JBPermissionIds.SET_PROJECT_URI,
|
|
476
|
-
alsoGrantAccessIf:
|
|
474
|
+
alsoGrantAccessIf: isOmnichainOperator
|
|
477
475
|
});
|
|
478
476
|
}
|
|
479
477
|
|
|
@@ -559,7 +557,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
559
557
|
// Minting is restricted to: the project's owner, addresses with permission to `MINT_TOKENS`, the project's
|
|
560
558
|
// terminals, and the project's data hook.
|
|
561
559
|
_requirePermissionAllowingOverrideFrom({
|
|
562
|
-
account:
|
|
560
|
+
account: _ownerOf(projectId),
|
|
563
561
|
projectId: projectId,
|
|
564
562
|
permissionId: JBPermissionIds.MINT_TOKENS,
|
|
565
563
|
alsoGrantAccessIf: senderIsTerminalOrDataHook || senderHasDataHookMintPermission
|
|
@@ -626,7 +624,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
626
624
|
|
|
627
625
|
// Enforce permissions.
|
|
628
626
|
_requirePermissionAllowingOverrideFrom({
|
|
629
|
-
account:
|
|
627
|
+
account: _ownerOf(projectId),
|
|
630
628
|
projectId: projectId,
|
|
631
629
|
permissionId: JBPermissionIds.QUEUE_RULESETS,
|
|
632
630
|
alsoGrantAccessIf: _msgSender() == OMNICHAIN_RULESET_OPERATOR
|
|
@@ -665,7 +663,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
665
663
|
{
|
|
666
664
|
// Enforce permissions.
|
|
667
665
|
_requirePermissionFrom({
|
|
668
|
-
account:
|
|
666
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_SPLIT_GROUPS
|
|
669
667
|
});
|
|
670
668
|
|
|
671
669
|
// Set the split groups.
|
|
@@ -679,7 +677,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
679
677
|
function setTokenFor(uint256 projectId, IJBToken token) external override {
|
|
680
678
|
// Enforce permissions.
|
|
681
679
|
_requirePermissionFrom({
|
|
682
|
-
account:
|
|
680
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN
|
|
683
681
|
});
|
|
684
682
|
|
|
685
683
|
// Get a reference to the current ruleset.
|
|
@@ -703,7 +701,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
703
701
|
function setTokenMetadataOf(uint256 projectId, string calldata name, string calldata symbol) external override {
|
|
704
702
|
// Enforce permissions.
|
|
705
703
|
_requirePermissionFrom({
|
|
706
|
-
account:
|
|
704
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN_METADATA
|
|
707
705
|
});
|
|
708
706
|
|
|
709
707
|
TOKENS.setTokenMetadataFor({projectId: projectId, name: name, symbol: symbol});
|
|
@@ -718,7 +716,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
718
716
|
function setUriOf(uint256 projectId, string calldata uri) external override {
|
|
719
717
|
// Enforce permissions.
|
|
720
718
|
_requirePermissionFrom({
|
|
721
|
-
account:
|
|
719
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_PROJECT_URI
|
|
722
720
|
});
|
|
723
721
|
|
|
724
722
|
// Set the project's metadata URI.
|
|
@@ -1197,7 +1195,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1197
1195
|
JBRuleset memory ruleset = _currentRulesetOf(projectId);
|
|
1198
1196
|
|
|
1199
1197
|
// Get a reference to the project's owner.
|
|
1200
|
-
address owner =
|
|
1198
|
+
address owner = _ownerOf(projectId);
|
|
1201
1199
|
|
|
1202
1200
|
// Reset the pending reserved token balance.
|
|
1203
1201
|
pendingReservedTokenBalanceOf[projectId] = 0;
|
|
@@ -1324,6 +1322,13 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1324
1322
|
return ERC2771Context._msgSender();
|
|
1325
1323
|
}
|
|
1326
1324
|
|
|
1325
|
+
/// @notice The owner of a project.
|
|
1326
|
+
/// @param projectId The ID of the project to get the owner of.
|
|
1327
|
+
/// @return The owner of the project.
|
|
1328
|
+
function _ownerOf(uint256 projectId) internal view returns (address) {
|
|
1329
|
+
return PROJECTS.ownerOf(projectId);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1327
1332
|
/// @notice The project's upcoming ruleset.
|
|
1328
1333
|
/// @param projectId The ID of the project to check.
|
|
1329
1334
|
/// @return The project's upcoming ruleset.
|
|
@@ -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.
|
|
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
|
-
//
|
|
15
|
+
// -------------------- internal stored properties ------------------- //
|
|
15
16
|
//*********************************************************************//
|
|
16
17
|
|
|
17
|
-
/// @notice
|
|
18
|
-
/// @dev
|
|
19
|
-
|
|
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.
|
|
36
|
-
///
|
|
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
|
-
|
|
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.
|
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -334,7 +334,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
334
334
|
|
|
335
335
|
// This payout is eligible for a fee since the funds are leaving this contract and the split hook isn't a
|
|
336
336
|
// feeless address.
|
|
337
|
-
if (!_isFeeless(address(split.hook))) {
|
|
337
|
+
if (!_isFeeless({addr: address(split.hook), projectId: projectId})) {
|
|
338
338
|
unchecked {
|
|
339
339
|
netPayoutAmount -= _feeAmountFrom(amount);
|
|
340
340
|
}
|
|
@@ -377,7 +377,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
377
377
|
// the fee model taxes value leaving the protocol ecosystem, not internal rebalancing.
|
|
378
378
|
// This payout is eligible for a fee if the funds are leaving this contract and the receiving terminal isn't
|
|
379
379
|
// a feeless address.
|
|
380
|
-
if (terminal != this && !_isFeeless(address(terminal))) {
|
|
380
|
+
if (terminal != this && !_isFeeless({addr: address(terminal), projectId: projectId})) {
|
|
381
381
|
unchecked {
|
|
382
382
|
netPayoutAmount -= _feeAmountFrom(amount);
|
|
383
383
|
}
|
|
@@ -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
|
}
|
|
@@ -546,7 +546,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
546
546
|
// Migration to a non-feeless terminal incurs the standard 2.5% fee, same as any other fund egress.
|
|
547
547
|
// This also settles any fee-free surplus liability that would otherwise be lost on the new terminal.
|
|
548
548
|
uint256 feeAmount;
|
|
549
|
-
if (!_isFeeless(address(to)) && projectId != _FEE_BENEFICIARY_PROJECT_ID) {
|
|
549
|
+
if (!_isFeeless({addr: address(to), projectId: projectId}) && projectId != _FEE_BENEFICIARY_PROJECT_ID) {
|
|
550
550
|
feeAmount = _takeFeeFrom({
|
|
551
551
|
projectId: projectId,
|
|
552
552
|
token: token,
|
|
@@ -904,16 +904,16 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
904
904
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
905
905
|
)
|
|
906
906
|
{
|
|
907
|
-
(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
+
});
|
|
917
917
|
}
|
|
918
918
|
|
|
919
919
|
/// @notice Simulates a payment without modifying state — use this to preview how many project tokens a payer
|
|
@@ -1155,7 +1155,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1155
1155
|
uint256 cashOutTaxRate;
|
|
1156
1156
|
|
|
1157
1157
|
// Cache whether the beneficiary is feeless.
|
|
1158
|
-
bool beneficiaryIsFeeless = _isFeeless(beneficiary);
|
|
1158
|
+
bool beneficiaryIsFeeless = _isFeeless({addr: beneficiary, projectId: projectId});
|
|
1159
1159
|
|
|
1160
1160
|
{
|
|
1161
1161
|
// Cache the controller to avoid a redundant external call (also used inside STORE.recordCashOutFor).
|
|
@@ -1445,8 +1445,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1445
1445
|
}
|
|
1446
1446
|
|
|
1447
1447
|
// Get the fee for the specified amount.
|
|
1448
|
-
uint256 specificationAmountFee =
|
|
1449
|
-
|
|
1448
|
+
uint256 specificationAmountFee = _isFeeless({addr: address(specification.hook), projectId: projectId})
|
|
1449
|
+
? 0
|
|
1450
|
+
: _feeAmountFrom(specification.amount);
|
|
1450
1451
|
|
|
1451
1452
|
// Add the specification's amount to the amount eligible for fees.
|
|
1452
1453
|
if (specificationAmountFee != 0) {
|
|
@@ -1842,7 +1843,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1842
1843
|
// Send any leftover funds to the project owner and update the fee tracking accordingly.
|
|
1843
1844
|
if (leftoverPayoutAmount != 0) {
|
|
1844
1845
|
// Keep a reference to the fee for the leftover payout amount.
|
|
1845
|
-
uint256 fee =
|
|
1846
|
+
uint256 fee =
|
|
1847
|
+
_isFeeless({addr: projectOwner, projectId: projectId}) ? 0 : _feeAmountFrom(leftoverPayoutAmount);
|
|
1846
1848
|
|
|
1847
1849
|
uint256 netLeftoverPayoutAmount;
|
|
1848
1850
|
unchecked {
|
|
@@ -2025,7 +2027,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2025
2027
|
// Take a fee from the `amountPaidOut`, if needed.
|
|
2026
2028
|
// The net amount is the final amount withdrawn after the fee has been taken.
|
|
2027
2029
|
netAmountPaidOut = amountPaidOut
|
|
2028
|
-
- (_isFeeless(owner) || _isFeeless(beneficiary)
|
|
2030
|
+
- (_isFeeless({addr: owner, projectId: projectId}) || _isFeeless({addr: beneficiary, projectId: projectId})
|
|
2029
2031
|
? 0
|
|
2030
2032
|
: _takeFeeFrom({
|
|
2031
2033
|
projectId: projectId,
|
|
@@ -2113,9 +2115,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
2113
2115
|
|
|
2114
2116
|
/// @notice Returns a flag indicating if interacting with an address should not incur fees.
|
|
2115
2117
|
/// @param addr The address to check.
|
|
2116
|
-
/// @
|
|
2117
|
-
|
|
2118
|
-
|
|
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});
|
|
2119
2122
|
}
|
|
2120
2123
|
|
|
2121
2124
|
/// @notice The calldata. Preferred to use over `msg.data`.
|
package/src/JBTerminalStore.sol
CHANGED
|
@@ -847,57 +847,6 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
847
847
|
// -------------------------- internal views ------------------------- //
|
|
848
848
|
//*********************************************************************//
|
|
849
849
|
|
|
850
|
-
/// @notice Computes the surplus relevant for a cash out (total or local, depending on ruleset flag).
|
|
851
|
-
/// @dev When `useTotalSurplusForCashOuts` is enabled, surplus is aggregated from ALL registered terminals without
|
|
852
|
-
/// validation. Projects MUST only register trusted terminals — an untrusted terminal can over-report surplus and
|
|
853
|
-
/// cause the executing terminal to overpay cash-outs.
|
|
854
|
-
/// @param terminal The terminal recording the cash out.
|
|
855
|
-
/// @param projectId The ID of the project to cash out from.
|
|
856
|
-
/// @param tokenToReclaim The token to reclaim.
|
|
857
|
-
/// @param ruleset The ruleset during the cash out.
|
|
858
|
-
/// @return The surplus amount in the token's native decimals and currency.
|
|
859
|
-
function _cashOutSurplusOf(
|
|
860
|
-
address terminal,
|
|
861
|
-
uint256 projectId,
|
|
862
|
-
address tokenToReclaim,
|
|
863
|
-
JBRuleset memory ruleset
|
|
864
|
-
)
|
|
865
|
-
internal
|
|
866
|
-
view
|
|
867
|
-
returns (uint256)
|
|
868
|
-
{
|
|
869
|
-
// Look up the accounting context (decimals, currency) for the token being reclaimed at this terminal.
|
|
870
|
-
JBAccountingContext memory accountingContext = _accountingContextForTokenOf[terminal][projectId][tokenToReclaim];
|
|
871
|
-
|
|
872
|
-
// If the ruleset uses total surplus, aggregate across ALL terminals and ALL tokens.
|
|
873
|
-
if (ruleset.useTotalSurplusForCashOuts()) {
|
|
874
|
-
return JBSurplus.currentSurplusOf({
|
|
875
|
-
projectId: projectId,
|
|
876
|
-
// Get every terminal the project has registered.
|
|
877
|
-
terminals: DIRECTORY.terminalsOf(projectId),
|
|
878
|
-
// Empty tokens array = include all tokens at each terminal.
|
|
879
|
-
tokens: new address[](0),
|
|
880
|
-
// Express the result in the reclaimed token's decimals and currency.
|
|
881
|
-
decimals: accountingContext.decimals,
|
|
882
|
-
currency: accountingContext.currency
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// Otherwise, only account for the specific token's surplus at this terminal.
|
|
887
|
-
JBAccountingContext[] memory singleContext = new JBAccountingContext[](1);
|
|
888
|
-
singleContext[0] = accountingContext;
|
|
889
|
-
|
|
890
|
-
// Compute surplus from only this terminal using only the reclaimed token's balance.
|
|
891
|
-
return _surplusFrom({
|
|
892
|
-
terminal: terminal,
|
|
893
|
-
projectId: projectId,
|
|
894
|
-
accountingContexts: singleContext,
|
|
895
|
-
ruleset: ruleset,
|
|
896
|
-
targetDecimals: accountingContext.decimals,
|
|
897
|
-
targetCurrency: accountingContext.currency
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
|
|
901
850
|
/// @notice Calls the data hook, validates noop specifications, and computes the bonding curve reclaim amount.
|
|
902
851
|
/// @dev Extracted from `_computeCashOutFrom` to keep it under the EVM stack depth limit (16 slots).
|
|
903
852
|
/// @param ruleset The current ruleset (used to resolve the data hook address).
|
|
@@ -979,14 +928,18 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
979
928
|
// Get a reference to the project's current ruleset.
|
|
980
929
|
ruleset = RULESETS.currentOf(projectId);
|
|
981
930
|
|
|
982
|
-
// Get the project's current surplus for the token being reclaimed.
|
|
983
|
-
uint256 surplus = _cashOutSurplusOf({
|
|
984
|
-
terminal: terminal, projectId: projectId, tokenToReclaim: tokenToReclaim, ruleset: ruleset
|
|
985
|
-
});
|
|
986
|
-
|
|
987
931
|
// Get the accounting context for the token being reclaimed.
|
|
988
932
|
JBAccountingContext memory accountingContext = _accountingContextForTokenOf[terminal][projectId][tokenToReclaim];
|
|
989
933
|
|
|
934
|
+
// Get the project's current surplus across ALL terminals and ALL tokens.
|
|
935
|
+
uint256 surplus = JBSurplus.currentSurplusOf({
|
|
936
|
+
projectId: projectId,
|
|
937
|
+
terminals: DIRECTORY.terminalsOf(projectId),
|
|
938
|
+
tokens: new address[](0),
|
|
939
|
+
decimals: accountingContext.decimals,
|
|
940
|
+
currency: accountingContext.currency
|
|
941
|
+
});
|
|
942
|
+
|
|
990
943
|
// Get the total number of outstanding project tokens.
|
|
991
944
|
uint256 effectiveTotalSupply =
|
|
992
945
|
IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
|
|
@@ -1019,7 +972,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1019
972
|
decimals: accountingContext.decimals,
|
|
1020
973
|
currency: accountingContext.currency
|
|
1021
974
|
});
|
|
1022
|
-
context.
|
|
975
|
+
context.scopeCashOutsToLocalBalances = ruleset.scopeCashOutsToLocalBalances();
|
|
1023
976
|
context.cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
1024
977
|
context.beneficiaryIsFeeless = beneficiaryIsFeeless;
|
|
1025
978
|
context.metadata = metadata;
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
-
/// @notice Tracks addresses that are exempt from fees.
|
|
4
|
+
/// @notice Tracks addresses that are exempt from fees, both globally and on a per-project basis.
|
|
5
|
+
/// @dev `projectId = 0` is the wildcard — an address feeless for project 0 is feeless for ALL projects.
|
|
5
6
|
interface IJBFeelessAddresses {
|
|
6
|
-
/// @notice An address's feeless status was set.
|
|
7
|
+
/// @notice An address's feeless status was set for a project (or globally if projectId is 0).
|
|
8
|
+
/// @param projectId The project the feeless status applies to. 0 means all projects.
|
|
7
9
|
/// @param addr The address whose feeless status was set.
|
|
8
10
|
/// @param isFeeless Whether the address is feeless.
|
|
9
11
|
/// @param caller The address that set the feeless status.
|
|
10
|
-
event SetFeelessAddress(address indexed addr, bool indexed isFeeless, address caller);
|
|
12
|
+
event SetFeelessAddress(uint256 indexed projectId, address indexed addr, bool indexed isFeeless, address caller);
|
|
11
13
|
|
|
12
|
-
/// @notice Returns whether the specified address is feeless
|
|
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.
|
|
13
16
|
/// @param addr The address to check.
|
|
14
|
-
/// @
|
|
15
|
-
|
|
17
|
+
/// @param projectId The ID of the project to check.
|
|
18
|
+
/// @return A flag indicating whether the address is feeless (globally or for the project).
|
|
19
|
+
function isFeelessFor(address addr, uint256 projectId) external view returns (bool);
|
|
16
20
|
|
|
17
|
-
/// @notice Sets whether an address is feeless.
|
|
21
|
+
/// @notice Sets whether an address is feeless globally (for all projects).
|
|
18
22
|
/// @param addr The address to set the feeless status of.
|
|
19
23
|
/// @param flag A flag indicating whether the address should be feeless.
|
|
20
24
|
function setFeelessAddress(address addr, bool flag) external;
|
|
25
|
+
|
|
26
|
+
/// @notice Sets whether an address is feeless for a specific project.
|
|
27
|
+
/// @param projectId The ID of the project. 0 means all projects (same as `setFeelessAddress`).
|
|
28
|
+
/// @param addr The address to set the feeless status of.
|
|
29
|
+
/// @param flag A flag indicating whether the address should be feeless for the project.
|
|
30
|
+
function setFeelessAddressFor(uint256 projectId, address addr, bool flag) external;
|
|
21
31
|
}
|
|
@@ -67,7 +67,7 @@ library JBRulesetMetadataResolver {
|
|
|
67
67
|
return ((ruleset.metadata >> 78) & 1) == 1;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function
|
|
70
|
+
function scopeCashOutsToLocalBalances(JBRuleset memory ruleset) internal pure returns (bool) {
|
|
71
71
|
return ((ruleset.metadata >> 79) & 1) == 1;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -123,8 +123,8 @@ library JBRulesetMetadataResolver {
|
|
|
123
123
|
if (rulesetMetadata.ownerMustSendPayouts) packed |= 1 << 77;
|
|
124
124
|
// hold fees in bit 78.
|
|
125
125
|
if (rulesetMetadata.holdFees) packed |= 1 << 78;
|
|
126
|
-
//
|
|
127
|
-
if (rulesetMetadata.
|
|
126
|
+
// scopeCashOutsToLocalBalances in bit 79.
|
|
127
|
+
if (rulesetMetadata.scopeCashOutsToLocalBalances) packed |= 1 << 79;
|
|
128
128
|
// use pay data source in bit 80.
|
|
129
129
|
if (rulesetMetadata.useDataHookForPay) packed |= 1 << 80;
|
|
130
130
|
// use cash out data source in bit 81.
|
|
@@ -154,7 +154,7 @@ library JBRulesetMetadataResolver {
|
|
|
154
154
|
allowAddPriceFeed: allowAddPriceFeed(ruleset),
|
|
155
155
|
ownerMustSendPayouts: ownerMustSendPayouts(ruleset),
|
|
156
156
|
holdFees: holdFees(ruleset),
|
|
157
|
-
|
|
157
|
+
scopeCashOutsToLocalBalances: scopeCashOutsToLocalBalances(ruleset),
|
|
158
158
|
useDataHookForPay: useDataHookForPay(ruleset),
|
|
159
159
|
useDataHookForCashOut: useDataHookForCashOut(ruleset),
|
|
160
160
|
dataHook: dataHook(ruleset),
|
|
@@ -14,7 +14,8 @@ import {JBTokenAmount} from "./JBTokenAmount.sol";
|
|
|
14
14
|
/// @custom:member surplus The surplus amount used for the calculation, as a fixed point number with 18 decimals.
|
|
15
15
|
/// Includes the token of the surplus, the surplus value, the number of decimals
|
|
16
16
|
/// included, and the currency of the surplus.
|
|
17
|
-
/// @custom:member
|
|
17
|
+
/// @custom:member scopeCashOutsToLocalBalances If true, omnichain hooks should use only local chain balances for cash
|
|
18
|
+
/// outs (skip cross-chain aggregation).
|
|
18
19
|
/// @custom:member cashOutTaxRate The cash out tax rate of the ruleset the cash out is made during, out of
|
|
19
20
|
/// `JBConstants.MAX_CASH_OUT_TAX_RATE`.
|
|
20
21
|
/// @custom:member beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address. Useful for data hooks
|
|
@@ -29,7 +30,7 @@ struct JBBeforeCashOutRecordedContext {
|
|
|
29
30
|
uint256 cashOutCount;
|
|
30
31
|
uint256 totalSupply;
|
|
31
32
|
JBTokenAmount surplus;
|
|
32
|
-
bool
|
|
33
|
+
bool scopeCashOutsToLocalBalances;
|
|
33
34
|
uint256 cashOutTaxRate;
|
|
34
35
|
bool beneficiaryIsFeeless;
|
|
35
36
|
bytes metadata;
|
|
@@ -21,8 +21,8 @@ pragma solidity ^0.8.0;
|
|
|
21
21
|
/// @custom:member allowAddPriceFeed If `true`, the project can register new price feeds in `JBPrices`.
|
|
22
22
|
/// @custom:member ownerMustSendPayouts If `true`, only the project owner can trigger payout distribution.
|
|
23
23
|
/// @custom:member holdFees If `true`, fees are accumulated but not processed until a future ruleset (or manually).
|
|
24
|
-
/// @custom:member
|
|
25
|
-
///
|
|
24
|
+
/// @custom:member scopeCashOutsToLocalBalances If `true`, omnichain cash-out calculations use only the local chain's
|
|
25
|
+
/// balances (not cross-chain aggregates).
|
|
26
26
|
/// @custom:member useDataHookForPay If `true`, the data hook is called before recording payments.
|
|
27
27
|
/// @custom:member useDataHookForCashOut If `true`, the data hook is called before recording cash outs.
|
|
28
28
|
/// @custom:member dataHook Contract called before pay/cash-out to potentially override token counts or add hooks.
|
|
@@ -42,7 +42,7 @@ struct JBRulesetMetadata {
|
|
|
42
42
|
bool allowAddPriceFeed;
|
|
43
43
|
bool ownerMustSendPayouts;
|
|
44
44
|
bool holdFees;
|
|
45
|
-
bool
|
|
45
|
+
bool scopeCashOutsToLocalBalances;
|
|
46
46
|
bool useDataHookForPay;
|
|
47
47
|
bool useDataHookForCashOut;
|
|
48
48
|
address dataHook;
|
package/test/helpers/JBTest.sol
CHANGED
|
@@ -43,7 +43,7 @@ contract JBTest is Test {
|
|
|
43
43
|
allowAddAccountingContext: true,
|
|
44
44
|
allowAddPriceFeed: true,
|
|
45
45
|
holdFees: false,
|
|
46
|
-
|
|
46
|
+
scopeCashOutsToLocalBalances: true,
|
|
47
47
|
useDataHookForPay: false,
|
|
48
48
|
useDataHookForCashOut: false,
|
|
49
49
|
dataHook: address(0),
|
|
@@ -81,7 +81,7 @@ contract JBTest is Test {
|
|
|
81
81
|
allowAddAccountingContext: true,
|
|
82
82
|
allowAddPriceFeed: false,
|
|
83
83
|
holdFees: false,
|
|
84
|
-
|
|
84
|
+
scopeCashOutsToLocalBalances: false,
|
|
85
85
|
useDataHookForPay: false,
|
|
86
86
|
useDataHookForCashOut: false,
|
|
87
87
|
dataHook: address(0),
|
|
@@ -105,7 +105,7 @@ contract JBTest is Test {
|
|
|
105
105
|
allowAddAccountingContext: false,
|
|
106
106
|
allowAddPriceFeed: false,
|
|
107
107
|
holdFees: true,
|
|
108
|
-
|
|
108
|
+
scopeCashOutsToLocalBalances: true,
|
|
109
109
|
useDataHookForPay: false,
|
|
110
110
|
useDataHookForCashOut: false,
|
|
111
111
|
dataHook: address(0),
|