@bananapus/core-v6 0.0.29 → 0.0.31
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 +2 -3
- 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 +11 -3
- package/src/JBDirectory.sol +1 -0
- package/src/JBMultiTerminal.sol +126 -72
- package/src/JBRulesets.sol +2 -1
- package/src/JBTerminalStore.sol +22 -11
- package/src/abstract/JBControlled.sol +7 -1
- 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 +1 -1
- package/src/libraries/JBPayoutSplitGroupLib.sol +3 -1
- package/src/libraries/JBRulesetMetadataResolver.sol +1 -1
- package/src/libraries/JBSplitGroupIds.sol +1 -1
- package/src/libraries/JBSurplus.sol +1 -1
- package/src/structs/JBSplit.sol +4 -1
- package/test/TestForwardedTokenConsumption.sol +418 -0
- package/test/audit/CycledSurplusAllowanceReset.t.sol +184 -0
- package/test/units/static/JBController/TestPreviewMintOf.sol +5 -3
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +15 -11
- 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/JBController.sol
CHANGED
|
@@ -158,6 +158,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
158
158
|
RULESETS = rulesets;
|
|
159
159
|
SPLITS = splits;
|
|
160
160
|
TOKENS = tokens;
|
|
161
|
+
// slither-disable-next-line missing-zero-check
|
|
161
162
|
OMNICHAIN_RULESET_OPERATOR = omnichainRulesetOperator;
|
|
162
163
|
}
|
|
163
164
|
|
|
@@ -820,8 +821,8 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
820
821
|
override
|
|
821
822
|
returns (uint256 beneficiaryTokenCount, uint256 reservedTokenCount)
|
|
822
823
|
{
|
|
823
|
-
//
|
|
824
|
-
if (tokenCount == 0)
|
|
824
|
+
// A zero preview amount means there are no tokens to split.
|
|
825
|
+
if (tokenCount == 0) return (0, 0);
|
|
825
826
|
|
|
826
827
|
// Keep a reference to the current ruleset.
|
|
827
828
|
JBRuleset memory ruleset = _currentRulesetOf(projectId);
|
|
@@ -900,6 +901,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
900
901
|
JBTerminalConfig memory terminalConfig = terminalConfigurations[i];
|
|
901
902
|
|
|
902
903
|
// Add the accounting contexts for the specified tokens.
|
|
904
|
+
// slither-disable-next-line calls-loop
|
|
903
905
|
terminalConfig.terminal
|
|
904
906
|
.addAccountingContextsFor({
|
|
905
907
|
projectId: projectId, accountingContexts: terminalConfig.accountingContextsToAccept
|
|
@@ -945,6 +947,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
945
947
|
}
|
|
946
948
|
|
|
947
949
|
// Queue its ruleset.
|
|
950
|
+
// slither-disable-next-line calls-loop
|
|
948
951
|
JBRuleset memory ruleset = RULESETS.queueFor({
|
|
949
952
|
projectId: projectId,
|
|
950
953
|
duration: rulesetConfig.duration,
|
|
@@ -956,11 +959,13 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
956
959
|
});
|
|
957
960
|
|
|
958
961
|
// Set its split groups.
|
|
962
|
+
// slither-disable-next-line calls-loop
|
|
959
963
|
SPLITS.setSplitGroupsOf({
|
|
960
964
|
projectId: projectId, rulesetId: ruleset.id, splitGroups: rulesetConfig.splitGroups
|
|
961
965
|
});
|
|
962
966
|
|
|
963
967
|
// Set its fund access limits.
|
|
968
|
+
// slither-disable-next-line calls-loop
|
|
964
969
|
FUND_ACCESS_LIMITS.setFundAccessLimitsFor({
|
|
965
970
|
projectId: projectId, rulesetId: ruleset.id, fundAccessLimitGroups: rulesetConfig.fundAccessLimitGroups
|
|
966
971
|
});
|
|
@@ -1023,7 +1028,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1023
1028
|
projectId: projectId, tokenCount: splitTokenCount, recipient: address(split.hook), token: token
|
|
1024
1029
|
});
|
|
1025
1030
|
|
|
1026
|
-
// slither-disable-next-line reentrancy-events
|
|
1031
|
+
// slither-disable-next-line calls-loop,reentrancy-events
|
|
1027
1032
|
try split.hook
|
|
1028
1033
|
.processSplitWith(
|
|
1029
1034
|
JBSplitHookContext({
|
|
@@ -1047,6 +1052,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1047
1052
|
|
|
1048
1053
|
if (split.projectId != 0) {
|
|
1049
1054
|
// Get a reference to the receiving project's primary payment terminal for the token.
|
|
1055
|
+
// slither-disable-next-line calls-loop
|
|
1050
1056
|
IJBTerminal terminal = token == IJBToken(address(0))
|
|
1051
1057
|
? IJBTerminal(address(0))
|
|
1052
1058
|
: DIRECTORY.primaryTerminalOf({projectId: split.projectId, token: address(token)});
|
|
@@ -1065,6 +1071,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1065
1071
|
bytes memory metadata = bytes(abi.encodePacked(projectId));
|
|
1066
1072
|
|
|
1067
1073
|
// Try to fulfill the payment.
|
|
1074
|
+
// slither-disable-next-line calls-loop
|
|
1068
1075
|
try this.executePayReservedTokenToTerminal({
|
|
1069
1076
|
projectId: split.projectId,
|
|
1070
1077
|
terminal: terminal,
|
|
@@ -1088,6 +1095,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1088
1095
|
}
|
|
1089
1096
|
} else if (beneficiary == address(0xdead)) {
|
|
1090
1097
|
// If the split has no project ID, and the beneficiary is 0xdead, burn.
|
|
1098
|
+
// slither-disable-next-line calls-loop
|
|
1091
1099
|
TOKENS.burnFrom({holder: address(this), projectId: projectId, count: splitTokenCount});
|
|
1092
1100
|
} else {
|
|
1093
1101
|
// If the split has no project Id, send to beneficiary.
|
package/src/JBDirectory.sol
CHANGED
|
@@ -139,6 +139,7 @@ contract JBDirectory is JBPermissioned, Ownable, IJBDirectory {
|
|
|
139
139
|
// slither-disable-next-line reentrancy-no-eth
|
|
140
140
|
controllerOf[projectId] = controller;
|
|
141
141
|
|
|
142
|
+
// slither-disable-next-line reentrancy-events
|
|
142
143
|
emit SetController({projectId: projectId, controller: controller, caller: msg.sender});
|
|
143
144
|
|
|
144
145
|
// Notify the new controller that migration is complete and it is now the active controller.
|
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 ------------------------ //
|
|
@@ -209,6 +208,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
209
208
|
|
|
210
209
|
// Emit an event for each accounting context.
|
|
211
210
|
for (uint256 i; i < accountingContexts.length; i++) {
|
|
211
|
+
// slither-disable-next-line reentrancy-events
|
|
212
212
|
emit SetAccountingContext({projectId: projectId, context: accountingContexts[i], caller: _msgSender()});
|
|
213
213
|
}
|
|
214
214
|
}
|
|
@@ -251,7 +251,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
251
251
|
/// those tokens.
|
|
252
252
|
/// @param holder The account whose tokens are being cashed out.
|
|
253
253
|
/// @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
|
|
254
|
+
/// @param cashOutCount The number of project tokens to cash out and burn, as a fixed point number with 18
|
|
255
|
+
/// decimals.
|
|
255
256
|
/// @param tokenToReclaim The token being reclaimed.
|
|
256
257
|
/// @param minTokensReclaimed The minimum number of terminal tokens expected in return, as a fixed point number with
|
|
257
258
|
/// the same number of decimals as this terminal. If the amount of tokens minted for the beneficiary would be less
|
|
@@ -289,9 +290,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
289
290
|
});
|
|
290
291
|
|
|
291
292
|
// The amount being reclaimed must be at least as much as was expected.
|
|
292
|
-
|
|
293
|
-
revert JBMultiTerminal_UnderMinTokensReclaimed(reclaimAmount, minTokensReclaimed);
|
|
294
|
-
}
|
|
293
|
+
_checkMin({value: reclaimAmount, min: minTokensReclaimed});
|
|
295
294
|
}
|
|
296
295
|
|
|
297
296
|
/// @notice Executes a payout to a split.
|
|
@@ -349,6 +348,9 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
349
348
|
// If this terminal's token is the native token, send it in `msg.value`.
|
|
350
349
|
split.hook.processSplitWith{value: payValue}(context);
|
|
351
350
|
|
|
351
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
352
|
+
_afterTransferTo({to: address(split.hook), token: token});
|
|
353
|
+
|
|
352
354
|
// Otherwise, if a project is specified, make a payment to it.
|
|
353
355
|
} else if (split.projectId != 0) {
|
|
354
356
|
// Get a reference to the terminal being used.
|
|
@@ -542,20 +544,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
542
544
|
// Transfer the balance minus the fee to the new terminal.
|
|
543
545
|
uint256 migrationAmount = balance - feeAmount;
|
|
544
546
|
|
|
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("")
|
|
547
|
+
_externalAddToBalance({
|
|
548
|
+
terminal: to, projectId: projectId, token: token, amount: migrationAmount, metadata: bytes("")
|
|
559
549
|
});
|
|
560
550
|
}
|
|
561
551
|
}
|
|
@@ -612,9 +602,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
612
602
|
}
|
|
613
603
|
|
|
614
604
|
// The token count for the beneficiary must be greater than or equal to the specified minimum.
|
|
615
|
-
|
|
616
|
-
revert JBMultiTerminal_UnderMinReturnedTokens(beneficiaryTokenCount, minReturnedTokens);
|
|
617
|
-
}
|
|
605
|
+
_checkMin({value: beneficiaryTokenCount, min: minReturnedTokens});
|
|
618
606
|
}
|
|
619
607
|
|
|
620
608
|
/// @notice Process any fees that are being held for the project.
|
|
@@ -654,10 +642,14 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
654
642
|
// if the current fee isn't yet unlocked.
|
|
655
643
|
if (heldFee.unlockTimestamp > block.timestamp) break;
|
|
656
644
|
|
|
657
|
-
// Delete the entry
|
|
645
|
+
// Delete the entry and advance the index *before* the external call. This is intentional:
|
|
646
|
+
// 1. It prevents reentrancy from reprocessing the same fee.
|
|
647
|
+
// 2. If `_processFee` fails (try-catch), the fee amount is returned to the project's balance via
|
|
648
|
+
// `_recordAddedBalanceFor` — the fee is forgiven rather than retried. This is a deliberate design
|
|
649
|
+
// choice: projects should not have funds permanently stuck because the fee route is misconfigured or
|
|
650
|
+
// reverting.
|
|
651
|
+
// A `FeeReverted` event is emitted so the forgiveness is observable off-chain.
|
|
658
652
|
delete _heldFeesOf[projectId][token][currentIndex];
|
|
659
|
-
|
|
660
|
-
// Update the index before the external call to prevent reentrancy from reprocessing the same fee.
|
|
661
653
|
_nextHeldFeeIndexOf[projectId][token] = currentIndex + 1;
|
|
662
654
|
|
|
663
655
|
// Process the fee.
|
|
@@ -715,9 +707,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
715
707
|
amountPaidOut = _sendPayoutsOf({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
716
708
|
|
|
717
709
|
// The amount being paid out must be at least as much as was expected.
|
|
718
|
-
|
|
719
|
-
revert JBMultiTerminal_UnderMinTokensPaidOut(amountPaidOut, minTokensPaidOut);
|
|
720
|
-
}
|
|
710
|
+
_checkMin({value: amountPaidOut, min: minTokensPaidOut});
|
|
721
711
|
}
|
|
722
712
|
|
|
723
713
|
/// @notice Allows a project to pay out funds from its surplus up to the current surplus allowance.
|
|
@@ -770,9 +760,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
770
760
|
});
|
|
771
761
|
|
|
772
762
|
// The amount being withdrawn must be at least as much as was expected.
|
|
773
|
-
|
|
774
|
-
revert JBMultiTerminal_UnderMinTokensPaidOut(netAmountPaidOut, minTokensPaidOut);
|
|
775
|
-
}
|
|
763
|
+
_checkMin({value: netAmountPaidOut, min: minTokensPaidOut});
|
|
776
764
|
}
|
|
777
765
|
|
|
778
766
|
//*********************************************************************//
|
|
@@ -1022,6 +1010,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1022
1010
|
// Set the allowance to `spend` tokens for the user.
|
|
1023
1011
|
try PERMIT2.permit({owner: _msgSender(), permitSingle: permitSingle, signature: allowance.signature}) {}
|
|
1024
1012
|
catch (bytes memory reason) {
|
|
1013
|
+
// slither-disable-next-line reentrancy-events
|
|
1025
1014
|
emit Permit2AllowanceFailed(token, _msgSender(), reason);
|
|
1026
1015
|
}
|
|
1027
1016
|
}
|
|
@@ -1087,6 +1076,29 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1087
1076
|
return 0;
|
|
1088
1077
|
}
|
|
1089
1078
|
|
|
1079
|
+
/// @notice Cap fee-free surplus at the project's remaining balance after an outflow.
|
|
1080
|
+
/// @dev Non-fee-free funds are considered to leave first. Fee-free surplus only decreases when the remaining
|
|
1081
|
+
/// balance can no longer support it. This prevents attackers from using outflows to drain the fee-free counter
|
|
1082
|
+
/// and then cashing out without incurring fees.
|
|
1083
|
+
/// @param projectId The ID of the project.
|
|
1084
|
+
/// @param token The token whose fee-free surplus to cap.
|
|
1085
|
+
function _capFeeFreeSurplus(uint256 projectId, address token) internal {
|
|
1086
|
+
// Get the current fee-free surplus for this project/token pair.
|
|
1087
|
+
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][token];
|
|
1088
|
+
|
|
1089
|
+
// Nothing to cap if there's no fee-free surplus tracked.
|
|
1090
|
+
if (feeFreeSurplus == 0) return;
|
|
1091
|
+
|
|
1092
|
+
// Get the project's remaining balance (already decremented by the store's record call).
|
|
1093
|
+
uint256 remainingBalance = STORE.balanceOf({terminal: address(this), projectId: projectId, token: token});
|
|
1094
|
+
|
|
1095
|
+
// Cap fee-free surplus at the remaining balance.
|
|
1096
|
+
if (feeFreeSurplus > remainingBalance) {
|
|
1097
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1098
|
+
_feeFreeSurplusOf[projectId][token] = remainingBalance;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1090
1102
|
/// @notice Holders can cash out their tokens to reclaim some of a project's surplus, or to trigger rules determined
|
|
1091
1103
|
/// by
|
|
1092
1104
|
/// the project's current ruleset's data hook.
|
|
@@ -1154,6 +1166,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1154
1166
|
uint256 feeFreeSurplus = _feeFreeSurplusOf[projectId][tokenToReclaim];
|
|
1155
1167
|
if (feeFreeSurplus != 0) {
|
|
1156
1168
|
uint256 feeableAmount = reclaimAmount < feeFreeSurplus ? reclaimAmount : feeFreeSurplus;
|
|
1169
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1157
1170
|
_feeFreeSurplusOf[projectId][tokenToReclaim] = feeFreeSurplus - feeableAmount;
|
|
1158
1171
|
amountEligibleForFees += feeableAmount;
|
|
1159
1172
|
reclaimAmount -= JBFees.feeAmountFrom({amountBeforeFee: feeableAmount, feePercent: FEE});
|
|
@@ -1220,6 +1233,13 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1220
1233
|
});
|
|
1221
1234
|
}
|
|
1222
1235
|
|
|
1236
|
+
/// @notice Revert if a value is less than the specified minimum.
|
|
1237
|
+
/// @param value The value to compare against the minimum.
|
|
1238
|
+
/// @param min The minimum acceptable value.
|
|
1239
|
+
function _checkMin(uint256 value, uint256 min) internal pure {
|
|
1240
|
+
if (value < min) revert JBMultiTerminal_UnderMin(value, min);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1223
1243
|
/// @notice Fund a project either by calling this terminal's internal `addToBalance` function or by calling the
|
|
1224
1244
|
/// recipient terminal's `addToBalance` function.
|
|
1225
1245
|
/// @param terminal The terminal on which the project is expecting to receive funds.
|
|
@@ -1237,7 +1257,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1237
1257
|
)
|
|
1238
1258
|
internal
|
|
1239
1259
|
{
|
|
1240
|
-
//
|
|
1260
|
+
// Use the local internal path when staying on this terminal. Otherwise use the efficient external equivalent,
|
|
1261
|
+
// which forwards value directly after granting a temporary pull allowance.
|
|
1241
1262
|
if (terminal == IJBTerminal(address(this))) {
|
|
1242
1263
|
_addToBalanceOf({
|
|
1243
1264
|
projectId: projectId,
|
|
@@ -1248,20 +1269,8 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1248
1269
|
metadata: metadata
|
|
1249
1270
|
});
|
|
1250
1271
|
} 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
|
|
1272
|
+
_externalAddToBalance({
|
|
1273
|
+
terminal: terminal, projectId: projectId, token: token, amount: amount, metadata: metadata
|
|
1265
1274
|
});
|
|
1266
1275
|
}
|
|
1267
1276
|
}
|
|
@@ -1313,9 +1322,47 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1313
1322
|
memo: "",
|
|
1314
1323
|
metadata: metadata
|
|
1315
1324
|
});
|
|
1325
|
+
|
|
1326
|
+
// Revoke the temporary pull allowance now that the recipient terminal call has finished.
|
|
1327
|
+
_afterTransferTo({to: address(terminal), token: token});
|
|
1316
1328
|
}
|
|
1317
1329
|
}
|
|
1318
1330
|
|
|
1331
|
+
/// @notice Fund a project on another terminal by granting a temporary pull allowance for this call only.
|
|
1332
|
+
/// @param terminal The recipient terminal.
|
|
1333
|
+
/// @param projectId The ID of the project being funded.
|
|
1334
|
+
/// @param token The token being used.
|
|
1335
|
+
/// @param amount The amount being funded.
|
|
1336
|
+
/// @param metadata Additional metadata to include with the payment.
|
|
1337
|
+
function _externalAddToBalance(
|
|
1338
|
+
IJBTerminal terminal,
|
|
1339
|
+
uint256 projectId,
|
|
1340
|
+
address token,
|
|
1341
|
+
uint256 amount,
|
|
1342
|
+
bytes memory metadata
|
|
1343
|
+
)
|
|
1344
|
+
internal
|
|
1345
|
+
{
|
|
1346
|
+
// Trigger any inherited pre-transfer logic.
|
|
1347
|
+
// Keep a reference to the amount that'll be paid as a `msg.value`.
|
|
1348
|
+
// slither-disable-next-line reentrancy-events
|
|
1349
|
+
uint256 payValue = _beforeTransferTo({to: address(terminal), token: token, amount: amount});
|
|
1350
|
+
|
|
1351
|
+
// Add to balance on the recipient terminal.
|
|
1352
|
+
// If this terminal's token is the native token, send it in `msg.value`.
|
|
1353
|
+
terminal.addToBalanceOf{value: payValue}({
|
|
1354
|
+
projectId: projectId,
|
|
1355
|
+
token: token,
|
|
1356
|
+
amount: amount,
|
|
1357
|
+
shouldReturnHeldFees: false,
|
|
1358
|
+
memo: "",
|
|
1359
|
+
metadata: metadata
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
// Revoke the temporary pull allowance now that the recipient terminal call has finished.
|
|
1363
|
+
_afterTransferTo({to: address(terminal), token: token});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1319
1366
|
/// @notice Fulfills a list of cash out hook specifications.
|
|
1320
1367
|
/// @param projectId The ID of the project being cashed out from.
|
|
1321
1368
|
/// @param beneficiaryReclaimAmount The number of tokens that are being cashed out from the project.
|
|
@@ -1356,6 +1403,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1356
1403
|
cashOutMetadata: metadata
|
|
1357
1404
|
});
|
|
1358
1405
|
|
|
1406
|
+
// slither-disable-next-line calls-loop
|
|
1359
1407
|
for (uint256 i; i < specifications.length; i++) {
|
|
1360
1408
|
// Set the specification being iterated on.
|
|
1361
1409
|
JBCashOutHookSpecification memory specification = specifications[i];
|
|
@@ -1393,9 +1441,12 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1393
1441
|
});
|
|
1394
1442
|
|
|
1395
1443
|
// Fulfill the specification.
|
|
1396
|
-
// slither-disable-next-line reentrancy-events
|
|
1444
|
+
// slither-disable-next-line reentrancy-events,calls-loop
|
|
1397
1445
|
specification.hook.afterCashOutRecordedWith{value: payValue}(context);
|
|
1398
1446
|
|
|
1447
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
1448
|
+
_afterTransferTo({to: address(specification.hook), token: beneficiaryReclaimAmount.token});
|
|
1449
|
+
|
|
1399
1450
|
emit HookAfterRecordCashOut({
|
|
1400
1451
|
hook: specification.hook,
|
|
1401
1452
|
context: context,
|
|
@@ -1442,6 +1493,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1442
1493
|
});
|
|
1443
1494
|
|
|
1444
1495
|
// Fulfill each specification through their pay hooks.
|
|
1496
|
+
// slither-disable-next-line calls-loop
|
|
1445
1497
|
for (uint256 i; i < specifications.length; i++) {
|
|
1446
1498
|
// Set the specification being iterated on.
|
|
1447
1499
|
JBPayHookSpecification memory specification = specifications[i];
|
|
@@ -1468,9 +1520,12 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1468
1520
|
});
|
|
1469
1521
|
|
|
1470
1522
|
// Fulfill the specification.
|
|
1471
|
-
// slither-disable-next-line reentrancy-events
|
|
1523
|
+
// slither-disable-next-line reentrancy-events,calls-loop
|
|
1472
1524
|
specification.hook.afterPayRecordedWith{value: payValue}(context);
|
|
1473
1525
|
|
|
1526
|
+
// Revoke the temporary pull allowance now that the hook call has finished.
|
|
1527
|
+
_afterTransferTo({to: address(specification.hook), token: tokenAmount.token});
|
|
1528
|
+
|
|
1474
1529
|
emit HookAfterRecordPay({
|
|
1475
1530
|
hook: specification.hook,
|
|
1476
1531
|
context: context,
|
|
@@ -1590,6 +1645,10 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1590
1645
|
caller: _msgSender()
|
|
1591
1646
|
});
|
|
1592
1647
|
} catch (bytes memory reason) {
|
|
1648
|
+
// Fee processing failed — intentionally forgive the fee and return the amount to the project.
|
|
1649
|
+
// The held-fee entry (if any) was already deleted by `processHeldFeesOf` before this call, so there is no
|
|
1650
|
+
// retry path. This is by design: a broken or misconfigured fee route should not permanently lock project
|
|
1651
|
+
// funds. The `FeeReverted` event makes this observable off-chain.
|
|
1593
1652
|
emit FeeReverted({
|
|
1594
1653
|
projectId: projectId,
|
|
1595
1654
|
token: token,
|
|
@@ -1715,6 +1774,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1715
1774
|
STORE.recordPayoutFor({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
1716
1775
|
|
|
1717
1776
|
// Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
|
|
1777
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1718
1778
|
_capFeeFreeSurplus({projectId: projectId, token: token});
|
|
1719
1779
|
|
|
1720
1780
|
// Get a reference to the project's owner.
|
|
@@ -1758,6 +1818,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1758
1818
|
leftoverPayoutAmount -= fee;
|
|
1759
1819
|
}
|
|
1760
1820
|
} catch (bytes memory reason) {
|
|
1821
|
+
// slither-disable-next-line reentrancy-events
|
|
1761
1822
|
emit PayoutTransferReverted({
|
|
1762
1823
|
projectId: projectId,
|
|
1763
1824
|
addr: projectOwner,
|
|
@@ -1916,6 +1977,7 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1916
1977
|
STORE.recordUsedAllowanceOf({projectId: projectId, token: token, amount: amount, currency: currency});
|
|
1917
1978
|
|
|
1918
1979
|
// Cap fee-free surplus at remaining balance. Non-fee-free funds leave first.
|
|
1980
|
+
// slither-disable-next-line reentrancy-no-eth,reentrancy-eth,reentrancy-benign
|
|
1919
1981
|
_capFeeFreeSurplus({projectId: projectId, token: token});
|
|
1920
1982
|
|
|
1921
1983
|
// Take a fee from the `amountPaidOut`, if needed.
|
|
@@ -1952,32 +2014,24 @@ contract JBMultiTerminal is JBPermissioned, ERC2771Context, IJBMultiTerminal {
|
|
|
1952
2014
|
}
|
|
1953
2015
|
}
|
|
1954
2016
|
|
|
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
2017
|
//*********************************************************************//
|
|
1978
2018
|
// -------------------------- internal views ------------------------- //
|
|
1979
2019
|
//*********************************************************************//
|
|
1980
2020
|
|
|
2021
|
+
/// @notice Logic to be triggered after transferring tokens from this terminal.
|
|
2022
|
+
/// @dev Clears any allowance granted by `_beforeTransferTo` so receivers cannot retain pull access after the call.
|
|
2023
|
+
/// @param to The address whose temporary pull allowance should be cleared.
|
|
2024
|
+
/// @param token The token whose temporary allowance should be cleared.
|
|
2025
|
+
function _afterTransferTo(address to, address token) internal view {
|
|
2026
|
+
// Native-token transfers use `msg.value`, so there is no ERC-20 approval to clear.
|
|
2027
|
+
if (token == JBConstants.NATIVE_TOKEN) return;
|
|
2028
|
+
|
|
2029
|
+
// Revert if the callee returned without consuming the full forwarded ERC-20 amount.
|
|
2030
|
+
// slither-disable-next-line calls-loop
|
|
2031
|
+
uint256 allowance = IERC20(token).allowance({owner: address(this), spender: to});
|
|
2032
|
+
if (allowance != 0) revert JBMultiTerminal_TemporaryAllowanceNotConsumed(token, to, allowance);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1981
2035
|
/// @notice Returns a project's accounting context for a token, reverting if it is not accepted.
|
|
1982
2036
|
/// @param projectId The ID of the project to get the accounting context for.
|
|
1983
2037
|
/// @param token The token to get the accounting context for.
|
package/src/JBRulesets.sol
CHANGED
|
@@ -355,7 +355,8 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
355
355
|
/// @dev If a current ruleset of the project is not found, returns an empty ruleset with all properties set to 0.
|
|
356
356
|
/// @dev The first cycle returns the stored ruleset directly (cycleNumber=1, original weight). Subsequent cycles
|
|
357
357
|
/// simulate cycling with weight decay via `_simulateCycledRulesetBasedOn`. Payout limits reset each cycle because
|
|
358
|
-
/// the
|
|
358
|
+
/// they are keyed by `cycleNumber`, but the simulated cycle keeps the same `rulesetId` / config id as the stored
|
|
359
|
+
/// ruleset it is based on.
|
|
359
360
|
/// @param projectId The ID of the project to get the current ruleset of.
|
|
360
361
|
/// @return ruleset The project's current ruleset.
|
|
361
362
|
function currentOf(uint256 projectId) external view override returns (JBRuleset memory ruleset) {
|
package/src/JBTerminalStore.sol
CHANGED
|
@@ -228,12 +228,12 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
228
228
|
|
|
229
229
|
/// @notice Records a cash out from a project.
|
|
230
230
|
/// @dev Cashs out the project's tokens according to values provided by the ruleset's data hook. If the ruleset has
|
|
231
|
-
/// no
|
|
232
|
-
/// data hook, cashs out tokens along a cash out bonding curve that is a function of the number of tokens being
|
|
231
|
+
/// no data hook, cashs out tokens along a cash out bonding curve that is a function of the number of tokens being
|
|
233
232
|
/// burned.
|
|
234
233
|
/// @param holder The account that is cashing out tokens.
|
|
235
234
|
/// @param projectId The ID of the project being cashing out from.
|
|
236
|
-
/// @param cashOutCount The number of project tokens to cash out, as
|
|
235
|
+
/// @param cashOutCount The number of project tokens to cash out, as supplied by the caller and later burned by the
|
|
236
|
+
/// terminal, as a fixed point number with 18 decimals.
|
|
237
237
|
/// @param tokenToReclaim The token being reclaimed by the cash out.
|
|
238
238
|
/// @param beneficiaryIsFeeless Whether the cash out's beneficiary is a feeless address. Passed through to data
|
|
239
239
|
/// hooks so they can skip their own fees when value stays in the protocol (e.g. project-to-project routing).
|
|
@@ -895,18 +895,23 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
895
895
|
JBAccountingContext memory accountingContext = _accountingContextForTokenOf[terminal][projectId][tokenToReclaim];
|
|
896
896
|
|
|
897
897
|
// Get the total number of outstanding project tokens.
|
|
898
|
-
uint256
|
|
898
|
+
uint256 effectiveTotalSupply =
|
|
899
899
|
IJBController(address(DIRECTORY.controllerOf(projectId))).totalTokenSupplyWithReservedTokensOf(projectId);
|
|
900
900
|
|
|
901
901
|
// Can't cash out more tokens than are in the supply.
|
|
902
|
-
if (cashOutCount >
|
|
902
|
+
if (cashOutCount > effectiveTotalSupply) {
|
|
903
|
+
revert JBTerminalStore_InsufficientTokens(cashOutCount, effectiveTotalSupply);
|
|
904
|
+
}
|
|
903
905
|
|
|
904
|
-
// SECURITY NOTE: The data hook has absolute control over cash-out
|
|
905
|
-
// It can set
|
|
906
|
+
// SECURITY NOTE: The data hook has absolute control over cash-out pricing.
|
|
907
|
+
// It can set effectiveTotalSupply, effectiveCashOutCount, and cashOutTaxRate to arbitrary values,
|
|
906
908
|
// completely overriding the terminal's bonding curve math. For example, setting
|
|
907
|
-
//
|
|
909
|
+
// effectiveTotalSupply = surplus makes reclaimAmount = effectiveCashOutCount, bypassing the curve.
|
|
910
|
+
// The terminal still burns the caller-supplied cashOutCount after pricing completes.
|
|
908
911
|
// Project owners MUST audit their data hooks with the same rigor as the terminal.
|
|
909
912
|
|
|
913
|
+
uint256 effectiveCashOutCount = cashOutCount;
|
|
914
|
+
|
|
910
915
|
// If the ruleset has a data hook which is enabled for cash outs, use it to derive a claim amount and memo.
|
|
911
916
|
if (ruleset.useDataHookForCashOut() && ruleset.dataHook() != address(0)) {
|
|
912
917
|
// Build the cash out context field-by-field — the struct has 11 fields, too many for a literal.
|
|
@@ -916,7 +921,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
916
921
|
context.projectId = projectId;
|
|
917
922
|
context.rulesetId = ruleset.id;
|
|
918
923
|
context.cashOutCount = cashOutCount;
|
|
919
|
-
context.totalSupply =
|
|
924
|
+
context.totalSupply = effectiveTotalSupply;
|
|
920
925
|
context.surplus = JBTokenAmount({
|
|
921
926
|
token: accountingContext.token,
|
|
922
927
|
value: surplus,
|
|
@@ -928,7 +933,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
928
933
|
context.beneficiaryIsFeeless = beneficiaryIsFeeless;
|
|
929
934
|
context.metadata = metadata;
|
|
930
935
|
|
|
931
|
-
(cashOutTaxRate,
|
|
936
|
+
(cashOutTaxRate, effectiveCashOutCount, effectiveTotalSupply, hookSpecifications) =
|
|
932
937
|
IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
|
|
933
938
|
|
|
934
939
|
// Noop specifications are informational only, so they can't also request forwarded funds.
|
|
@@ -944,7 +949,10 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
944
949
|
// Apply the bonding curve to calculate how much of the surplus is reclaimable.
|
|
945
950
|
if (surplus != 0) {
|
|
946
951
|
reclaimAmount = JBCashOuts.cashOutFrom({
|
|
947
|
-
surplus: surplus,
|
|
952
|
+
surplus: surplus,
|
|
953
|
+
cashOutCount: effectiveCashOutCount,
|
|
954
|
+
totalSupply: effectiveTotalSupply,
|
|
955
|
+
cashOutTaxRate: cashOutTaxRate
|
|
948
956
|
});
|
|
949
957
|
}
|
|
950
958
|
}
|
|
@@ -1193,6 +1201,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1193
1201
|
});
|
|
1194
1202
|
|
|
1195
1203
|
// Add up all the balances.
|
|
1204
|
+
// slither-disable-next-line calls-loop
|
|
1196
1205
|
surplus = (surplus == 0 || accountingContext.currency == targetCurrency)
|
|
1197
1206
|
? surplus
|
|
1198
1207
|
: mulDiv({
|
|
@@ -1208,6 +1217,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1208
1217
|
});
|
|
1209
1218
|
|
|
1210
1219
|
// Get a reference to the payout limit during the ruleset for the token.
|
|
1220
|
+
// slither-disable-next-line calls-loop
|
|
1211
1221
|
JBCurrencyAmount[] memory payoutLimits = IJBController(address(DIRECTORY.controllerOf(projectId)))
|
|
1212
1222
|
.FUND_ACCESS_LIMITS()
|
|
1213
1223
|
.payoutLimitsOf({
|
|
@@ -1247,6 +1257,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1247
1257
|
|
|
1248
1258
|
// Convert the `payoutLimit`'s amount to be in terms of the provided currency.
|
|
1249
1259
|
if (payoutLimit.amount != 0 && payoutLimit.currency != targetCurrency) {
|
|
1260
|
+
// slither-disable-next-line calls-loop
|
|
1250
1261
|
uint256 converted = mulDiv({
|
|
1251
1262
|
x: payoutLimit.amount,
|
|
1252
1263
|
y: 10 ** _MAX_FIXED_POINT_FIDELITY, // Use `_MAX_FIXED_POINT_FIDELITY` to keep as much of the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {IJBControlled} from "./../interfaces/IJBControlled.sol";
|
|
5
5
|
import {IJBDirectory} from "./../interfaces/IJBDirectory.sol";
|
|
@@ -39,9 +39,15 @@ abstract contract JBControlled is IJBControlled {
|
|
|
39
39
|
DIRECTORY = directory;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
//*********************************************************************//
|
|
43
|
+
// -------------------------- internal views ------------------------- //
|
|
44
|
+
//*********************************************************************//
|
|
45
|
+
|
|
42
46
|
/// @notice Only allows the controller of the specified project to proceed.
|
|
43
47
|
function _onlyControllerOf(uint256 projectId) internal view {
|
|
48
|
+
// slither-disable-next-line calls-loop
|
|
44
49
|
if (address(DIRECTORY.controllerOf(projectId)) != msg.sender) {
|
|
50
|
+
// slither-disable-next-line calls-loop
|
|
45
51
|
revert JBControlled_ControllerUnauthorized(address(DIRECTORY.controllerOf(projectId)));
|
|
46
52
|
}
|
|
47
53
|
}
|
|
@@ -17,8 +17,9 @@ interface IJBRulesetDataHook is IERC165 {
|
|
|
17
17
|
/// @notice Calculates data before a cash out is recorded in the terminal store.
|
|
18
18
|
/// @param context The context passed to this data hook by the `cashOutTokensOf(...)` function.
|
|
19
19
|
/// @return cashOutTaxRate The rate determining the reclaimable amount for a given surplus and token supply.
|
|
20
|
-
/// @return
|
|
21
|
-
///
|
|
20
|
+
/// @return effectiveCashOutCount The effective token count to use for pricing the cash out. The terminal still
|
|
21
|
+
/// burns the caller-supplied token count.
|
|
22
|
+
/// @return effectiveTotalSupply The effective total supply to use for pricing the cash out.
|
|
22
23
|
/// @return hookSpecifications The amount and data to send to cash out hooks instead of returning to the
|
|
23
24
|
/// beneficiary.
|
|
24
25
|
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
@@ -26,8 +27,8 @@ interface IJBRulesetDataHook is IERC165 {
|
|
|
26
27
|
view
|
|
27
28
|
returns (
|
|
28
29
|
uint256 cashOutTaxRate,
|
|
29
|
-
uint256
|
|
30
|
-
uint256
|
|
30
|
+
uint256 effectiveCashOutCount,
|
|
31
|
+
uint256 effectiveTotalSupply,
|
|
31
32
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
32
33
|
);
|
|
33
34
|
|