@bananapus/core-v6 0.0.16 → 0.0.18
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/ADMINISTRATION.md +1 -1
- package/ARCHITECTURE.md +2 -1
- package/AUDIT_INSTRUCTIONS.md +342 -0
- package/CHANGE_LOG.md +400 -0
- package/README.md +4 -4
- package/RISKS.md +171 -50
- package/SKILLS.md +9 -6
- package/USER_JOURNEYS.md +622 -0
- package/package.json +2 -2
- package/script/DeployPeriphery.s.sol +7 -1
- package/src/JBController.sol +5 -0
- package/src/JBDeadline.sol +3 -0
- package/src/JBDirectory.sol +2 -1
- package/src/JBMultiTerminal.sol +50 -9
- package/src/JBPermissions.sol +2 -0
- package/src/JBPrices.sol +8 -2
- package/src/JBRulesets.sol +3 -0
- package/src/JBSplits.sol +9 -5
- package/src/JBTerminalStore.sol +54 -47
- package/src/JBTokens.sol +3 -0
- package/src/interfaces/IJBTerminalStore.sol +3 -0
- package/src/libraries/JBFees.sol +2 -0
- package/src/libraries/JBMetadataResolver.sol +17 -4
- package/src/structs/JBBeforeCashOutRecordedContext.sol +4 -0
- package/test/TestAuditResponseDesignProofs.sol +434 -0
- package/test/TestDataHookFuzzing.sol +520 -0
- package/test/TestFeeFreeCashOutBypass.sol +617 -0
- package/test/TestL2SequencerPriceFeed.sol +292 -0
- package/test/TestMetadataOffsetOverflow.sol +179 -0
- package/test/TestMultiTerminalSurplus.sol +348 -0
- package/test/TestPermit2DataHook.t.sol +360 -0
- package/test/TestRulesetQueueing.sol +1 -2
- package/test/TestRulesetWeightCaching.sol +122 -124
- package/test/WeirdTokenTests.t.sol +37 -0
- package/test/regression/HoldFeesCashOutReserved.t.sol +415 -0
- package/test/regression/WeightCacheBoundary.t.sol +291 -0
- package/test/units/static/JBMultiTerminal/TestAddToBalanceOf.sol +2 -2
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +18 -17
- package/test/units/static/JBMultiTerminal/TestPay.sol +6 -4
- package/test/units/static/JBMultiTerminal/TestProcessHeldFeesOf.sol +206 -18
- package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +280 -0
- package/test/units/static/JBSplits/TestSelfManagedSplitGroups.sol +55 -12
- package/test/units/static/JBTerminalStore/TestRecordCashOutsFor.sol +72 -0
- package/docs/book.css +0 -13
- package/docs/book.toml +0 -12
- package/docs/solidity.min.js +0 -74
- package/docs/src/README.md +0 -703
- package/docs/src/SUMMARY.md +0 -94
- package/docs/src/src/JBChainlinkV3PriceFeed.sol/contract.JBChainlinkV3PriceFeed.md +0 -83
- package/docs/src/src/JBChainlinkV3SequencerPriceFeed.sol/contract.JBChainlinkV3SequencerPriceFeed.md +0 -88
- package/docs/src/src/JBController.sol/contract.JBController.md +0 -1121
- package/docs/src/src/JBDeadline.sol/contract.JBDeadline.md +0 -84
- package/docs/src/src/JBDirectory.sol/contract.JBDirectory.md +0 -294
- package/docs/src/src/JBERC20.sol/contract.JBERC20.md +0 -190
- package/docs/src/src/JBFeelessAddresses.sol/contract.JBFeelessAddresses.md +0 -80
- package/docs/src/src/JBFundAccessLimits.sol/contract.JBFundAccessLimits.md +0 -253
- package/docs/src/src/JBMultiTerminal.sol/contract.JBMultiTerminal.md +0 -1472
- package/docs/src/src/JBPermissions.sol/contract.JBPermissions.md +0 -199
- package/docs/src/src/JBPrices.sol/contract.JBPrices.md +0 -154
- package/docs/src/src/JBProjects.sol/contract.JBProjects.md +0 -131
- package/docs/src/src/JBRulesets.sol/contract.JBRulesets.md +0 -677
- package/docs/src/src/JBSplits.sol/contract.JBSplits.md +0 -237
- package/docs/src/src/JBTerminalStore.sol/contract.JBTerminalStore.md +0 -591
- package/docs/src/src/JBTokens.sol/contract.JBTokens.md +0 -353
- package/docs/src/src/README.md +0 -25
- package/docs/src/src/abstract/JBControlled.sol/abstract.JBControlled.md +0 -64
- package/docs/src/src/abstract/JBPermissioned.sol/abstract.JBPermissioned.md +0 -84
- package/docs/src/src/abstract/README.md +0 -5
- package/docs/src/src/enums/JBApprovalStatus.sol/enum.JBApprovalStatus.md +0 -17
- package/docs/src/src/enums/README.md +0 -4
- package/docs/src/src/interfaces/IJBCashOutHook.sol/interface.IJBCashOutHook.md +0 -29
- package/docs/src/src/interfaces/IJBCashOutTerminal.sol/interface.IJBCashOutTerminal.md +0 -57
- package/docs/src/src/interfaces/IJBControlled.sol/interface.IJBControlled.md +0 -12
- package/docs/src/src/interfaces/IJBController.sol/interface.IJBController.md +0 -334
- package/docs/src/src/interfaces/IJBDirectory.sol/interface.IJBDirectory.md +0 -108
- package/docs/src/src/interfaces/IJBDirectoryAccessControl.sol/interface.IJBDirectoryAccessControl.md +0 -19
- package/docs/src/src/interfaces/IJBFeeTerminal.sol/interface.IJBFeeTerminal.md +0 -91
- package/docs/src/src/interfaces/IJBFeelessAddresses.sol/interface.IJBFeelessAddresses.md +0 -26
- package/docs/src/src/interfaces/IJBFundAccessLimits.sol/interface.IJBFundAccessLimits.md +0 -88
- package/docs/src/src/interfaces/IJBMigratable.sol/interface.IJBMigratable.md +0 -29
- package/docs/src/src/interfaces/IJBMultiTerminal.sol/interface.IJBMultiTerminal.md +0 -50
- package/docs/src/src/interfaces/IJBPayHook.sol/interface.IJBPayHook.md +0 -28
- package/docs/src/src/interfaces/IJBPayoutTerminal.sol/interface.IJBPayoutTerminal.md +0 -105
- package/docs/src/src/interfaces/IJBPermissioned.sol/interface.IJBPermissioned.md +0 -12
- package/docs/src/src/interfaces/IJBPermissions.sol/interface.IJBPermissions.md +0 -74
- package/docs/src/src/interfaces/IJBPermitTerminal.sol/interface.IJBPermitTerminal.md +0 -15
- package/docs/src/src/interfaces/IJBPriceFeed.sol/interface.IJBPriceFeed.md +0 -12
- package/docs/src/src/interfaces/IJBPrices.sol/interface.IJBPrices.md +0 -74
- package/docs/src/src/interfaces/IJBProjectUriRegistry.sol/interface.IJBProjectUriRegistry.md +0 -19
- package/docs/src/src/interfaces/IJBProjects.sol/interface.IJBProjects.md +0 -49
- package/docs/src/src/interfaces/IJBRulesetApprovalHook.sol/interface.IJBRulesetApprovalHook.md +0 -35
- package/docs/src/src/interfaces/IJBRulesetDataHook.sol/interface.IJBRulesetDataHook.md +0 -97
- package/docs/src/src/interfaces/IJBRulesets.sol/interface.IJBRulesets.md +0 -165
- package/docs/src/src/interfaces/IJBSplitHook.sol/interface.IJBSplitHook.md +0 -31
- package/docs/src/src/interfaces/IJBSplits.sol/interface.IJBSplits.md +0 -35
- package/docs/src/src/interfaces/IJBTerminal.sol/interface.IJBTerminal.md +0 -141
- package/docs/src/src/interfaces/IJBTerminalStore.sol/interface.IJBTerminalStore.md +0 -198
- package/docs/src/src/interfaces/IJBToken.sol/interface.IJBToken.md +0 -54
- package/docs/src/src/interfaces/IJBTokenUriResolver.sol/interface.IJBTokenUriResolver.md +0 -12
- package/docs/src/src/interfaces/IJBTokens.sol/interface.IJBTokens.md +0 -151
- package/docs/src/src/interfaces/README.md +0 -33
- package/docs/src/src/libraries/JBCashOuts.sol/library.JBCashOuts.md +0 -40
- package/docs/src/src/libraries/JBConstants.sol/library.JBConstants.md +0 -52
- package/docs/src/src/libraries/JBCurrencyIds.sol/library.JBCurrencyIds.md +0 -19
- package/docs/src/src/libraries/JBFees.sol/library.JBFees.md +0 -52
- package/docs/src/src/libraries/JBFixedPointNumber.sol/library.JBFixedPointNumber.md +0 -12
- package/docs/src/src/libraries/JBMetadataResolver.sol/library.JBMetadataResolver.md +0 -242
- package/docs/src/src/libraries/JBRulesetMetadataResolver.sol/library.JBRulesetMetadataResolver.md +0 -180
- package/docs/src/src/libraries/JBSplitGroupIds.sol/library.JBSplitGroupIds.md +0 -14
- package/docs/src/src/libraries/JBSurplus.sol/library.JBSurplus.md +0 -44
- package/docs/src/src/libraries/README.md +0 -12
- package/docs/src/src/periphery/JBDeadline1Day.sol/contract.JBDeadline1Day.md +0 -15
- package/docs/src/src/periphery/JBDeadline3Days.sol/contract.JBDeadline3Days.md +0 -15
- package/docs/src/src/periphery/JBDeadline3Hours.sol/contract.JBDeadline3Hours.md +0 -15
- package/docs/src/src/periphery/JBDeadline7Days.sol/contract.JBDeadline7Days.md +0 -15
- package/docs/src/src/periphery/JBMatchingPriceFeed.sol/contract.JBMatchingPriceFeed.md +0 -22
- package/docs/src/src/periphery/README.md +0 -8
- package/docs/src/src/structs/JBAccountingContext.sol/struct.JBAccountingContext.md +0 -20
- package/docs/src/src/structs/JBAfterCashOutRecordedContext.sol/struct.JBAfterCashOutRecordedContext.md +0 -43
- package/docs/src/src/structs/JBAfterPayRecordedContext.sol/struct.JBAfterPayRecordedContext.md +0 -42
- package/docs/src/src/structs/JBBeforeCashOutRecordedContext.sol/struct.JBBeforeCashOutRecordedContext.md +0 -45
- package/docs/src/src/structs/JBBeforePayRecordedContext.sol/struct.JBBeforePayRecordedContext.md +0 -41
- package/docs/src/src/structs/JBCashOutHookSpecification.sol/struct.JBCashOutHookSpecification.md +0 -22
- package/docs/src/src/structs/JBCurrencyAmount.sol/struct.JBCurrencyAmount.md +0 -17
- package/docs/src/src/structs/JBFee.sol/struct.JBFee.md +0 -20
- package/docs/src/src/structs/JBFundAccessLimitGroup.sol/struct.JBFundAccessLimitGroup.md +0 -39
- package/docs/src/src/structs/JBPayHookSpecification.sol/struct.JBPayHookSpecification.md +0 -22
- package/docs/src/src/structs/JBPermissionsData.sol/struct.JBPermissionsData.md +0 -21
- package/docs/src/src/structs/JBRuleset.sol/struct.JBRuleset.md +0 -55
- package/docs/src/src/structs/JBRulesetConfig.sol/struct.JBRulesetConfig.md +0 -51
- package/docs/src/src/structs/JBRulesetMetadata.sol/struct.JBRulesetMetadata.md +0 -79
- package/docs/src/src/structs/JBRulesetWeightCache.sol/struct.JBRulesetWeightCache.md +0 -16
- package/docs/src/src/structs/JBRulesetWithMetadata.sol/struct.JBRulesetWithMetadata.md +0 -16
- package/docs/src/src/structs/JBSingleAllowance.sol/struct.JBSingleAllowance.md +0 -26
- package/docs/src/src/structs/JBSplit.sol/struct.JBSplit.md +0 -49
- package/docs/src/src/structs/JBSplitGroup.sol/struct.JBSplitGroup.md +0 -17
- package/docs/src/src/structs/JBSplitHookContext.sol/struct.JBSplitHookContext.md +0 -29
- package/docs/src/src/structs/JBTerminalConfig.sol/struct.JBTerminalConfig.md +0 -16
- package/docs/src/src/structs/JBTokenAmount.sol/struct.JBTokenAmount.md +0 -23
- package/docs/src/src/structs/README.md +0 -25
package/src/JBDeadline.sol
CHANGED
|
@@ -11,6 +11,9 @@ import {JBRuleset} from "./structs/JBRuleset.sol";
|
|
|
11
11
|
/// seconds before the current ruleset ends. In other words, rulesets must be queued before the deadline to take effect.
|
|
12
12
|
/// @dev Project rulesets are stored in a queue. Rulesets take effect after the previous ruleset in the queue ends, and
|
|
13
13
|
/// only if they are approved by the previous ruleset's approval hook.
|
|
14
|
+
/// @dev If `DURATION` is set longer than the ruleset's cycle duration, no queued ruleset can ever satisfy the deadline
|
|
15
|
+
/// and the current ruleset will effectively be locked in perpetuity. Choose a `DURATION` shorter than the shortest
|
|
16
|
+
/// cycle it will govern.
|
|
14
17
|
contract JBDeadline is IJBRulesetApprovalHook {
|
|
15
18
|
//*********************************************************************//
|
|
16
19
|
// ---------------- public immutable stored properties --------------- //
|
package/src/JBDirectory.sol
CHANGED
|
@@ -185,7 +185,8 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
|
|
|
185
185
|
revert JBDirectory_TokenNotAccepted(projectId, token, terminal);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
//
|
|
188
|
+
// Implicit terminal addition is by design. A primary terminal must be in the terminals list;
|
|
189
|
+
// implicit addition avoids requiring a separate addTerminalsOf call.
|
|
189
190
|
_addTerminalIfNeeded({projectId: projectId, terminal: terminal});
|
|
190
191
|
|
|
191
192
|
// Store the terminal as the project's primary terminal for the token.
|
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -136,6 +136,18 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
136
136
|
/// @custom:param projectId The ID of the project to get a list of accepted tokens for.
|
|
137
137
|
mapping(uint256 projectId => JBAccountingContext[]) internal _accountingContextsOf;
|
|
138
138
|
|
|
139
|
+
/// @notice The cumulative amount of fee-free intra-terminal payouts a project has received for a given token.
|
|
140
|
+
/// @dev Incremented each time a fee-free payout lands (same terminal, no fee charged). During cashout with
|
|
141
|
+
/// `cashOutTaxRate == 0`, fees are applied only up to this amount, then decremented. This prevents a round-trip
|
|
142
|
+
/// fee bypass (intra-terminal payout → zero-tax cashout) while scoping the fee precisely to the fee-free inflow
|
|
143
|
+
/// — legitimate cashouts beyond this amount remain fee-free.
|
|
144
|
+
/// @dev WARNING: This accumulator persists across rulesets and cannot be cleared. Once a fee-free payout
|
|
145
|
+
/// increments it, the balance remains until consumed by a zero-tax cashout. There is no admin function to reset
|
|
146
|
+
/// it. Projects switching from zero-tax to non-zero-tax rulesets will carry forward any unconsumed balance.
|
|
147
|
+
/// @custom:param projectId The ID of the project that received the payout.
|
|
148
|
+
/// @custom:param token The token that was received.
|
|
149
|
+
mapping(uint256 projectId => mapping(address token => uint256)) internal _feeFreeSurplusOf;
|
|
150
|
+
|
|
139
151
|
/// @notice Fees that are being held for each project.
|
|
140
152
|
/// @dev Projects can temporarily hold fees and unlock them later by adding funds to the project's balance.
|
|
141
153
|
/// @dev Held fees can be processed at any time by this terminal's owner.
|
|
@@ -416,12 +428,21 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
416
428
|
revert JBMultiTerminal_RecipientProjectTerminalNotFound(split.projectId, token);
|
|
417
429
|
}
|
|
418
430
|
|
|
431
|
+
// Fees apply to fund egress, not intra-terminal accounting. When both projects share this terminal,
|
|
432
|
+
// funds stay within the contract (addToBalance or pay) so no fee is charged. This is intentional:
|
|
433
|
+
// the fee model taxes value leaving the protocol ecosystem, not internal rebalancing.
|
|
419
434
|
// This payout is eligible for a fee if the funds are leaving this contract and the receiving terminal isn't
|
|
420
|
-
// a
|
|
435
|
+
// a feeless address.
|
|
421
436
|
if (terminal != this && !_isFeeless(address(terminal))) {
|
|
422
437
|
netPayoutAmount -= JBFees.feeAmountFrom({amountBeforeFee: amount, feePercent: FEE});
|
|
423
438
|
}
|
|
424
439
|
|
|
440
|
+
// Track the fee-free payout amount. During cashout at zero tax rate, fees apply
|
|
441
|
+
// only up to this accumulated amount, preventing round-trip fee bypass.
|
|
442
|
+
if (terminal == this) {
|
|
443
|
+
_feeFreeSurplusOf[split.projectId][token] += netPayoutAmount;
|
|
444
|
+
}
|
|
445
|
+
|
|
425
446
|
// Send the `projectId` in the metadata as a referral.
|
|
426
447
|
bytes memory metadata = bytes(abi.encodePacked(projectId));
|
|
427
448
|
|
|
@@ -541,6 +562,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
541
562
|
revert JBMultiTerminal_TerminalTokensIncompatible({projectId: projectId, token: token, terminal: to});
|
|
542
563
|
}
|
|
543
564
|
|
|
565
|
+
// Terminal migration intentionally does not transfer held fees. Held fees belong to the
|
|
566
|
+
// fee beneficiary (project #1), not the migrating project. They unlock after 28 days regardless of terminal.
|
|
544
567
|
// Record the migration in the store.
|
|
545
568
|
// slither-disable-next-line reentrancy-events
|
|
546
569
|
balance = STORE.recordTerminalMigration({projectId: projectId, token: token});
|
|
@@ -625,6 +648,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
625
648
|
}
|
|
626
649
|
|
|
627
650
|
/// @notice Process any fees that are being held for the project.
|
|
651
|
+
/// @dev Reentrancy safety: the loop re-reads `_nextHeldFeeIndexOf` from storage each iteration and advances the
|
|
652
|
+
/// index before the external `_processFee` call, so a reentrant call cannot double-process the same fee entry.
|
|
628
653
|
/// @param projectId The ID of the project to process held fees for.
|
|
629
654
|
/// @param token The token to process held fees for.
|
|
630
655
|
/// @param count The number of fees to process.
|
|
@@ -1049,6 +1074,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1049
1074
|
accountingContext: accountingContext,
|
|
1050
1075
|
balanceAccountingContexts: balanceAccountingContexts,
|
|
1051
1076
|
cashOutCount: cashOutCount,
|
|
1077
|
+
beneficiaryIsFeeless: _isFeeless(beneficiary),
|
|
1052
1078
|
metadata: metadata
|
|
1053
1079
|
});
|
|
1054
1080
|
}
|
|
@@ -1064,12 +1090,22 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1064
1090
|
|
|
1065
1091
|
// Send the reclaimed funds to the beneficiary.
|
|
1066
1092
|
if (reclaimAmount != 0) {
|
|
1067
|
-
// Determine if a fee should be taken. Fees are not
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1093
|
+
// Determine if a fee should be taken. Fees are not taken if the beneficiary is feeless.
|
|
1094
|
+
if (!_isFeeless(beneficiary)) {
|
|
1095
|
+
if (cashOutTaxRate != 0) {
|
|
1096
|
+
// Non-zero tax: fees apply to the full reclaim amount.
|
|
1097
|
+
amountEligibleForFees += reclaimAmount;
|
|
1098
|
+
reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: reclaimAmount, feePercent: FEE});
|
|
1099
|
+
} else {
|
|
1100
|
+
// Zero tax: fees apply only up to the fee-free surplus (round-trip prevention).
|
|
1101
|
+
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][tokenToReclaim];
|
|
1102
|
+
if (feeFreeSurplus != 0) {
|
|
1103
|
+
uint256 feeableAmount = reclaimAmount < feeFreeSurplus ? reclaimAmount : feeFreeSurplus;
|
|
1104
|
+
_feeFreeSurplusOf[projectId][tokenToReclaim] = feeFreeSurplus - feeableAmount;
|
|
1105
|
+
amountEligibleForFees += feeableAmount;
|
|
1106
|
+
reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: feeableAmount, feePercent: FEE});
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1073
1109
|
}
|
|
1074
1110
|
|
|
1075
1111
|
// Subtract the fee from the reclaim amount.
|
|
@@ -1623,7 +1659,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1623
1659
|
internal
|
|
1624
1660
|
returns (uint256)
|
|
1625
1661
|
{
|
|
1626
|
-
//
|
|
1662
|
+
// Failed split payouts consume the payout limit by design. The try-catch prevents a single
|
|
1663
|
+
// split from DoS-ing the entire payout. Failed splits' amounts are returned to the project balance via
|
|
1664
|
+
// `_recordAddedBalanceFor`. Payout limit consumption is correct because the project authorized the
|
|
1665
|
+
// distribution.
|
|
1627
1666
|
// slither-disable-next-line reentrancy-events
|
|
1628
1667
|
try this.executePayout({
|
|
1629
1668
|
split: split, projectId: projectId, token: token, amount: amount, originalMessageSender: _msgSender()
|
|
@@ -1697,7 +1736,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1697
1736
|
? 0
|
|
1698
1737
|
: JBFees.feeAmountFrom({amountBeforeFee: leftoverPayoutAmount, feePercent: FEE});
|
|
1699
1738
|
|
|
1700
|
-
//
|
|
1739
|
+
// Failed owner transfer consumes the payout limit by design. Same pattern as split payouts:
|
|
1740
|
+
// the try-catch prevents revert, failed amount is returned to project balance, and the owner can retry
|
|
1741
|
+
// via addToBalanceOf or in the next cycle.
|
|
1701
1742
|
try this.executeTransferTo({addr: projectOwner, token: token, amount: leftoverPayoutAmount - fee}) {
|
|
1702
1743
|
if (fee > 0) {
|
|
1703
1744
|
amountEligibleForFees += leftoverPayoutAmount;
|
package/src/JBPermissions.sol
CHANGED
|
@@ -154,6 +154,8 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
|
|
|
154
154
|
uint256 operatorAccountWildcardProjectPermissions =
|
|
155
155
|
includeWildcardProjectId ? permissionsOf[operator][account][WILDCARD_PROJECT_ID] : 0;
|
|
156
156
|
|
|
157
|
+
// Returns true for empty permission arrays by design (vacuous truth). An empty set of
|
|
158
|
+
// required permissions is trivially satisfied. Callers should validate non-empty permission arrays if needed.
|
|
157
159
|
for (uint256 i; i < permissionIds.length; i++) {
|
|
158
160
|
// Set the permission being iterated on.
|
|
159
161
|
uint256 permissionId = permissionIds[i];
|
package/src/JBPrices.sol
CHANGED
|
@@ -15,8 +15,10 @@ import {IJBProjects} from "./interfaces/IJBProjects.sol";
|
|
|
15
15
|
|
|
16
16
|
/// @notice Manages and normalizes price feeds. Price feeds are contracts which return the "pricing currency" cost of 1
|
|
17
17
|
/// "unit currency".
|
|
18
|
-
/// @dev Price feeds are immutable once set and cannot be replaced or removed.
|
|
19
|
-
/// a
|
|
18
|
+
/// @dev Price feeds are immutable once set and cannot be replaced or removed. This prevents oracle manipulation via
|
|
19
|
+
/// admin-key attacks, but means a misconfigured or failing feed will cause operations using that currency pair to
|
|
20
|
+
/// revert (DoS, not fund loss). Select feeds carefully — recovery requires deploying a new JBPrices contract and
|
|
21
|
+
/// migrating projects.
|
|
20
22
|
contract JBPrices is JBControlled, JBPermissioned, ERC2771Context, Ownable, IJBPrices {
|
|
21
23
|
//*********************************************************************//
|
|
22
24
|
// --------------------------- custom errors ------------------------- //
|
|
@@ -134,6 +136,10 @@ contract JBPrices is JBControlled, JBPermissioned, ERC2771Context, Ownable, IJBP
|
|
|
134
136
|
: priceFeedFor[projectId][unitCurrency][pricingCurrency]);
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
// Price feed immutability is by design to prevent admin-key attacks on price oracles.
|
|
140
|
+
// If a feed fails, operations using that currency pair revert (DoS but not fund loss). Projects can use
|
|
141
|
+
// alternative currency pairs. A default feed for a currency pair prevents per-project overrides to ensure
|
|
142
|
+
// price consistency; projects should use unused currency IDs for custom pricing.
|
|
137
143
|
// Store the feed.
|
|
138
144
|
priceFeedFor[projectId][pricingCurrency][unitCurrency] = feed;
|
|
139
145
|
|
package/src/JBRulesets.sol
CHANGED
|
@@ -353,6 +353,9 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
353
353
|
|
|
354
354
|
/// @notice The ruleset that is currently active for the specified project.
|
|
355
355
|
/// @dev If a current ruleset of the project is not found, returns an empty ruleset with all properties set to 0.
|
|
356
|
+
/// @dev The first cycle returns the stored ruleset directly (cycleNumber=1, original weight). Subsequent cycles
|
|
357
|
+
/// simulate cycling with weight decay via `_simulateCycledRulesetBasedOn`. Payout limits reset each cycle because
|
|
358
|
+
/// the terminal store keys usage by rulesetId, and each cycle produces a new simulated rulesetId.
|
|
356
359
|
/// @param projectId The ID of the project to get the current ruleset of.
|
|
357
360
|
/// @return ruleset The project's current ruleset.
|
|
358
361
|
function currentOf(uint256 projectId) external view override returns (JBRuleset memory ruleset) {
|
package/src/JBSplits.sol
CHANGED
|
@@ -77,8 +77,9 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
77
77
|
|
|
78
78
|
/// @notice Sets a project's split groups.
|
|
79
79
|
/// @dev Only a project's controller can set its splits, unless the first 160 bits of the group's ID match
|
|
80
|
-
/// `msg.sender
|
|
81
|
-
///
|
|
80
|
+
/// `msg.sender` AND the upper 96 bits are non-zero, in which case the caller can set its own splits.
|
|
81
|
+
/// GroupIds with zero upper 96 bits (i.e. bare addresses) are reserved for protocol use (e.g. terminal
|
|
82
|
+
/// payout groups keyed by token address) and always require controller authorization.
|
|
82
83
|
/// @dev The new split groups must include any currently set splits that are locked.
|
|
83
84
|
/// @param projectId The ID of the project to set the split groups of.
|
|
84
85
|
/// @param rulesetId The ID of the ruleset the split groups should be active in. Send
|
|
@@ -101,9 +102,12 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
101
102
|
// Get a reference to the grouped split being iterated on.
|
|
102
103
|
JBSplitGroup memory splitGroup = splitGroups[i];
|
|
103
104
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
105
|
+
// Self-auth: lower 160 bits must match msg.sender AND upper 96 bits must be non-zero.
|
|
106
|
+
// GroupIds with zero upper bits are reserved for protocol use (e.g. terminal payout groups)
|
|
107
|
+
// and always require controller authorization to prevent token contracts from hijacking payouts.
|
|
108
|
+
bool isSelfManaged = splitGroup.groupId >> 160 != 0 && address(uint160(splitGroup.groupId)) == msg.sender;
|
|
109
|
+
|
|
110
|
+
if (!isSelfManaged && !controllerChecked) {
|
|
107
111
|
_onlyControllerOf(projectId);
|
|
108
112
|
controllerChecked = true;
|
|
109
113
|
}
|
package/src/JBTerminalStore.sol
CHANGED
|
@@ -156,6 +156,8 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
156
156
|
/// @param accountingContext The accounting context of the token being reclaimed by the cash out.
|
|
157
157
|
/// @param balanceAccountingContexts The accounting contexts of the tokens whose balances should contribute to the
|
|
158
158
|
/// surplus being reclaimed from.
|
|
159
|
+
/// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address. Passed through to data
|
|
160
|
+
/// hooks so they can skip their own fees when value stays in the protocol (e.g. project-to-project routing).
|
|
159
161
|
/// @param metadata Bytes to send to the data hook, if the project's current ruleset specifies one.
|
|
160
162
|
/// @return ruleset The ruleset during the cash out was made during, as a `JBRuleset` struct. This ruleset will
|
|
161
163
|
/// have a cash out tax rate provided by the cash out hook if applicable.
|
|
@@ -170,6 +172,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
170
172
|
uint256 cashOutCount,
|
|
171
173
|
JBAccountingContext calldata accountingContext,
|
|
172
174
|
JBAccountingContext[] calldata balanceAccountingContexts,
|
|
175
|
+
bool beneficiaryIsFeeless,
|
|
173
176
|
bytes memory metadata
|
|
174
177
|
)
|
|
175
178
|
external
|
|
@@ -184,10 +187,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
184
187
|
// Get a reference to the project's current ruleset.
|
|
185
188
|
ruleset = RULESETS.currentOf(projectId);
|
|
186
189
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
uint256 currentSurplus = ruleset.useTotalSurplusForCashOuts()
|
|
190
|
+
// Store the current surplus in `reclaimAmount` temporarily to avoid allocating a separate local variable
|
|
191
|
+
// (saves one stack slot, which is needed to fit the 7th parameter without hitting stack-too-deep).
|
|
192
|
+
reclaimAmount = ruleset.useTotalSurplusForCashOuts()
|
|
191
193
|
? JBSurplus.currentSurplusOf({
|
|
192
194
|
projectId: projectId,
|
|
193
195
|
terminals: DIRECTORY.terminalsOf(projectId),
|
|
@@ -204,54 +206,59 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
204
206
|
targetCurrency: accountingContext.currency
|
|
205
207
|
});
|
|
206
208
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
209
|
+
// Scoped to keep `totalSupply` and `context` off the outer stack.
|
|
210
|
+
{
|
|
211
|
+
// Get the total number of outstanding project tokens.
|
|
212
|
+
uint256 totalSupply = IJBController(address(DIRECTORY.controllerOf(projectId)))
|
|
213
|
+
.totalTokenSupplyWithReservedTokensOf(projectId);
|
|
214
|
+
|
|
215
|
+
// Can't cash out more tokens than are in the supply.
|
|
216
|
+
if (cashOutCount > totalSupply) revert JBTerminalStore_InsufficientTokens(cashOutCount, totalSupply);
|
|
217
|
+
|
|
218
|
+
// SECURITY NOTE: The data hook has absolute control over cash-out economics.
|
|
219
|
+
// It can set totalSupply, cashOutCount, and cashOutTaxRate to arbitrary values,
|
|
220
|
+
// completely overriding the terminal's bonding curve math. For example, setting
|
|
221
|
+
// totalSupply = surplus makes reclaimAmount = cashOutCount, bypassing the curve.
|
|
222
|
+
// Project owners MUST audit their data hooks with the same rigor as the terminal.
|
|
223
|
+
|
|
224
|
+
// If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
|
|
225
|
+
if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
|
|
226
|
+
// Build the cash out context field-by-field to avoid stack-too-deep
|
|
227
|
+
// (the struct has 11 fields — a struct literal would require all values on the stack at once).
|
|
228
|
+
JBBeforeCashOutRecordedContext memory context;
|
|
229
|
+
context.terminal = msg.sender;
|
|
230
|
+
context.holder = holder;
|
|
231
|
+
context.projectId = projectId;
|
|
232
|
+
context.rulesetId = ruleset.id;
|
|
233
|
+
context.cashOutCount = cashOutCount;
|
|
234
|
+
context.totalSupply = totalSupply;
|
|
235
|
+
context.surplus = JBTokenAmount({
|
|
231
236
|
token: accountingContext.token,
|
|
232
|
-
value:
|
|
237
|
+
value: reclaimAmount, // reclaimAmount temporarily holds the current surplus.
|
|
233
238
|
decimals: accountingContext.decimals,
|
|
234
239
|
currency: accountingContext.currency
|
|
235
|
-
})
|
|
236
|
-
useTotalSurplus
|
|
237
|
-
cashOutTaxRate
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
});
|
|
241
|
+
context.useTotalSurplus = ruleset.useTotalSurplusForCashOuts();
|
|
242
|
+
context.cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
243
|
+
context.beneficiaryIsFeeless = beneficiaryIsFeeless;
|
|
244
|
+
context.metadata = metadata;
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
(cashOutTaxRate, cashOutCount, totalSupply, hookSpecifications) =
|
|
247
|
+
IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
|
|
248
|
+
} else {
|
|
249
|
+
cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
250
|
+
}
|
|
246
251
|
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
reclaimAmount
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
// Calculate the reclaim amount. `reclaimAmount` currently holds the surplus — overwrite it with the
|
|
253
|
+
// result.
|
|
254
|
+
if (reclaimAmount != 0) {
|
|
255
|
+
reclaimAmount = JBCashOuts.cashOutFrom({
|
|
256
|
+
surplus: reclaimAmount,
|
|
257
|
+
cashOutCount: cashOutCount,
|
|
258
|
+
totalSupply: totalSupply,
|
|
259
|
+
cashOutTaxRate: cashOutTaxRate
|
|
260
|
+
});
|
|
261
|
+
}
|
|
255
262
|
}
|
|
256
263
|
|
|
257
264
|
// Keep a reference to the amount that should be added to the project's balance.
|
package/src/JBTokens.sol
CHANGED
|
@@ -275,6 +275,9 @@ contract JBTokens is JBControlled, IJBTokens {
|
|
|
275
275
|
|
|
276
276
|
/// @notice Set a project's token if not already set.
|
|
277
277
|
/// @dev Only a project's controller can set its token.
|
|
278
|
+
/// @dev If the provided ERC-20 has a pre-existing supply (minted outside this contract), that supply will be
|
|
279
|
+
/// included in `totalSupplyOf` and will dilute cash-out calculations for all token holders. Project owners are
|
|
280
|
+
/// responsible for ensuring the token's supply is appropriate before calling this function.
|
|
278
281
|
/// @param projectId The ID of the project to set the token of.
|
|
279
282
|
/// @param token The new token's address.
|
|
280
283
|
function setTokenFor(uint256 projectId, IJBToken token) external override onlyControllerOf(projectId) {
|
|
@@ -145,6 +145,8 @@ interface IJBTerminalStore {
|
|
|
145
145
|
/// @param cashOutCount The number of project tokens being cashed out.
|
|
146
146
|
/// @param accountingContext The accounting context of the token being reclaimed.
|
|
147
147
|
/// @param balanceAccountingContexts The accounting contexts to include in the balance calculation.
|
|
148
|
+
/// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address. Passed through to data
|
|
149
|
+
/// hooks so they can skip their own fees when value stays in the protocol (e.g. project-to-project routing).
|
|
148
150
|
/// @param metadata Extra data to pass along to the data hook.
|
|
149
151
|
/// @return ruleset The project's current ruleset.
|
|
150
152
|
/// @return reclaimAmount The amount reclaimed.
|
|
@@ -156,6 +158,7 @@ interface IJBTerminalStore {
|
|
|
156
158
|
uint256 cashOutCount,
|
|
157
159
|
JBAccountingContext calldata accountingContext,
|
|
158
160
|
JBAccountingContext[] calldata balanceAccountingContexts,
|
|
161
|
+
bool beneficiaryIsFeeless,
|
|
159
162
|
bytes calldata metadata
|
|
160
163
|
)
|
|
161
164
|
external
|
package/src/libraries/JBFees.sol
CHANGED
|
@@ -19,6 +19,8 @@ library JBFees {
|
|
|
19
19
|
|
|
20
20
|
/// @notice Returns the fee that would be taken from `amountBeforeFee`.
|
|
21
21
|
/// @dev Use this to forward-calculate the fee from a known pre-fee amount.
|
|
22
|
+
/// @dev Fee rounding error is bounded by N-1 wei (N = number of splits). Economically
|
|
23
|
+
/// insignificant. Rounds down (mulDiv floors), so the fee beneficiary may receive up to 1 wei less per split.
|
|
22
24
|
/// @param amountBeforeFee The amount before the fee is applied, as a fixed point number.
|
|
23
25
|
/// @param feePercent The fee percent, out of `JBConstants.MAX_FEE`.
|
|
24
26
|
/// @return The fee amount, as a fixed point number with the same number of decimals as the provided `amount`.
|
|
@@ -60,6 +60,9 @@ library JBMetadataResolver {
|
|
|
60
60
|
pure
|
|
61
61
|
returns (bytes memory newMetadata)
|
|
62
62
|
{
|
|
63
|
+
// Validate data padding upfront so that the fast path below cannot bypass the check.
|
|
64
|
+
if (dataToAdd.length < 32) revert JBMetadataResolver_DataNotPadded();
|
|
65
|
+
|
|
63
66
|
// Empty original metadata and maybe something in the first 32 bytes: create new metadata
|
|
64
67
|
if (originalMetadata.length <= RESERVED_SIZE) {
|
|
65
68
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
@@ -69,9 +72,6 @@ library JBMetadataResolver {
|
|
|
69
72
|
// There is something in the table offset, but not a valid entry - avoid overwriting
|
|
70
73
|
if (originalMetadata.length < RESERVED_SIZE + ID_SIZE + 1) revert JBMetadataResolver_MetadataTooShort();
|
|
71
74
|
|
|
72
|
-
// Make sure the data is padded to 32 bytes.
|
|
73
|
-
if (dataToAdd.length < 32) revert JBMetadataResolver_DataNotPadded();
|
|
74
|
-
|
|
75
75
|
// Get the first data offset - upcast to avoid overflow (same for other offset)...
|
|
76
76
|
uint256 firstOffset = uint8(originalMetadata[RESERVED_SIZE + ID_SIZE]);
|
|
77
77
|
|
|
@@ -103,7 +103,10 @@ library JBMetadataResolver {
|
|
|
103
103
|
if (i + TOTAL_ID_SIZE >= firstOffset * WORD_SIZE) {
|
|
104
104
|
// Increment every offset by 1 (as the table now takes one more word)
|
|
105
105
|
for (uint256 j = RESERVED_SIZE + ID_SIZE; j < lastOffsetIndex + 1; j += TOTAL_ID_SIZE) {
|
|
106
|
-
|
|
106
|
+
uint256 incremented = uint256(uint8(originalMetadata[j])) + 1;
|
|
107
|
+
if (incremented > 255) revert JBMetadataResolver_MetadataTooLong();
|
|
108
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
109
|
+
newMetadata[j] = bytes1(uint8(incremented));
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
// Increment the last offset so the new offset will be properly set too
|
|
@@ -123,6 +126,10 @@ library JBMetadataResolver {
|
|
|
123
126
|
newMetadata = abi.encodePacked(newMetadata, idToAdd, bytes1(uint8(newOffset)));
|
|
124
127
|
|
|
125
128
|
// Pad as needed - inlined for gas saving
|
|
129
|
+
// The length inflation to the next 32-byte boundary does NOT read uninitialized memory.
|
|
130
|
+
// abi.encodePacked always advances the free memory pointer to a 32-byte-aligned position, so the
|
|
131
|
+
// padding bytes (up to 31) are within the already-allocated region and guaranteed to be zero by
|
|
132
|
+
// Solidity's virgin-memory invariant. The zeros are the intended table padding per the metadata format.
|
|
126
133
|
uint256 paddedLength =
|
|
127
134
|
newMetadata.length % WORD_SIZE == 0 ? newMetadata.length : (newMetadata.length / WORD_SIZE + 1) * WORD_SIZE;
|
|
128
135
|
assembly ("memory-safe") {
|
|
@@ -162,6 +169,9 @@ library JBMetadataResolver {
|
|
|
162
169
|
function createMetadata(bytes4[] memory ids, bytes[] memory datas) internal pure returns (bytes memory metadata) {
|
|
163
170
|
if (ids.length != datas.length) revert JBMetadataResolver_LengthMismatch();
|
|
164
171
|
|
|
172
|
+
// Return empty bytes for empty input arrays to avoid underflow in offset calculation below.
|
|
173
|
+
if (ids.length == 0) return bytes("");
|
|
174
|
+
|
|
165
175
|
// Add a first empty 32B for the protocol reserved word
|
|
166
176
|
metadata = abi.encodePacked(bytes32(0));
|
|
167
177
|
|
|
@@ -218,6 +228,9 @@ library JBMetadataResolver {
|
|
|
218
228
|
|
|
219
229
|
/// @notice Parse the metadata to find the data for a specific ID
|
|
220
230
|
/// @dev Returns false and an empty bytes if no data is found
|
|
231
|
+
/// @dev Padding to 32-byte boundaries is required for ABI compatibility. The apparent
|
|
232
|
+
/// inconsistency between creation and parsing is intentional: creation pads for storage alignment,
|
|
233
|
+
/// parsing uses actual data length boundaries from the lookup table.
|
|
221
234
|
/// @param id The ID to find.
|
|
222
235
|
/// @param metadata The metadata to parse.
|
|
223
236
|
/// @return found Whether the {id:data} was found
|
|
@@ -16,6 +16,9 @@ import {JBTokenAmount} from "./JBTokenAmount.sol";
|
|
|
16
16
|
/// included, and the currency of the surplus.
|
|
17
17
|
/// @custom:member useTotalSurplus If surplus across all of a project's terminals is being used when making cash outs.
|
|
18
18
|
/// @custom:member cashOutTaxRate The cash out tax rate of the ruleset the cash out is being made during.
|
|
19
|
+
/// @custom:member beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address. Useful for data hooks
|
|
20
|
+
/// that charge their own fees — they can skip fees when value stays in the protocol (e.g. project-to-project
|
|
21
|
+
/// routing).
|
|
19
22
|
/// @custom:member metadata Extra data provided by the casher.
|
|
20
23
|
// forge-lint: disable-next-line(pascal-case-struct)
|
|
21
24
|
struct JBBeforeCashOutRecordedContext {
|
|
@@ -28,5 +31,6 @@ struct JBBeforeCashOutRecordedContext {
|
|
|
28
31
|
JBTokenAmount surplus;
|
|
29
32
|
bool useTotalSurplus;
|
|
30
33
|
uint256 cashOutTaxRate;
|
|
34
|
+
bool beneficiaryIsFeeless;
|
|
31
35
|
bytes metadata;
|
|
32
36
|
}
|