@bananapus/core-v6 0.0.31 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/JBController.sol +34 -14
- package/src/JBDirectory.sol +25 -13
- package/src/JBFundAccessLimits.sol +28 -7
- package/src/JBMultiTerminal.sol +54 -14
- package/src/JBPermissions.sol +17 -17
- package/src/JBRulesets.sol +95 -64
- package/src/JBSplits.sol +31 -12
- package/src/JBTerminalStore.sol +115 -42
- package/src/JBTokens.sol +5 -2
- package/src/abstract/JBControlled.sol +4 -3
- package/src/libraries/JBMetadataResolver.sol +4 -1
- package/src/libraries/JBPayoutSplitGroupLib.sol +4 -1
- package/src/libraries/JBSurplus.sol +4 -1
- package/test/TestForwardedTokenConsumption.sol +1 -0
- package/test/audit/CrossTerminalSurplusSpoof.t.sol +140 -0
- package/test/units/static/JBController/TestPreviewMintOf.sol +0 -1
- package/test/units/static/JBMultiTerminal/TestCashOutTokensOf.sol +0 -1
package/src/JBTerminalStore.sol
CHANGED
|
@@ -110,6 +110,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
110
110
|
/// @dev Increases as projects use their allowance.
|
|
111
111
|
/// @dev The used surplus allowance is represented as a fixed point number with the same amount of decimals as the
|
|
112
112
|
/// terminal it applies to.
|
|
113
|
+
/// @dev Surplus allowance usage is keyed by `ruleset.id`, not cycle number. Implicit cycle progression
|
|
114
|
+
/// (duration-based auto-cycling) does not reset allowance — this is by design. Projects must queue a new ruleset
|
|
115
|
+
/// to get a fresh allowance.
|
|
113
116
|
/// @custom:param terminal The terminal the surplus allowance applies to.
|
|
114
117
|
/// @custom:param projectId The ID of the project to get the used surplus allowance of.
|
|
115
118
|
/// @custom:param token The token the surplus allowance applies to in the terminal.
|
|
@@ -172,7 +175,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
// Record each accounting context.
|
|
175
|
-
for (uint256 i; i < contexts.length;
|
|
178
|
+
for (uint256 i; i < contexts.length;) {
|
|
176
179
|
JBAccountingContext calldata context = contexts[i];
|
|
177
180
|
|
|
178
181
|
// Make sure the token accounting context isn't already set.
|
|
@@ -213,6 +216,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
213
216
|
|
|
214
217
|
// Add the context to the list.
|
|
215
218
|
_accountingContextsOf[msg.sender][projectId].push(context);
|
|
219
|
+
unchecked {
|
|
220
|
+
++i;
|
|
221
|
+
}
|
|
216
222
|
}
|
|
217
223
|
}
|
|
218
224
|
|
|
@@ -277,26 +283,29 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
277
283
|
|
|
278
284
|
if (hookSpecifications.length != 0) {
|
|
279
285
|
uint256 numberOfSpecifications = hookSpecifications.length;
|
|
280
|
-
for (uint256 i; i < numberOfSpecifications;
|
|
286
|
+
for (uint256 i; i < numberOfSpecifications;) {
|
|
281
287
|
uint256 specificationAmount = hookSpecifications[i].amount;
|
|
282
288
|
if (specificationAmount != 0) {
|
|
283
289
|
balanceDiff += specificationAmount;
|
|
284
290
|
}
|
|
291
|
+
unchecked {
|
|
292
|
+
++i;
|
|
293
|
+
}
|
|
285
294
|
}
|
|
286
295
|
}
|
|
287
296
|
|
|
297
|
+
// Cache the balance slot to avoid redundant storage reads.
|
|
298
|
+
uint256 currentBalance = balanceOf[msg.sender][projectId][tokenToReclaim];
|
|
299
|
+
|
|
288
300
|
// The amount being reclaimed must be within the project's balance.
|
|
289
|
-
if (balanceDiff >
|
|
290
|
-
revert JBTerminalStore_InadequateTerminalStoreBalance(
|
|
291
|
-
balanceDiff, balanceOf[msg.sender][projectId][tokenToReclaim]
|
|
292
|
-
);
|
|
301
|
+
if (balanceDiff > currentBalance) {
|
|
302
|
+
revert JBTerminalStore_InadequateTerminalStoreBalance(balanceDiff, currentBalance);
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
// Remove the reclaimed funds from the project's balance.
|
|
296
306
|
if (balanceDiff != 0) {
|
|
297
307
|
unchecked {
|
|
298
|
-
balanceOf[msg.sender][projectId][tokenToReclaim] =
|
|
299
|
-
balanceOf[msg.sender][projectId][tokenToReclaim] - balanceDiff;
|
|
308
|
+
balanceOf[msg.sender][projectId][tokenToReclaim] = currentBalance - balanceDiff;
|
|
300
309
|
}
|
|
301
310
|
}
|
|
302
311
|
}
|
|
@@ -338,8 +347,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
338
347
|
|
|
339
348
|
// Add the correct balance difference to the token balance of the project.
|
|
340
349
|
if (balanceDiff != 0) {
|
|
341
|
-
|
|
342
|
-
|
|
350
|
+
// Cache the balance slot to avoid redundant storage reads.
|
|
351
|
+
uint256 currentBalance = balanceOf[msg.sender][projectId][amount.token];
|
|
352
|
+
balanceOf[msg.sender][projectId][amount.token] = currentBalance + balanceDiff;
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
355
|
|
|
@@ -385,26 +395,20 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
385
395
|
})
|
|
386
396
|
});
|
|
387
397
|
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
revert JBTerminalStore_InadequateTerminalStoreBalance(
|
|
391
|
-
amountPaidOut, balanceOf[msg.sender][projectId][token]
|
|
392
|
-
);
|
|
393
|
-
}
|
|
398
|
+
// Cache the balance slot to avoid redundant storage reads.
|
|
399
|
+
uint256 currentBalance = balanceOf[msg.sender][projectId][token];
|
|
394
400
|
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
401
|
+
// The amount being paid out must be available.
|
|
402
|
+
if (amountPaidOut > currentBalance) {
|
|
403
|
+
revert JBTerminalStore_InadequateTerminalStoreBalance(amountPaidOut, currentBalance);
|
|
398
404
|
}
|
|
399
405
|
|
|
400
406
|
// The new total amount which has been paid out during this ruleset.
|
|
401
407
|
uint256 newUsedPayoutLimitOf =
|
|
402
408
|
usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] + amount;
|
|
403
409
|
|
|
404
|
-
// Store the new amount.
|
|
405
|
-
usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] = newUsedPayoutLimitOf;
|
|
406
|
-
|
|
407
410
|
// Amount must be within what is still available to pay out.
|
|
411
|
+
// Validate BEFORE writing to storage to avoid wasting gas on SSTORE when the tx will revert.
|
|
408
412
|
uint256 payoutLimit = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
|
|
409
413
|
.payoutLimitOf({
|
|
410
414
|
projectId: projectId, rulesetId: ruleset.id, terminal: msg.sender, token: token, currency: currency
|
|
@@ -414,6 +418,14 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
414
418
|
if (newUsedPayoutLimitOf > payoutLimit || payoutLimit == 0) {
|
|
415
419
|
revert JBTerminalStore_InadequateControllerPayoutLimit(newUsedPayoutLimitOf, payoutLimit);
|
|
416
420
|
}
|
|
421
|
+
|
|
422
|
+
// Removed the paid out funds from the project's token balance.
|
|
423
|
+
unchecked {
|
|
424
|
+
balanceOf[msg.sender][projectId][token] = currentBalance - amountPaidOut;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Store the new used payout limit.
|
|
428
|
+
usedPayoutLimitOf[msg.sender][projectId][token][ruleset.cycleNumber][currency] = newUsedPayoutLimitOf;
|
|
417
429
|
}
|
|
418
430
|
|
|
419
431
|
/// @notice Records the migration of funds from this store.
|
|
@@ -493,17 +505,12 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
493
505
|
// The amount being used must be available in the surplus.
|
|
494
506
|
if (usedAmount > surplus) revert JBTerminalStore_InadequateTerminalStoreBalance(usedAmount, surplus);
|
|
495
507
|
|
|
496
|
-
// Update the project's balance.
|
|
497
|
-
balanceOf[msg.sender][projectId][token] = balanceOf[msg.sender][projectId][token] - usedAmount;
|
|
498
|
-
|
|
499
508
|
// Get a reference to the new used surplus allowance for this ruleset ID.
|
|
500
509
|
uint256 newUsedSurplusAllowanceOf =
|
|
501
510
|
usedSurplusAllowanceOf[msg.sender][projectId][token][ruleset.id][currency] + amount;
|
|
502
511
|
|
|
503
|
-
// Store the incremented value.
|
|
504
|
-
usedSurplusAllowanceOf[msg.sender][projectId][token][ruleset.id][currency] = newUsedSurplusAllowanceOf;
|
|
505
|
-
|
|
506
512
|
// There must be sufficient surplus allowance available.
|
|
513
|
+
// Validate BEFORE writing to storage to avoid wasting gas on SSTORE when the tx will revert.
|
|
507
514
|
uint256 surplusAllowance = IJBController(address(DIRECTORY.controllerOf(projectId))).FUND_ACCESS_LIMITS()
|
|
508
515
|
.surplusAllowanceOf({
|
|
509
516
|
projectId: projectId, rulesetId: ruleset.id, terminal: msg.sender, token: token, currency: currency
|
|
@@ -513,6 +520,15 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
513
520
|
if (newUsedSurplusAllowanceOf > surplusAllowance || surplusAllowance == 0) {
|
|
514
521
|
revert JBTerminalStore_InadequateControllerAllowance(newUsedSurplusAllowanceOf, surplusAllowance);
|
|
515
522
|
}
|
|
523
|
+
|
|
524
|
+
// Cache the balance slot to avoid redundant storage reads.
|
|
525
|
+
uint256 currentBalance = balanceOf[msg.sender][projectId][token];
|
|
526
|
+
|
|
527
|
+
// Update the project's balance.
|
|
528
|
+
balanceOf[msg.sender][projectId][token] = currentBalance - usedAmount;
|
|
529
|
+
|
|
530
|
+
// Store the incremented value.
|
|
531
|
+
usedSurplusAllowanceOf[msg.sender][projectId][token][ruleset.id][currency] = newUsedSurplusAllowanceOf;
|
|
516
532
|
}
|
|
517
533
|
|
|
518
534
|
//*********************************************************************//
|
|
@@ -774,9 +790,17 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
774
790
|
override
|
|
775
791
|
returns (uint256)
|
|
776
792
|
{
|
|
793
|
+
// Fetch the current ruleset once — used for both surplus calculation and cash out tax rate.
|
|
794
|
+
JBRuleset memory ruleset = RULESETS.currentOf(projectId);
|
|
795
|
+
|
|
777
796
|
// Aggregate surplus across the terminals, optionally filtered by the specified tokens.
|
|
778
797
|
uint256 currentSurplus = _currentSurplusOf({
|
|
779
|
-
projectId: projectId,
|
|
798
|
+
projectId: projectId,
|
|
799
|
+
terminals: terminals,
|
|
800
|
+
tokens: tokens,
|
|
801
|
+
decimals: decimals,
|
|
802
|
+
currency: currency,
|
|
803
|
+
ruleset: ruleset
|
|
780
804
|
});
|
|
781
805
|
|
|
782
806
|
// If there's no surplus, nothing can be reclaimed.
|
|
@@ -789,15 +813,12 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
789
813
|
// Can't cash out more tokens than are in the total supply.
|
|
790
814
|
if (cashOutCount > totalSupply) return 0;
|
|
791
815
|
|
|
792
|
-
// Get the cash out tax rate from the current ruleset.
|
|
793
|
-
uint256 cashOutTaxRate = RULESETS.currentOf(projectId).cashOutTaxRate();
|
|
794
|
-
|
|
795
816
|
// Return the amount of surplus terminal tokens that would be reclaimed.
|
|
796
817
|
return JBCashOuts.cashOutFrom({
|
|
797
818
|
surplus: currentSurplus,
|
|
798
819
|
cashOutCount: cashOutCount,
|
|
799
820
|
totalSupply: totalSupply,
|
|
800
|
-
cashOutTaxRate: cashOutTaxRate
|
|
821
|
+
cashOutTaxRate: ruleset.cashOutTaxRate()
|
|
801
822
|
});
|
|
802
823
|
}
|
|
803
824
|
|
|
@@ -806,6 +827,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
806
827
|
//*********************************************************************//
|
|
807
828
|
|
|
808
829
|
/// @notice Computes the surplus relevant for a cash out (total or local, depending on ruleset flag).
|
|
830
|
+
/// @dev When `useTotalSurplusForCashOuts` is enabled, surplus is aggregated from ALL registered terminals without
|
|
831
|
+
/// validation. Projects MUST only register trusted terminals — an untrusted terminal can over-report surplus and
|
|
832
|
+
/// cause the executing terminal to overpay cash-outs.
|
|
809
833
|
/// @param terminal The terminal the cash out is being recorded from.
|
|
810
834
|
/// @param projectId The ID of the project being cashed out from.
|
|
811
835
|
/// @param tokenToReclaim The token being reclaimed.
|
|
@@ -937,10 +961,13 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
937
961
|
IJBRulesetDataHook(ruleset.dataHook()).beforeCashOutRecordedWith(context);
|
|
938
962
|
|
|
939
963
|
// Noop specifications are informational only, so they can't also request forwarded funds.
|
|
940
|
-
for (uint256 i; i < hookSpecifications.length;
|
|
964
|
+
for (uint256 i; i < hookSpecifications.length;) {
|
|
941
965
|
if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
|
|
942
966
|
revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
|
|
943
967
|
}
|
|
968
|
+
unchecked {
|
|
969
|
+
++i;
|
|
970
|
+
}
|
|
944
971
|
}
|
|
945
972
|
} else {
|
|
946
973
|
cashOutTaxRate = ruleset.cashOutTaxRate();
|
|
@@ -1028,7 +1055,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1028
1055
|
balanceDiff = amount.value;
|
|
1029
1056
|
|
|
1030
1057
|
// Ensure that the specifications have valid amounts.
|
|
1031
|
-
for (uint256 i; i < hookSpecifications.length;
|
|
1058
|
+
for (uint256 i; i < hookSpecifications.length;) {
|
|
1032
1059
|
if (hookSpecifications[i].noop && hookSpecifications[i].amount != 0) {
|
|
1033
1060
|
revert JBTerminalStore_NoopHookSpecHasAmount(hookSpecifications[i].amount);
|
|
1034
1061
|
}
|
|
@@ -1044,6 +1071,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1044
1071
|
// Decrement the total amount being added to the local balance.
|
|
1045
1072
|
balanceDiff -= specifiedAmount;
|
|
1046
1073
|
}
|
|
1074
|
+
unchecked {
|
|
1075
|
+
++i;
|
|
1076
|
+
}
|
|
1047
1077
|
}
|
|
1048
1078
|
|
|
1049
1079
|
// If there's no amount being recorded, there's nothing left to do.
|
|
@@ -1086,15 +1116,46 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1086
1116
|
internal
|
|
1087
1117
|
view
|
|
1088
1118
|
returns (uint256 surplus)
|
|
1119
|
+
{
|
|
1120
|
+
// Fetch the ruleset once and delegate to the overload that accepts it.
|
|
1121
|
+
return _currentSurplusOf({
|
|
1122
|
+
projectId: projectId,
|
|
1123
|
+
terminals: terminals,
|
|
1124
|
+
tokens: tokens,
|
|
1125
|
+
decimals: decimals,
|
|
1126
|
+
currency: currency,
|
|
1127
|
+
ruleset: RULESETS.currentOf(projectId)
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/// @notice Gets the current surplus amount for a project across specified terminals and tokens, using a
|
|
1132
|
+
/// pre-fetched ruleset.
|
|
1133
|
+
/// @dev Use this overload when the caller already has the current ruleset to avoid a redundant
|
|
1134
|
+
/// `RULESETS.currentOf()` call.
|
|
1135
|
+
/// @param projectId The ID of the project to get surplus for.
|
|
1136
|
+
/// @param terminals The terminals to include. If empty, all project terminals are used.
|
|
1137
|
+
/// @param tokens The tokens to include. If empty, all tokens per terminal are used.
|
|
1138
|
+
/// @param decimals The number of decimals to expect in the resulting fixed point number.
|
|
1139
|
+
/// @param currency The currency the resulting amount should be in terms of.
|
|
1140
|
+
/// @param ruleset The project's current ruleset.
|
|
1141
|
+
/// @return surplus The current surplus amount.
|
|
1142
|
+
function _currentSurplusOf(
|
|
1143
|
+
uint256 projectId,
|
|
1144
|
+
IJBTerminal[] memory terminals,
|
|
1145
|
+
address[] memory tokens,
|
|
1146
|
+
uint256 decimals,
|
|
1147
|
+
uint256 currency,
|
|
1148
|
+
JBRuleset memory ruleset
|
|
1149
|
+
)
|
|
1150
|
+
internal
|
|
1151
|
+
view
|
|
1152
|
+
returns (uint256 surplus)
|
|
1089
1153
|
{
|
|
1090
1154
|
// If specific terminals were provided, use them. Otherwise, get all terminals from the directory.
|
|
1091
1155
|
IJBTerminal[] memory resolvedTerminals = terminals.length != 0 ? terminals : DIRECTORY.terminalsOf(projectId);
|
|
1092
1156
|
|
|
1093
|
-
// The ruleset determines payout limits, which affect surplus. Fetch it once for all terminals.
|
|
1094
|
-
JBRuleset memory ruleset = RULESETS.currentOf(projectId);
|
|
1095
|
-
|
|
1096
1157
|
// Sum surplus across each terminal.
|
|
1097
|
-
for (uint256 i; i < resolvedTerminals.length;
|
|
1158
|
+
for (uint256 i; i < resolvedTerminals.length;) {
|
|
1098
1159
|
address terminal = address(resolvedTerminals[i]);
|
|
1099
1160
|
|
|
1100
1161
|
// Build the list of accounting contexts to include in this terminal's surplus calculation.
|
|
@@ -1102,8 +1163,11 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1102
1163
|
if (tokens.length != 0) {
|
|
1103
1164
|
// Specific tokens requested: look up each token's accounting context at this terminal.
|
|
1104
1165
|
accountingContexts = new JBAccountingContext[](tokens.length);
|
|
1105
|
-
for (uint256 j; j < tokens.length;
|
|
1166
|
+
for (uint256 j; j < tokens.length;) {
|
|
1106
1167
|
accountingContexts[j] = _accountingContextForTokenOf[terminal][projectId][tokens[j]];
|
|
1168
|
+
unchecked {
|
|
1169
|
+
++j;
|
|
1170
|
+
}
|
|
1107
1171
|
}
|
|
1108
1172
|
} else {
|
|
1109
1173
|
// No token filter: use all accounting contexts registered at this terminal.
|
|
@@ -1119,6 +1183,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1119
1183
|
targetDecimals: decimals,
|
|
1120
1184
|
targetCurrency: currency
|
|
1121
1185
|
});
|
|
1186
|
+
unchecked {
|
|
1187
|
+
++i;
|
|
1188
|
+
}
|
|
1122
1189
|
}
|
|
1123
1190
|
}
|
|
1124
1191
|
|
|
@@ -1151,7 +1218,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1151
1218
|
uint256 numberOfTokenAccountingContexts = accountingContexts.length;
|
|
1152
1219
|
|
|
1153
1220
|
// Add payout limits from each token.
|
|
1154
|
-
for (uint256 i; i < numberOfTokenAccountingContexts;
|
|
1221
|
+
for (uint256 i; i < numberOfTokenAccountingContexts;) {
|
|
1155
1222
|
uint256 tokenSurplus = _tokenSurplusFrom({
|
|
1156
1223
|
terminal: terminal,
|
|
1157
1224
|
projectId: projectId,
|
|
@@ -1162,6 +1229,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1162
1229
|
});
|
|
1163
1230
|
// Increment the surplus with any remaining balance.
|
|
1164
1231
|
if (tokenSurplus > 0) surplus += tokenSurplus;
|
|
1232
|
+
unchecked {
|
|
1233
|
+
++i;
|
|
1234
|
+
}
|
|
1165
1235
|
}
|
|
1166
1236
|
}
|
|
1167
1237
|
|
|
@@ -1228,7 +1298,7 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1228
1298
|
uint256 numberOfPayoutLimits = payoutLimits.length;
|
|
1229
1299
|
|
|
1230
1300
|
// Loop through each payout limit to determine the cumulative normalized payout limit remaining.
|
|
1231
|
-
for (uint256 i; i < numberOfPayoutLimits;
|
|
1301
|
+
for (uint256 i; i < numberOfPayoutLimits;) {
|
|
1232
1302
|
JBCurrencyAmount memory payoutLimit = payoutLimits[i];
|
|
1233
1303
|
|
|
1234
1304
|
// Set the payout limit value to the amount still available to pay out during the ruleset.
|
|
@@ -1280,6 +1350,9 @@ contract JBTerminalStore is IJBTerminalStore {
|
|
|
1280
1350
|
} else {
|
|
1281
1351
|
return 0;
|
|
1282
1352
|
}
|
|
1353
|
+
unchecked {
|
|
1354
|
+
++i;
|
|
1355
|
+
}
|
|
1283
1356
|
}
|
|
1284
1357
|
}
|
|
1285
1358
|
}
|
package/src/JBTokens.sol
CHANGED
|
@@ -253,9 +253,12 @@ contract JBTokens is JBControlled, IJBTokens {
|
|
|
253
253
|
// Save a reference to whether there a token exists.
|
|
254
254
|
bool tokensWereClaimed = token != IJBToken(address(0));
|
|
255
255
|
|
|
256
|
+
// Cache the total supply to avoid a redundant external call on revert.
|
|
257
|
+
uint256 supply = totalSupplyOf(projectId);
|
|
258
|
+
|
|
256
259
|
// The total supply after minting can't exceed the maximum value storable in a uint208.
|
|
257
|
-
if (
|
|
258
|
-
revert JBTokens_OverflowAlert(
|
|
260
|
+
if (supply + count > type(uint208).max) {
|
|
261
|
+
revert JBTokens_OverflowAlert(supply + count, type(uint208).max);
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
if (tokensWereClaimed) {
|
|
@@ -45,10 +45,11 @@ abstract contract JBControlled is IJBControlled {
|
|
|
45
45
|
|
|
46
46
|
/// @notice Only allows the controller of the specified project to proceed.
|
|
47
47
|
function _onlyControllerOf(uint256 projectId) internal view {
|
|
48
|
+
// Cache the controller address to avoid a redundant external call on revert.
|
|
48
49
|
// slither-disable-next-line calls-loop
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
revert JBControlled_ControllerUnauthorized(
|
|
50
|
+
address controller = address(DIRECTORY.controllerOf(projectId));
|
|
51
|
+
if (controller != msg.sender) {
|
|
52
|
+
revert JBControlled_ControllerUnauthorized(controller);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
}
|
|
@@ -214,7 +214,7 @@ library JBMetadataResolver {
|
|
|
214
214
|
uint256 numberOfDatas = datas.length;
|
|
215
215
|
|
|
216
216
|
// Add each metadata to the array, each padded to 32 bytes
|
|
217
|
-
for (uint256 i; i < numberOfDatas;
|
|
217
|
+
for (uint256 i; i < numberOfDatas;) {
|
|
218
218
|
metadata = abi.encodePacked(metadata, datas[i]);
|
|
219
219
|
paddedLength = metadata.length % JBMetadataResolver.WORD_SIZE == 0
|
|
220
220
|
? metadata.length
|
|
@@ -223,6 +223,9 @@ library JBMetadataResolver {
|
|
|
223
223
|
assembly ("memory-safe") {
|
|
224
224
|
mstore(metadata, paddedLength)
|
|
225
225
|
}
|
|
226
|
+
unchecked {
|
|
227
|
+
++i;
|
|
228
|
+
}
|
|
226
229
|
}
|
|
227
230
|
}
|
|
228
231
|
|
|
@@ -69,7 +69,7 @@ library JBPayoutSplitGroupLib {
|
|
|
69
69
|
leftoverAmount = amount;
|
|
70
70
|
|
|
71
71
|
// Transfer between all splits.
|
|
72
|
-
for (uint256 i; i < payoutSplits.length;
|
|
72
|
+
for (uint256 i; i < payoutSplits.length;) {
|
|
73
73
|
// Get a reference to the split being iterated on.
|
|
74
74
|
JBSplit memory split = payoutSplits[i];
|
|
75
75
|
|
|
@@ -108,6 +108,9 @@ library JBPayoutSplitGroupLib {
|
|
|
108
108
|
netAmount: netPayoutAmount,
|
|
109
109
|
caller: caller
|
|
110
110
|
});
|
|
111
|
+
unchecked {
|
|
112
|
+
++i;
|
|
113
|
+
}
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
|
|
@@ -30,10 +30,13 @@ library JBSurplus {
|
|
|
30
30
|
uint256 numberOfTerminals = terminals.length;
|
|
31
31
|
|
|
32
32
|
// Add the current surplus for each terminal.
|
|
33
|
-
for (uint256 i; i < numberOfTerminals;
|
|
33
|
+
for (uint256 i; i < numberOfTerminals;) {
|
|
34
34
|
surplus += terminals[i].currentSurplusOf({
|
|
35
35
|
projectId: projectId, tokens: tokens, decimals: decimals, currency: currency
|
|
36
36
|
});
|
|
37
|
+
unchecked {
|
|
38
|
+
++i;
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -386,6 +386,7 @@ contract TestForwardedTokenConsumption_Local is TestBaseWorkflow {
|
|
|
386
386
|
returns (JBFundAccessLimitGroup[] memory)
|
|
387
387
|
{
|
|
388
388
|
JBCurrencyAmount[] memory payoutLimits = new JBCurrencyAmount[](1);
|
|
389
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
389
390
|
payoutLimits[0] =
|
|
390
391
|
JBCurrencyAmount({amount: uint224(payoutAmount), currency: uint32(uint160(address(usdcToken())))});
|
|
391
392
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.6;
|
|
3
|
+
|
|
4
|
+
import {TestBaseWorkflow} from "../helpers/TestBaseWorkflow.sol";
|
|
5
|
+
import {IJBController} from "../../src/interfaces/IJBController.sol";
|
|
6
|
+
import {IJBDirectory} from "../../src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBTerminal} from "../../src/interfaces/IJBTerminal.sol";
|
|
8
|
+
import {JBMultiTerminal} from "../../src/JBMultiTerminal.sol";
|
|
9
|
+
import {JBConstants} from "../../src/libraries/JBConstants.sol";
|
|
10
|
+
import {JBAccountingContext} from "../../src/structs/JBAccountingContext.sol";
|
|
11
|
+
import {JBFundAccessLimitGroup} from "../../src/structs/JBFundAccessLimitGroup.sol";
|
|
12
|
+
import {JBRulesetConfig} from "../../src/structs/JBRulesetConfig.sol";
|
|
13
|
+
import {JBRulesetMetadata} from "../../src/structs/JBRulesetMetadata.sol";
|
|
14
|
+
import {JBSplitGroup} from "../../src/structs/JBSplitGroup.sol";
|
|
15
|
+
import {JBTerminalConfig} from "../../src/structs/JBTerminalConfig.sol";
|
|
16
|
+
import {IJBRulesetApprovalHook} from "../../src/interfaces/IJBRulesetApprovalHook.sol";
|
|
17
|
+
|
|
18
|
+
/// @notice Verifies that `useTotalSurplusForCashOuts` trusts every terminal in the directory,
|
|
19
|
+
/// even though settlement still comes from the specific terminal the holder cashes out through.
|
|
20
|
+
contract CrossTerminalSurplusSpoof_Local is TestBaseWorkflow {
|
|
21
|
+
IJBController private _controller;
|
|
22
|
+
IJBDirectory private _directory;
|
|
23
|
+
JBMultiTerminal private _terminal;
|
|
24
|
+
address private _projectOwner;
|
|
25
|
+
address private _holder;
|
|
26
|
+
uint32 private _nativeCurrency;
|
|
27
|
+
uint256 private _projectId;
|
|
28
|
+
|
|
29
|
+
function setUp() public override {
|
|
30
|
+
super.setUp();
|
|
31
|
+
|
|
32
|
+
_controller = jbController();
|
|
33
|
+
_directory = jbDirectory();
|
|
34
|
+
_terminal = jbMultiTerminal();
|
|
35
|
+
_projectOwner = multisig();
|
|
36
|
+
_holder = beneficiary();
|
|
37
|
+
_nativeCurrency = uint32(uint160(JBConstants.NATIVE_TOKEN));
|
|
38
|
+
|
|
39
|
+
JBRulesetMetadata memory metadata = JBRulesetMetadata({
|
|
40
|
+
reservedPercent: 0,
|
|
41
|
+
cashOutTaxRate: 0,
|
|
42
|
+
baseCurrency: _nativeCurrency,
|
|
43
|
+
pausePay: false,
|
|
44
|
+
pauseCreditTransfers: false,
|
|
45
|
+
allowOwnerMinting: true,
|
|
46
|
+
allowSetCustomToken: true,
|
|
47
|
+
allowTerminalMigration: false,
|
|
48
|
+
allowSetTerminals: true,
|
|
49
|
+
allowSetController: false,
|
|
50
|
+
allowAddAccountingContext: true,
|
|
51
|
+
allowAddPriceFeed: false,
|
|
52
|
+
ownerMustSendPayouts: false,
|
|
53
|
+
holdFees: false,
|
|
54
|
+
useTotalSurplusForCashOuts: true,
|
|
55
|
+
useDataHookForPay: false,
|
|
56
|
+
useDataHookForCashOut: false,
|
|
57
|
+
dataHook: address(0),
|
|
58
|
+
metadata: 0
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
62
|
+
rulesetConfigs[0] = JBRulesetConfig({
|
|
63
|
+
mustStartAtOrAfter: 0,
|
|
64
|
+
duration: 0,
|
|
65
|
+
weight: 1000e18,
|
|
66
|
+
weightCutPercent: 0,
|
|
67
|
+
approvalHook: IJBRulesetApprovalHook(address(0)),
|
|
68
|
+
metadata: metadata,
|
|
69
|
+
splitGroups: new JBSplitGroup[](0),
|
|
70
|
+
fundAccessLimitGroups: new JBFundAccessLimitGroup[](0)
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
|
|
74
|
+
contexts[0] = JBAccountingContext({token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: _nativeCurrency});
|
|
75
|
+
|
|
76
|
+
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
77
|
+
terminalConfigs[0] = JBTerminalConfig({terminal: _terminal, accountingContextsToAccept: contexts});
|
|
78
|
+
|
|
79
|
+
_projectId = _controller.launchProjectFor({
|
|
80
|
+
owner: _projectOwner,
|
|
81
|
+
projectUri: "spoofed-surplus",
|
|
82
|
+
rulesetConfigurations: rulesetConfigs,
|
|
83
|
+
terminalConfigurations: terminalConfigs,
|
|
84
|
+
memo: ""
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function test_partialCashOutCanDrainLocalTerminalUsingSpoofedSiblingSurplus() external {
|
|
89
|
+
vm.deal(_holder, 1 ether);
|
|
90
|
+
|
|
91
|
+
vm.prank(_holder);
|
|
92
|
+
uint256 minted = _terminal.pay{value: 1 ether}({
|
|
93
|
+
projectId: _projectId,
|
|
94
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
95
|
+
amount: 1 ether,
|
|
96
|
+
beneficiary: _holder,
|
|
97
|
+
minReturnedTokens: 0,
|
|
98
|
+
memo: "",
|
|
99
|
+
metadata: ""
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assertEq(address(_terminal).balance, 1 ether, "terminal should hold the paid ETH");
|
|
103
|
+
|
|
104
|
+
address spoofedTerminal = makeAddr("spoofedTerminal");
|
|
105
|
+
IJBTerminal[] memory terminals = new IJBTerminal[](2);
|
|
106
|
+
terminals[0] = IJBTerminal(address(_terminal));
|
|
107
|
+
terminals[1] = IJBTerminal(spoofedTerminal);
|
|
108
|
+
|
|
109
|
+
vm.prank(_projectOwner);
|
|
110
|
+
_directory.setTerminalsOf(_projectId, terminals);
|
|
111
|
+
|
|
112
|
+
vm.mockCall(
|
|
113
|
+
spoofedTerminal,
|
|
114
|
+
abi.encodeCall(IJBTerminal.currentSurplusOf, (_projectId, new address[](0), 18, _nativeCurrency)),
|
|
115
|
+
abi.encode(1 ether)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
uint256 holderBalanceBefore = _holder.balance;
|
|
119
|
+
|
|
120
|
+
vm.prank(_holder);
|
|
121
|
+
uint256 reclaimed = _terminal.cashOutTokensOf({
|
|
122
|
+
holder: _holder,
|
|
123
|
+
projectId: _projectId,
|
|
124
|
+
cashOutCount: minted / 2,
|
|
125
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
126
|
+
minTokensReclaimed: 0,
|
|
127
|
+
beneficiary: payable(_holder),
|
|
128
|
+
metadata: new bytes(0)
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
assertEq(reclaimed, 1 ether, "spoofed global surplus should let a half burn reclaim the full local balance");
|
|
132
|
+
assertEq(_holder.balance - holderBalanceBefore, 1 ether, "holder should receive the full terminal balance");
|
|
133
|
+
assertEq(address(_terminal).balance, 0, "the honest terminal should be fully drained");
|
|
134
|
+
assertEq(
|
|
135
|
+
jbTokens().totalBalanceOf(_holder, _projectId),
|
|
136
|
+
minted / 2,
|
|
137
|
+
"the holder should keep half their project tokens after draining the terminal"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
|
-
import {JBController} from "../../../../src/JBController.sol";
|
|
5
4
|
import {IJBRulesetApprovalHook} from "../../../../src/interfaces/IJBRulesetApprovalHook.sol";
|
|
6
5
|
import {IJBRulesets} from "../../../../src/interfaces/IJBRulesets.sol";
|
|
7
6
|
import {JBConstants} from "../../../../src/libraries/JBConstants.sol";
|
|
@@ -5,7 +5,6 @@ import {MockERC20} from "../../../mock/MockERC20.sol";
|
|
|
5
5
|
import {JBMultiTerminal} from "../../../../src/JBMultiTerminal.sol";
|
|
6
6
|
import {JBPermissioned} from "../../../../src/abstract/JBPermissioned.sol";
|
|
7
7
|
import {IJBCashOutHook} from "../../../../src/interfaces/IJBCashOutHook.sol";
|
|
8
|
-
import {IJBCashOutTerminal} from "../../../../src/interfaces/IJBCashOutTerminal.sol";
|
|
9
8
|
import {IJBController} from "../../../../src/interfaces/IJBController.sol";
|
|
10
9
|
import {IJBDirectory} from "../../../../src/interfaces/IJBDirectory.sol";
|
|
11
10
|
import {IJBFeelessAddresses} from "../../../../src/interfaces/IJBFeelessAddresses.sol";
|