@bananapus/core-v6 0.0.30 → 0.0.32
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 +43 -13
- package/ARCHITECTURE.md +62 -137
- package/AUDIT_INSTRUCTIONS.md +149 -428
- package/CHANGELOG.md +73 -0
- package/README.md +90 -201
- package/RISKS.md +27 -12
- package/SKILLS.md +31 -441
- package/STYLE_GUIDE.md +52 -19
- package/USER_JOURNEYS.md +76 -627
- package/package.json +1 -2
- package/references/entrypoints.md +160 -0
- package/references/types-errors-events.md +297 -0
- package/script/Deploy.s.sol +7 -2
- package/script/DeployPeriphery.s.sol +51 -4
- package/src/JBController.sol +45 -17
- package/src/JBDirectory.sol +26 -13
- package/src/JBFundAccessLimits.sol +28 -7
- package/src/JBMultiTerminal.sol +180 -86
- package/src/JBPermissions.sol +17 -17
- package/src/JBRulesets.sol +82 -23
- package/src/JBSplits.sol +31 -12
- package/src/JBTerminalStore.sol +137 -53
- package/src/JBTokens.sol +5 -2
- package/src/abstract/JBControlled.sol +10 -3
- package/src/abstract/JBPermissioned.sol +1 -1
- package/src/interfaces/IJBRulesetDataHook.sol +5 -4
- package/src/libraries/JBCashOuts.sol +1 -1
- package/src/libraries/JBConstants.sol +1 -1
- package/src/libraries/JBCurrencyIds.sol +1 -1
- package/src/libraries/JBFees.sol +1 -1
- package/src/libraries/JBFixedPointNumber.sol +1 -1
- package/src/libraries/JBMetadataResolver.sol +5 -2
- package/src/libraries/JBPayoutSplitGroupLib.sol +7 -2
- package/src/libraries/JBRulesetMetadataResolver.sol +1 -1
- package/src/libraries/JBSplitGroupIds.sol +1 -1
- package/src/libraries/JBSurplus.sol +5 -2
- package/src/structs/JBSplit.sol +4 -1
- package/test/TestForwardedTokenConsumption.sol +419 -0
- package/test/audit/CrossTerminalSurplusSpoof.t.sol +140 -0
- package/test/audit/CycledSurplusAllowanceReset.t.sol +184 -0
- package/test/units/static/JBController/TestPreviewMintOf.sol +5 -4
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +15 -12
- package/test/units/static/JBMultiTerminal/TestExecutePayout.sol +6 -0
- package/test/units/static/JBMultiTerminal/TestExecuteProcessFee.sol +3 -0
- package/test/units/static/JBMultiTerminal/TestMigrateBalanceOf.sol +3 -0
- package/test/units/static/JBMultiTerminal/TestPay.sol +7 -15
- package/test/units/static/JBMultiTerminal/TestSendPayoutsOf.sol +1 -1
- package/test/units/static/JBMultiTerminal/TestUseAllowanceOf.sol +1 -1
- package/CHANGE_LOG.md +0 -479
package/src/JBMultiTerminal.sol
CHANGED
|
@@ -66,10 +66,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
66
66
|
error JBMultiTerminal_RecipientProjectTerminalNotFound(uint256 projectId, address token);
|
|
67
67
|
error JBMultiTerminal_SplitHookInvalid(IJBSplitHook hook);
|
|
68
68
|
error JBMultiTerminal_TerminalTokensIncompatible(uint256 projectId, address token, IJBTerminal terminal);
|
|
69
|
+
error JBMultiTerminal_TemporaryAllowanceNotConsumed(address token, address spender, uint256 allowance);
|
|
69
70
|
error JBMultiTerminal_TokenNotAccepted(address token);
|
|
70
|
-
error
|
|
71
|
-
error JBMultiTerminal_UnderMinTokensPaidOut(uint256 amount, uint256 min);
|
|
72
|
-
error JBMultiTerminal_UnderMinTokensReclaimed(uint256 amount, uint256 min);
|
|
71
|
+
error JBMultiTerminal_UnderMin(uint256 value, uint256 min);
|
|
73
72
|
|
|
74
73
|
//*********************************************************************//
|
|
75
74
|
// ------------------------- public constants ------------------------ //
|
|
@@ -208,8 +207,12 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
208
207
|
STORE.recordAccountingContextOf({projectId: projectId, contexts: accountingContexts});
|
|
209
208
|
|
|
210
209
|
// Emit an event for each accounting context.
|
|
211
|
-
for (uint256 i; i < accountingContexts.length;
|
|
210
|
+
for (uint256 i; i < accountingContexts.length;) {
|
|
211
|
+
// slither-disable-next-line reentrancy-events
|
|
212
212
|
emit SetAccountingContext({projectId: projectId, context: accountingContexts[i], caller: _msgSender()});
|
|
213
|
+
unchecked {
|
|
214
|
+
++i;
|
|
215
|
+
}
|
|
213
216
|
}
|
|
214
217
|
}
|
|
215
218
|
|
|
@@ -251,7 +254,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
251
254
|
/// those tokens.
|
|
252
255
|
/// @param holder The account whose tokens are being cashed out.
|
|
253
256
|
/// @param projectId The ID of the project the project tokens belong to.
|
|
254
|
-
/// @param cashOutCount The number of project tokens to cash out, as a fixed point number with 18
|
|
257
|
+
/// @param cashOutCount The number of project tokens to cash out and burn, as a fixed point number with 18
|
|
258
|
+
/// decimals.
|
|
255
259
|
/// @param tokenToReclaim The token being reclaimed.
|
|
256
260
|
/// @param minTokensReclaimed The minimum number of terminal tokens expected in return, as a fixed point number with
|
|
257
261
|
/// the same number of decimals as this terminal. If the amount of tokens minted for the beneficiary would be less
|
|
@@ -289,9 +293,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
289
293
|
});
|
|
290
294
|
|
|
291
295
|
// The amount being reclaimed must be at least as much as was expected.
|
|
292
|
-
|
|
293
|
-
revert JBMultiTerminal_UnderMinTokensReclaimed(reclaimAmount, minTokensReclaimed);
|
|
294
|
-
}
|
|
296
|
+
_checkMin({value: reclaimAmount, min: minTokensReclaimed});
|
|
295
297
|
}
|
|
296
298
|
|
|
297
299
|
/// @notice Executes a payout to a split.
|
|
@@ -349,6 +351,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
349
351
|
// If this terminal's token is the native token, send it in `msg.value`.
|
|
350
352
|
split.hook.processSplitWith{value: payValue}(context);
|
|
351
353
|
|
|
354
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
355
|
+
_afterTransferTo({to: address(split.hook), token: token});
|
|
356
|
+
|
|
352
357
|
// Otherwise, if a project is specified, make a payment to it.
|
|
353
358
|
} else if (split.projectId != 0) {
|
|
354
359
|
// Get a reference to the terminal being used.
|
|
@@ -542,20 +547,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
542
547
|
// Transfer the balance minus the fee to the new terminal.
|
|
543
548
|
uint256 migrationAmount = balance - feeAmount;
|
|
544
549
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
// slither-disable-next-line reentrancy-events
|
|
548
|
-
uint256 payValue = _beforeTransferTo({to: address(to), token: token, amount: migrationAmount});
|
|
549
|
-
|
|
550
|
-
// Withdraw the balance to transfer to the new terminal.
|
|
551
|
-
// slither-disable-next-line reentrancy-events
|
|
552
|
-
to.addToBalanceOf{value: payValue}({
|
|
553
|
-
projectId: projectId,
|
|
554
|
-
token: token,
|
|
555
|
-
amount: migrationAmount,
|
|
556
|
-
shouldReturnHeldFees: false,
|
|
557
|
-
memo: "",
|
|
558
|
-
metadata: bytes("")
|
|
550
|
+
_externalAddToBalance({
|
|
551
|
+
terminal: to, projectId: projectId, token: token, amount: migrationAmount, metadata: bytes("")
|
|
559
552
|
});
|
|
560
553
|
}
|
|
561
554
|
}
|
|
@@ -592,11 +585,15 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
592
585
|
// Get a reference to the beneficiary's balance before the payment.
|
|
593
586
|
uint256 beneficiaryBalanceBefore = TOKENS.totalBalanceOf({holder: beneficiary, projectId: projectId});
|
|
594
587
|
|
|
588
|
+
// Accept the funds.
|
|
589
|
+
uint256 acceptedAmount =
|
|
590
|
+
_acceptFundsFor({projectId: projectId, token: token, amount: amount, metadata: metadata});
|
|
591
|
+
|
|
595
592
|
// Pay the project.
|
|
596
593
|
_pay({
|
|
597
594
|
projectId: projectId,
|
|
598
595
|
token: token,
|
|
599
|
-
amount:
|
|
596
|
+
amount: acceptedAmount,
|
|
600
597
|
payer: _msgSender(),
|
|
601
598
|
beneficiary: beneficiary,
|
|
602
599
|
memo: memo,
|
|
@@ -612,9 +609,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
612
609
|
}
|
|
613
610
|
|
|
614
611
|
// The token count for the beneficiary must be greater than or equal to the specified minimum.
|
|
615
|
-
|
|
616
|
-
revert JBMultiTerminal_UnderMinReturnedTokens(beneficiaryTokenCount, minReturnedTokens);
|
|
617
|
-
}
|
|
612
|
+
_checkMin({value: beneficiaryTokenCount, min: minReturnedTokens});
|
|
618
613
|
}
|
|
619
614
|
|
|
620
615
|
/// @notice Process any fees that are being held for the project.
|
|
@@ -639,7 +634,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
639
634
|
|
|
640
635
|
// Process each fee. Re-read the index and array length from storage each iteration to account for reentrant
|
|
641
636
|
// calls that may have already advanced the index or cleaned up the array.
|
|
642
|
-
for (uint256 i; i < count;
|
|
637
|
+
for (uint256 i; i < count;) {
|
|
643
638
|
// Read the current index from storage (not a cached value) to prevent reentrancy from
|
|
644
639
|
// causing double-processing.
|
|
645
640
|
uint256 currentIndex = _nextHeldFeeIndexOf[projectId][token];
|
|
@@ -654,10 +649,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
654
649
|
// if the current fee isn't yet unlocked.
|
|
655
650
|
if (heldFee.unlockTimestamp > block.timestamp) break;
|
|
656
651
|
|
|
657
|
-
// Delete the entry
|
|
652
|
+
// Delete the entry and advance the index *before* the external call. This is intentional:
|
|
653
|
+
// 1. It prevents reentrancy from reprocessing the same fee.
|
|
654
|
+
// 2. If `_processFee` fails (try-catch), the fee amount is returned to the project's balance via
|
|
655
|
+
// `_recordAddedBalanceFor` — the fee is forgiven rather than retried. This is a deliberate design
|
|
656
|
+
// choice: projects should not have funds permanently stuck because the fee route is misconfigured or
|
|
657
|
+
// reverting.
|
|
658
|
+
// A `FeeReverted` event is emitted so the forgiveness is observable off-chain.
|
|
658
659
|
delete _heldFeesOf[projectId][token][currentIndex];
|
|
659
|
-
|
|
660
|
-
// Update the index before the external call to prevent reentrancy from reprocessing the same fee.
|
|
661
660
|
_nextHeldFeeIndexOf[projectId][token] = currentIndex + 1;
|
|
662
661
|
|
|
663
662
|
// Process the fee.
|
|
@@ -670,6 +669,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
670
669
|
feeTerminal: feeTerminal,
|
|
671
670
|
wasHeld: true
|
|
672
671
|
});
|
|
672
|
+
unchecked {
|
|
673
|
+
++i;
|
|
674
|
+
}
|
|
673
675
|
}
|
|
674
676
|
|
|
675
677
|
// If all held fees have been processed, reset the array and index entirely to bound storage growth.
|
|
@@ -715,9 +717,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
715
717
|
amountPaidOut = _sendPayoutsOf({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
716
718
|
|
|
717
719
|
// The amount being paid out must be at least as much as was expected.
|
|
718
|
-
|
|
719
|
-
revert JBMultiTerminal_UnderMinTokensPaidOut(amountPaidOut, minTokensPaidOut);
|
|
720
|
-
}
|
|
720
|
+
_checkMin({value: amountPaidOut, min: minTokensPaidOut});
|
|
721
721
|
}
|
|
722
722
|
|
|
723
723
|
/// @notice Allows a project to pay out funds from its surplus up to the current surplus allowance.
|
|
@@ -770,9 +770,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
770
770
|
});
|
|
771
771
|
|
|
772
772
|
// The amount being withdrawn must be at least as much as was expected.
|
|
773
|
-
|
|
774
|
-
revert JBMultiTerminal_UnderMinTokensPaidOut(netAmountPaidOut, minTokensPaidOut);
|
|
775
|
-
}
|
|
773
|
+
_checkMin({value: netAmountPaidOut, min: minTokensPaidOut});
|
|
776
774
|
}
|
|
777
775
|
|
|
778
776
|
//*********************************************************************//
|
|
@@ -862,8 +860,11 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
862
860
|
heldFees = new JBFee[](count);
|
|
863
861
|
|
|
864
862
|
// Copy the fees into the array.
|
|
865
|
-
for (uint256 i; i < count;
|
|
863
|
+
for (uint256 i; i < count;) {
|
|
866
864
|
heldFees[i] = _heldFeesOf[projectId][token][startIndex + i];
|
|
865
|
+
unchecked {
|
|
866
|
+
++i;
|
|
867
|
+
}
|
|
867
868
|
}
|
|
868
869
|
}
|
|
869
870
|
|
|
@@ -1022,6 +1023,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1022
1023
|
// Set the allowance to `spend` tokens for the user.
|
|
1023
1024
|
try PERMIT2.permit({owner: _msgSender(), permitSingle: permitSingle, signature: allowance.signature}) {}
|
|
1024
1025
|
catch (bytes memory reason) {
|
|
1026
|
+
// slither-disable-next-line reentrancy-events
|
|
1025
1027
|
emit Permit2AllowanceFailed(token, _msgSender(), reason);
|
|
1026
1028
|
}
|
|
1027
1029
|
}
|
|
@@ -1087,6 +1089,29 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1087
1089
|
return 0;
|
|
1088
1090
|
}
|
|
1089
1091
|
|
|
1092
|
+
/// @notice Cap fee-free surplus at the project's remaining balance after an outflow.
|
|
1093
|
+
/// @dev Non-fee-free funds are considered to leave first. Fee-free surplus only decreases when the remaining
|
|
1094
|
+
/// balance can no longer support it. This prevents attackers from using outflows to drain the fee-free counter
|
|
1095
|
+
/// and then cashing out without incurring fees.
|
|
1096
|
+
/// @param projectId The ID of the project.
|
|
1097
|
+
/// @param token The token whose fee-free surplus to cap.
|
|
1098
|
+
function _capFeeFreeSurplus(uint256 projectId, address token) internal {
|
|
1099
|
+
// Get the current fee-free surplus for this project/token pair.
|
|
1100
|
+
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][token];
|
|
1101
|
+
|
|
1102
|
+
// Nothing to cap if there's no fee-free surplus tracked.
|
|
1103
|
+
if (feeFreeSurplus == 0) return;
|
|
1104
|
+
|
|
1105
|
+
// Get the project's remaining balance (already decremented by the store's record call).
|
|
1106
|
+
uint256 remainingBalance = STORE.balanceOf({terminal: address(this), projectId: projectId, token: token});
|
|
1107
|
+
|
|
1108
|
+
// Cap fee-free surplus at the remaining balance.
|
|
1109
|
+
if (feeFreeSurplus > remainingBalance) {
|
|
1110
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1111
|
+
_feeFreeSurplusOf[projectId][token] = remainingBalance;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1090
1115
|
/// @notice Holders can cash out their tokens to reclaim some of a project's surplus, or to trigger rules determined
|
|
1091
1116
|
/// by
|
|
1092
1117
|
/// the project's current ruleset's data hook.
|
|
@@ -1122,13 +1147,16 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1122
1147
|
// Keep a reference to the cash out tax rate being used.
|
|
1123
1148
|
uint256 cashOutTaxRate;
|
|
1124
1149
|
|
|
1150
|
+
// Cache whether the beneficiary is feeless.
|
|
1151
|
+
bool beneficiaryIsFeeless = _isFeeless(beneficiary);
|
|
1152
|
+
|
|
1125
1153
|
// Record the cash out.
|
|
1126
1154
|
(ruleset, reclaimAmount, cashOutTaxRate, hookSpecifications) = STORE.recordCashOutFor({
|
|
1127
1155
|
holder: holder,
|
|
1128
1156
|
projectId: projectId,
|
|
1129
1157
|
cashOutCount: cashOutCount,
|
|
1130
1158
|
tokenToReclaim: tokenToReclaim,
|
|
1131
|
-
beneficiaryIsFeeless:
|
|
1159
|
+
beneficiaryIsFeeless: beneficiaryIsFeeless,
|
|
1132
1160
|
metadata: metadata
|
|
1133
1161
|
});
|
|
1134
1162
|
|
|
@@ -1144,7 +1172,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1144
1172
|
// Send the reclaimed funds to the beneficiary.
|
|
1145
1173
|
if (reclaimAmount != 0) {
|
|
1146
1174
|
// Determine if a fee should be taken. Fees are not taken if the beneficiary is feeless.
|
|
1147
|
-
if (!
|
|
1175
|
+
if (!beneficiaryIsFeeless) {
|
|
1148
1176
|
if (cashOutTaxRate != 0) {
|
|
1149
1177
|
// Non-zero tax: fees apply to the full reclaim amount.
|
|
1150
1178
|
amountEligibleForFees += reclaimAmount;
|
|
@@ -1154,6 +1182,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1154
1182
|
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][tokenToReclaim];
|
|
1155
1183
|
if (feeFreeSurplus != 0) {
|
|
1156
1184
|
uint256 feeableAmount = reclaimAmount < feeFreeSurplus ? reclaimAmount : feeFreeSurplus;
|
|
1185
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1157
1186
|
_feeFreeSurplusOf[projectId][tokenToReclaim] = feeFreeSurplus - feeableAmount;
|
|
1158
1187
|
amountEligibleForFees += feeableAmount;
|
|
1159
1188
|
reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: feeableAmount, feePercent: FEE});
|
|
@@ -1220,6 +1249,13 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1220
1249
|
});
|
|
1221
1250
|
}
|
|
1222
1251
|
|
|
1252
|
+
/// @notice Revert if a value is less than the specified minimum.
|
|
1253
|
+
/// @param value The value to compare against the minimum.
|
|
1254
|
+
/// @param min The minimum acceptable value.
|
|
1255
|
+
function _checkMin(uint256 value, uint256 min) internal pure {
|
|
1256
|
+
if (value < min) revert JBMultiTerminal_UnderMin(value, min);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1223
1259
|
/// @notice Fund a project either by calling this terminal's internal `addToBalance` function or by calling the
|
|
1224
1260
|
/// recipient terminal's `addToBalance` function.
|
|
1225
1261
|
/// @param terminal The terminal on which the project is expecting to receive funds.
|
|
@@ -1237,7 +1273,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1237
1273
|
)
|
|
1238
1274
|
internal
|
|
1239
1275
|
{
|
|
1240
|
-
//
|
|
1276
|
+
// Use the local internal path when staying on this terminal. Otherwise use the efficient external equivalent,
|
|
1277
|
+
// which forwards value directly after granting a temporary pull allowance.
|
|
1241
1278
|
if (terminal == IJBTerminal(address(this))) {
|
|
1242
1279
|
_addToBalanceOf({
|
|
1243
1280
|
projectId: projectId,
|
|
@@ -1248,20 +1285,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1248
1285
|
metadata: metadata
|
|
1249
1286
|
});
|
|
1250
1287
|
} else {
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
// slither-disable-next-line reentrancy-events
|
|
1254
|
-
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1255
|
-
|
|
1256
|
-
// Add to balance.
|
|
1257
|
-
// If this terminal's token is the native token, send it in `msg.value`.
|
|
1258
|
-
terminal.addToBalanceOf{value: payValue}({
|
|
1259
|
-
projectId: projectId,
|
|
1260
|
-
token: token,
|
|
1261
|
-
amount: amount,
|
|
1262
|
-
shouldReturnHeldFees: false,
|
|
1263
|
-
memo: "",
|
|
1264
|
-
metadata: metadata
|
|
1288
|
+
_externalAddToBalance({
|
|
1289
|
+
terminal: terminal, projectId: projectId, token: token, amount: amount, metadata: metadata
|
|
1265
1290
|
});
|
|
1266
1291
|
}
|
|
1267
1292
|
}
|
|
@@ -1313,9 +1338,47 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1313
1338
|
memo: "",
|
|
1314
1339
|
metadata: metadata
|
|
1315
1340
|
});
|
|
1341
|
+
|
|
1342
|
+
// Revoke the temporary pull allowance now that the recipient terminal call has finished.
|
|
1343
|
+
_afterTransferTo({to: address(terminal), token: token});
|
|
1316
1344
|
}
|
|
1317
1345
|
}
|
|
1318
1346
|
|
|
1347
|
+
/// @notice Fund a project on another terminal by granting a temporary pull allowance for this call only.
|
|
1348
|
+
/// @param terminal The recipient terminal.
|
|
1349
|
+
/// @param projectId The ID of the project being funded.
|
|
1350
|
+
/// @param token The token being used.
|
|
1351
|
+
/// @param amount The amount being funded.
|
|
1352
|
+
/// @param metadata Additional metadata to include with the payment.
|
|
1353
|
+
function _externalAddToBalance(
|
|
1354
|
+
IJBTerminal terminal,
|
|
1355
|
+
uint256 projectId,
|
|
1356
|
+
address token,
|
|
1357
|
+
uint256 amount,
|
|
1358
|
+
bytes memory metadata
|
|
1359
|
+
)
|
|
1360
|
+
internal
|
|
1361
|
+
{
|
|
1362
|
+
// Trigger any inherited pre-transfer logic.
|
|
1363
|
+
// Keep a reference to the amount that'll be paid as a `msg.value`.
|
|
1364
|
+
// slither-disable-next-line reentrancy-events
|
|
1365
|
+
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1366
|
+
|
|
1367
|
+
// Add to balance on the recipient terminal.
|
|
1368
|
+
// If this terminal's token is the native token, send it in `msg.value`.
|
|
1369
|
+
terminal.addToBalanceOf{value: payValue}({
|
|
1370
|
+
projectId: projectId,
|
|
1371
|
+
token: token,
|
|
1372
|
+
amount: amount,
|
|
1373
|
+
shouldReturnHeldFees: false,
|
|
1374
|
+
memo: "",
|
|
1375
|
+
metadata: metadata
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
// Revoke the temporary pull allowance now that the recipient terminal call has finished.
|
|
1379
|
+
_afterTransferTo({to: address(terminal), token: token});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1319
1382
|
/// @notice Fulfills a list of cash out hook specifications.
|
|
1320
1383
|
/// @param projectId The ID of the project being cashed out from.
|
|
1321
1384
|
/// @param beneficiaryReclaimAmount The number of tokens that are being cashed out from the project.
|
|
@@ -1356,12 +1419,18 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1356
1419
|
cashOutMetadata: metadata
|
|
1357
1420
|
});
|
|
1358
1421
|
|
|
1359
|
-
|
|
1422
|
+
// slither-disable-next-line calls-loop
|
|
1423
|
+
for (uint256 i; i < specifications.length;) {
|
|
1360
1424
|
// Set the specification being iterated on.
|
|
1361
1425
|
JBCashOutHookSpecification memory specification = specifications[i];
|
|
1362
1426
|
|
|
1363
1427
|
// A noop specification is informational only and doesn't trigger the hook.
|
|
1364
|
-
if (specification.noop)
|
|
1428
|
+
if (specification.noop) {
|
|
1429
|
+
unchecked {
|
|
1430
|
+
++i;
|
|
1431
|
+
}
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1365
1434
|
|
|
1366
1435
|
// Get the fee for the specified amount.
|
|
1367
1436
|
uint256 specificationAmountFee = _isFeeless(address(specification.hook))
|
|
@@ -1393,9 +1462,12 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1393
1462
|
});
|
|
1394
1463
|
|
|
1395
1464
|
// Fulfill the specification.
|
|
1396
|
-
// slither-disable-next-line reentrancy-events
|
|
1465
|
+
// slither-disable-next-line reentrancy-events,calls-loop
|
|
1397
1466
|
specification.hook.afterCashOutRecordedWith{value: payValue}(context);
|
|
1398
1467
|
|
|
1468
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
1469
|
+
_afterTransferTo({to: address(specification.hook), token: beneficiaryReclaimAmount.token});
|
|
1470
|
+
|
|
1399
1471
|
emit HookAfterRecordCashOut({
|
|
1400
1472
|
hook: specification.hook,
|
|
1401
1473
|
context: context,
|
|
@@ -1403,6 +1475,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1403
1475
|
fee: specificationAmountFee,
|
|
1404
1476
|
caller: _msgSender()
|
|
1405
1477
|
});
|
|
1478
|
+
unchecked {
|
|
1479
|
+
++i;
|
|
1480
|
+
}
|
|
1406
1481
|
}
|
|
1407
1482
|
}
|
|
1408
1483
|
|
|
@@ -1442,12 +1517,18 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1442
1517
|
});
|
|
1443
1518
|
|
|
1444
1519
|
// Fulfill each specification through their pay hooks.
|
|
1445
|
-
|
|
1520
|
+
// slither-disable-next-line calls-loop
|
|
1521
|
+
for (uint256 i; i < specifications.length;) {
|
|
1446
1522
|
// Set the specification being iterated on.
|
|
1447
1523
|
JBPayHookSpecification memory specification = specifications[i];
|
|
1448
1524
|
|
|
1449
1525
|
// A noop specification is informational only and doesn't trigger the hook.
|
|
1450
|
-
if (specification.noop)
|
|
1526
|
+
if (specification.noop) {
|
|
1527
|
+
unchecked {
|
|
1528
|
+
++i;
|
|
1529
|
+
}
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1451
1532
|
|
|
1452
1533
|
// Pass the correct token `forwardedAmount` to the hook.
|
|
1453
1534
|
context.forwardedAmount = JBTokenAmount({
|
|
@@ -1468,15 +1549,21 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1468
1549
|
});
|
|
1469
1550
|
|
|
1470
1551
|
// Fulfill the specification.
|
|
1471
|
-
// slither-disable-next-line reentrancy-events
|
|
1552
|
+
// slither-disable-next-line reentrancy-events,calls-loop
|
|
1472
1553
|
specification.hook.afterPayRecordedWith{value: payValue}(context);
|
|
1473
1554
|
|
|
1555
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
1556
|
+
_afterTransferTo({to: address(specification.hook), token: tokenAmount.token});
|
|
1557
|
+
|
|
1474
1558
|
emit HookAfterRecordPay({
|
|
1475
1559
|
hook: specification.hook,
|
|
1476
1560
|
context: context,
|
|
1477
1561
|
specificationAmount: specification.amount,
|
|
1478
1562
|
caller: _msgSender()
|
|
1479
1563
|
});
|
|
1564
|
+
unchecked {
|
|
1565
|
+
++i;
|
|
1566
|
+
}
|
|
1480
1567
|
}
|
|
1481
1568
|
}
|
|
1482
1569
|
|
|
@@ -1590,6 +1677,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1590
1677
|
caller: _msgSender()
|
|
1591
1678
|
});
|
|
1592
1679
|
} catch (bytes memory reason) {
|
|
1680
|
+
// Fee processing failed — intentionally forgive the fee and return the amount to the project.
|
|
1681
|
+
// The held-fee entry (if any) was already deleted by `processHeldFeesOf` before this call, so there is no
|
|
1682
|
+
// retry path. This is by design: a broken or misconfigured fee route should not permanently lock project
|
|
1683
|
+
// funds. The `FeeReverted` event makes this observable off-chain.
|
|
1593
1684
|
emit FeeReverted({
|
|
1594
1685
|
projectId: projectId,
|
|
1595
1686
|
token: token,
|
|
@@ -1615,6 +1706,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1615
1706
|
}
|
|
1616
1707
|
|
|
1617
1708
|
/// @notice Returns held fees to the project who paid them based on the specified amount.
|
|
1709
|
+
/// @dev Fee rounding during partial replenishment can zero out dust-level fee entries (< 40 wei at 2.5% fee).
|
|
1710
|
+
/// This is accepted behavior — dust fees are economically insignificant.
|
|
1618
1711
|
/// @param projectId The project held fees are being returned to.
|
|
1619
1712
|
/// @param token The token that the held fees are in.
|
|
1620
1713
|
/// @param amount The amount to base the calculation on, as a fixed point number with the same number of decimals
|
|
@@ -1641,7 +1734,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1641
1734
|
uint256 newStartIndex = startIndex;
|
|
1642
1735
|
|
|
1643
1736
|
// Process each fee.
|
|
1644
|
-
for (uint256 i; i < count;
|
|
1737
|
+
for (uint256 i; i < count;) {
|
|
1645
1738
|
// Save the fee being iterated on.
|
|
1646
1739
|
JBFee memory heldFee = _heldFeesOf[projectId][token][startIndex + i];
|
|
1647
1740
|
|
|
@@ -1675,6 +1768,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1675
1768
|
leftoverAmount = 0;
|
|
1676
1769
|
}
|
|
1677
1770
|
}
|
|
1771
|
+
unchecked {
|
|
1772
|
+
++i;
|
|
1773
|
+
}
|
|
1678
1774
|
}
|
|
1679
1775
|
|
|
1680
1776
|
// Update the next held fee index.
|
|
@@ -1707,6 +1803,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1707
1803
|
internal
|
|
1708
1804
|
returns (uint256 amountPaidOut)
|
|
1709
1805
|
{
|
|
1806
|
+
// Cache the message sender.
|
|
1807
|
+
address sender = _msgSender();
|
|
1808
|
+
|
|
1710
1809
|
// Keep a reference to the ruleset.
|
|
1711
1810
|
JBRuleset memory ruleset;
|
|
1712
1811
|
|
|
@@ -1715,6 +1814,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1715
1814
|
STORE.recordPayoutFor({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
1716
1815
|
|
|
1717
1816
|
// Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
|
|
1817
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1718
1818
|
_capFeeFreeSurplus({projectId: projectId, token: token});
|
|
1719
1819
|
|
|
1720
1820
|
// Get a reference to the project's owner.
|
|
@@ -1739,7 +1839,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1739
1839
|
token: token,
|
|
1740
1840
|
rulesetId: ruleset.id,
|
|
1741
1841
|
amount: amountPaidOut,
|
|
1742
|
-
caller:
|
|
1842
|
+
caller: sender
|
|
1743
1843
|
});
|
|
1744
1844
|
|
|
1745
1845
|
// Send any leftover funds to the project owner and update the fee tracking accordingly.
|
|
@@ -1758,6 +1858,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1758
1858
|
leftoverPayoutAmount -= fee;
|
|
1759
1859
|
}
|
|
1760
1860
|
} catch (bytes memory reason) {
|
|
1861
|
+
// slither-disable-next-line reentrancy-events
|
|
1761
1862
|
emit PayoutTransferReverted({
|
|
1762
1863
|
projectId: projectId,
|
|
1763
1864
|
addr: projectOwner,
|
|
@@ -1765,7 +1866,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1765
1866
|
amount: leftoverPayoutAmount - fee,
|
|
1766
1867
|
fee: fee,
|
|
1767
1868
|
reason: reason,
|
|
1768
|
-
caller:
|
|
1869
|
+
caller: sender
|
|
1769
1870
|
});
|
|
1770
1871
|
|
|
1771
1872
|
// Add balance back to the project.
|
|
@@ -1791,7 +1892,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1791
1892
|
amountPaidOut: amountPaidOut,
|
|
1792
1893
|
fee: feeTaken,
|
|
1793
1894
|
netLeftoverPayoutAmount: leftoverPayoutAmount,
|
|
1794
|
-
caller:
|
|
1895
|
+
caller: sender
|
|
1795
1896
|
});
|
|
1796
1897
|
}
|
|
1797
1898
|
|
|
@@ -1916,6 +2017,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1916
2017
|
STORE.recordUsedAllowanceOf({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
1917
2018
|
|
|
1918
2019
|
// Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
|
|
2020
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1919
2021
|
_capFeeFreeSurplus({projectId: projectId, token: token});
|
|
1920
2022
|
|
|
1921
2023
|
// Take a fee from the `amountPaidOut`, if needed.
|
|
@@ -1952,32 +2054,24 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1952
2054
|
}
|
|
1953
2055
|
}
|
|
1954
2056
|
|
|
1955
|
-
/// @notice Cap fee-free surplus at the project's remaining balance after an outflow.
|
|
1956
|
-
/// @dev Non-fee-free funds are considered to leave first. Fee-free surplus only decreases when the remaining
|
|
1957
|
-
/// balance can no longer support it. This prevents attackers from using outflows to drain the fee-free counter
|
|
1958
|
-
/// and then cashing out without incurring fees.
|
|
1959
|
-
/// @param projectId The ID of the project.
|
|
1960
|
-
/// @param token The token whose fee-free surplus to cap.
|
|
1961
|
-
function _capFeeFreeSurplus(uint256 projectId, address token) internal {
|
|
1962
|
-
// Get the current fee-free surplus for this project/token pair.
|
|
1963
|
-
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][token];
|
|
1964
|
-
|
|
1965
|
-
// Nothing to cap if there's no fee-free surplus tracked.
|
|
1966
|
-
if (feeFreeSurplus == 0) return;
|
|
1967
|
-
|
|
1968
|
-
// Get the project's remaining balance (already decremented by the store's record call).
|
|
1969
|
-
uint256 remainingBalance = STORE.balanceOf({terminal: address(this), projectId: projectId, token: token});
|
|
1970
|
-
|
|
1971
|
-
// Cap fee-free surplus at the remaining balance.
|
|
1972
|
-
if (feeFreeSurplus > remainingBalance) {
|
|
1973
|
-
_feeFreeSurplusOf[projectId][token] = remainingBalance;
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
2057
|
//*********************************************************************//
|
|
1978
2058
|
// -------------------------- internal views ------------------------- //
|
|
1979
2059
|
//*********************************************************************//
|
|
1980
2060
|
|
|
2061
|
+
/// @notice Logic to be triggered after transferring tokens from this terminal.
|
|
2062
|
+
/// @dev Clears any allowance granted by `_beforeTransferTo` so receivers cannot retain pull access after the call.
|
|
2063
|
+
/// @param to The address whose temporary pull allowance should be cleared.
|
|
2064
|
+
/// @param token The token whose temporary allowance should be cleared.
|
|
2065
|
+
function _afterTransferTo(address to, address token) internal view {
|
|
2066
|
+
// Native-token transfers use `msg.value`, so there is no ERC-20 approval to clear.
|
|
2067
|
+
if (token == JBConstants.NATIVE_TOKEN) return;
|
|
2068
|
+
|
|
2069
|
+
// Revert if the callee returned without consuming the full forwarded ERC-20 amount.
|
|
2070
|
+
// slither-disable-next-line calls-loop
|
|
2071
|
+
uint256 allowance = IERC20(token).allowance({owner: address(this), spender: to});
|
|
2072
|
+
if (allowance != 0) revert JBMultiTerminal_TemporaryAllowanceNotConsumed(token, to, allowance);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1981
2075
|
/// @notice Returns a project's accounting context for a token, reverting if it is not accepted.
|
|
1982
2076
|
/// @param projectId The ID of the project to get the accounting context for.
|
|
1983
2077
|
/// @param token The token to get the accounting context for.
|
package/src/JBPermissions.sol
CHANGED
|
@@ -160,7 +160,7 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
|
|
|
160
160
|
|
|
161
161
|
// Returns true for empty permission arrays by design (vacuous truth). An empty set of
|
|
162
162
|
// required permissions is trivially satisfied. Callers should validate non-empty permission arrays if needed.
|
|
163
|
-
for (uint256 i; i < permissionIds.length;
|
|
163
|
+
for (uint256 i; i < permissionIds.length;) {
|
|
164
164
|
// Set the permission being iterated on.
|
|
165
165
|
uint256 permissionId = permissionIds[i];
|
|
166
166
|
|
|
@@ -176,6 +176,9 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
|
|
|
176
176
|
) {
|
|
177
177
|
return false;
|
|
178
178
|
}
|
|
179
|
+
unchecked {
|
|
180
|
+
++i;
|
|
181
|
+
}
|
|
179
182
|
}
|
|
180
183
|
return true;
|
|
181
184
|
}
|
|
@@ -210,29 +213,23 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
|
|
|
210
213
|
// Indexes above 255 don't exist
|
|
211
214
|
if (permissionId > 255) revert JBPermissions_PermissionIdOutOfBounds(permissionId);
|
|
212
215
|
|
|
216
|
+
// Cache both permission slots upfront to avoid redundant storage reads.
|
|
217
|
+
uint256 projectPermissions = permissionsOf[operator][account][projectId];
|
|
218
|
+
uint256 wildcardPermissions =
|
|
219
|
+
includeWildcardProjectId ? permissionsOf[operator][account][WILDCARD_PROJECT_ID] : 0;
|
|
220
|
+
|
|
213
221
|
// If the ROOT permission is set and should be included, return true.
|
|
214
222
|
if (
|
|
215
223
|
includeRoot
|
|
216
|
-
&& (_includesPermission({
|
|
217
|
-
|
|
218
|
-
})
|
|
219
|
-
|| (includeWildcardProjectId
|
|
220
|
-
&& _includesPermission({
|
|
221
|
-
permissions: permissionsOf[operator][account][WILDCARD_PROJECT_ID],
|
|
222
|
-
permissionId: JBPermissionIds.ROOT
|
|
223
|
-
})))
|
|
224
|
+
&& (_includesPermission({permissions: projectPermissions, permissionId: JBPermissionIds.ROOT})
|
|
225
|
+
|| _includesPermission({permissions: wildcardPermissions, permissionId: JBPermissionIds.ROOT}))
|
|
224
226
|
) {
|
|
225
227
|
return true;
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
// Otherwise return the t/f flag of the specified id.
|
|
229
|
-
return _includesPermission({
|
|
230
|
-
|
|
231
|
-
})
|
|
232
|
-
|| (includeWildcardProjectId
|
|
233
|
-
&& _includesPermission({
|
|
234
|
-
permissions: permissionsOf[operator][account][WILDCARD_PROJECT_ID], permissionId: permissionId
|
|
235
|
-
}));
|
|
231
|
+
return _includesPermission({permissions: projectPermissions, permissionId: permissionId})
|
|
232
|
+
|| _includesPermission({permissions: wildcardPermissions, permissionId: permissionId});
|
|
236
233
|
}
|
|
237
234
|
|
|
238
235
|
//*********************************************************************//
|
|
@@ -251,13 +248,16 @@ contract JBPermissions is ERC2771Context, IJBPermissions {
|
|
|
251
248
|
/// @param permissionIds The IDs of the permissions to pack.
|
|
252
249
|
/// @return packed The packed value.
|
|
253
250
|
function _packedPermissions(uint8[] calldata permissionIds) internal pure returns (uint256 packed) {
|
|
254
|
-
for (uint256 i; i < permissionIds.length;
|
|
251
|
+
for (uint256 i; i < permissionIds.length;) {
|
|
255
252
|
// Set the permission being iterated on.
|
|
256
253
|
uint256 permissionId = permissionIds[i];
|
|
257
254
|
|
|
258
255
|
// Turn on the bit at the ID.
|
|
259
256
|
// forge-lint: disable-next-line(incorrect-shift)
|
|
260
257
|
packed |= 1 << permissionId;
|
|
258
|
+
unchecked {
|
|
259
|
+
++i;
|
|
260
|
+
}
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
}
|