@bananapus/core-v6 0.0.31 → 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/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 +80 -22
- 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/JBRulesets.sol
CHANGED
|
@@ -672,17 +672,19 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
672
672
|
revert JBRulesets_WeightCacheRequired(projectId);
|
|
673
673
|
}
|
|
674
674
|
|
|
675
|
-
|
|
676
|
-
|
|
675
|
+
// Cache the cut factor and max percent to avoid recomputing each iteration.
|
|
676
|
+
uint256 cutFactor = JBConstants.MAX_WEIGHT_CUT_PERCENT - baseRulesetWeightCutPercent;
|
|
677
|
+
uint256 maxPercent = JBConstants.MAX_WEIGHT_CUT_PERCENT;
|
|
678
|
+
|
|
679
|
+
for (uint256 i; i < weightCutMultiple;) {
|
|
677
680
|
// Base the new weight on the specified ruleset's weight.
|
|
678
|
-
weight = mulDiv(
|
|
679
|
-
weight,
|
|
680
|
-
JBConstants.MAX_WEIGHT_CUT_PERCENT - baseRulesetWeightCutPercent,
|
|
681
|
-
JBConstants.MAX_WEIGHT_CUT_PERCENT
|
|
682
|
-
);
|
|
681
|
+
weight = mulDiv(weight, cutFactor, maxPercent);
|
|
683
682
|
|
|
684
683
|
// The calculation doesn't need to continue if the weight is 0.
|
|
685
684
|
if (weight == 0) break;
|
|
685
|
+
unchecked {
|
|
686
|
+
++i;
|
|
687
|
+
}
|
|
686
688
|
}
|
|
687
689
|
}
|
|
688
690
|
|
|
@@ -721,6 +723,7 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
721
723
|
}
|
|
722
724
|
|
|
723
725
|
// Get a reference to the latest ruleset's struct.
|
|
726
|
+
// Note: full metadata is loaded because `_approvalStatusOf` forwards the struct to external approval hooks.
|
|
724
727
|
JBRuleset memory baseRuleset = _getStructFor({projectId: projectId, rulesetId: latestId});
|
|
725
728
|
|
|
726
729
|
// Get a reference to the approval status.
|
|
@@ -743,7 +746,9 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
743
746
|
&& approvalStatus != JBApprovalStatus.ApprovalExpected
|
|
744
747
|
&& approvalStatus != JBApprovalStatus.Empty)
|
|
745
748
|
) {
|
|
746
|
-
|
|
749
|
+
// Metadata not needed — the fallback ruleset is only used for intrinsic fields (start, basedOnId, etc.)
|
|
750
|
+
// and not forwarded to any external approval hook.
|
|
751
|
+
baseRuleset = _getStructWithoutMetadataFor({projectId: projectId, rulesetId: baseRuleset.basedOnId});
|
|
747
752
|
}
|
|
748
753
|
|
|
749
754
|
// Make sure the ruleset starts after the base ruleset.
|
|
@@ -896,11 +901,14 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
896
901
|
// slither-disable-next-line incorrect-equality
|
|
897
902
|
if (ruleset.basedOnId == 0) return JBApprovalStatus.Empty;
|
|
898
903
|
|
|
899
|
-
//
|
|
900
|
-
|
|
904
|
+
// Read only the packed user properties to extract the approval hook address,
|
|
905
|
+
// avoiding the cost of loading the full parent ruleset struct.
|
|
906
|
+
uint256 packedUserProperties = _packedUserPropertiesOf[projectId][ruleset.basedOnId];
|
|
907
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
908
|
+
IJBRulesetApprovalHook approvalHook = IJBRulesetApprovalHook(address(uint160(packedUserProperties)));
|
|
901
909
|
|
|
902
910
|
// If there is no approval hook, it's considered empty.
|
|
903
|
-
if (
|
|
911
|
+
if (approvalHook == IJBRulesetApprovalHook(address(0))) {
|
|
904
912
|
return JBApprovalStatus.Empty;
|
|
905
913
|
}
|
|
906
914
|
|
|
@@ -909,9 +917,7 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
909
917
|
// Note: A malicious hook that consumes all gas (e.g. infinite loop) could still DoS via gas exhaustion.
|
|
910
918
|
// This is accepted risk since the project owner chose their own approval hook.
|
|
911
919
|
// slither-disable-next-line calls-loop
|
|
912
|
-
try
|
|
913
|
-
JBApprovalStatus status
|
|
914
|
-
) {
|
|
920
|
+
try approvalHook.approvalStatusOf({projectId: projectId, ruleset: ruleset}) returns (JBApprovalStatus status) {
|
|
915
921
|
return status;
|
|
916
922
|
} catch {
|
|
917
923
|
return JBApprovalStatus.Failed;
|
|
@@ -928,8 +934,8 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
928
934
|
// Get a reference to the project's latest ruleset.
|
|
929
935
|
uint256 rulesetId = latestRulesetIdOf[projectId];
|
|
930
936
|
|
|
931
|
-
// Get the struct for the latest ruleset.
|
|
932
|
-
JBRuleset memory ruleset =
|
|
937
|
+
// Get the struct for the latest ruleset (metadata not needed — only traversal fields are checked).
|
|
938
|
+
JBRuleset memory ruleset = _getStructWithoutMetadataFor({projectId: projectId, rulesetId: rulesetId});
|
|
933
939
|
|
|
934
940
|
// Loop through all most recently queued rulesets until an approvable one is found, or we've proven one can't
|
|
935
941
|
// exist.
|
|
@@ -945,12 +951,64 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
945
951
|
return ruleset.id;
|
|
946
952
|
}
|
|
947
953
|
|
|
948
|
-
ruleset =
|
|
954
|
+
ruleset = _getStructWithoutMetadataFor({projectId: projectId, rulesetId: ruleset.basedOnId});
|
|
949
955
|
} while (ruleset.cycleNumber != 0);
|
|
950
956
|
|
|
951
957
|
return 0;
|
|
952
958
|
}
|
|
953
959
|
|
|
960
|
+
/// @notice Unpack a ruleset's intrinsic and user properties without loading metadata.
|
|
961
|
+
/// @dev Saves one cold SLOAD (~2,100 gas) compared to `_getStructFor`. Use this for linked-list traversal where
|
|
962
|
+
/// only `id`, `start`, `duration`, `basedOnId`, `cycleNumber`, `weight`, `weightCutPercent`, and `approvalHook`
|
|
963
|
+
/// are needed.
|
|
964
|
+
/// @param projectId The ID of the project the ruleset belongs to.
|
|
965
|
+
/// @param rulesetId The ID of the ruleset to get the struct for.
|
|
966
|
+
/// @return ruleset A ruleset struct with `metadata` set to 0.
|
|
967
|
+
function _getStructWithoutMetadataFor(
|
|
968
|
+
uint256 projectId,
|
|
969
|
+
uint256 rulesetId
|
|
970
|
+
)
|
|
971
|
+
internal
|
|
972
|
+
view
|
|
973
|
+
returns (JBRuleset memory ruleset)
|
|
974
|
+
{
|
|
975
|
+
// Return an empty ruleset if the specified `rulesetId` is 0.
|
|
976
|
+
// slither-disable-next-line incorrect-equality
|
|
977
|
+
if (rulesetId == 0) return ruleset;
|
|
978
|
+
|
|
979
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
980
|
+
ruleset.id = uint48(rulesetId);
|
|
981
|
+
|
|
982
|
+
uint256 packedIntrinsicProperties = _packedIntrinsicPropertiesOf[projectId][rulesetId];
|
|
983
|
+
|
|
984
|
+
// `weight` in bits 0-111 bits.
|
|
985
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
986
|
+
ruleset.weight = uint112(packedIntrinsicProperties);
|
|
987
|
+
// `basedOnId` in bits 112-159 bits.
|
|
988
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
989
|
+
ruleset.basedOnId = uint48(packedIntrinsicProperties >> 112);
|
|
990
|
+
// `start` in bits 160-207 bits.
|
|
991
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
992
|
+
ruleset.start = uint48(packedIntrinsicProperties >> 160);
|
|
993
|
+
// `cycleNumber` in bits 208-255 bits.
|
|
994
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
995
|
+
ruleset.cycleNumber = uint48(packedIntrinsicProperties >> 208);
|
|
996
|
+
|
|
997
|
+
uint256 packedUserProperties = _packedUserPropertiesOf[projectId][rulesetId];
|
|
998
|
+
|
|
999
|
+
// approval hook in bits 0-159 bits.
|
|
1000
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1001
|
+
ruleset.approvalHook = IJBRulesetApprovalHook(address(uint160(packedUserProperties)));
|
|
1002
|
+
// `duration` in bits 160-191 bits.
|
|
1003
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1004
|
+
ruleset.duration = uint32(packedUserProperties >> 160);
|
|
1005
|
+
// weight cut percent in bits 192-223 bits.
|
|
1006
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1007
|
+
ruleset.weightCutPercent = uint32(packedUserProperties >> 192);
|
|
1008
|
+
|
|
1009
|
+
// metadata intentionally not loaded — saves one cold SLOAD (~2,100 gas).
|
|
1010
|
+
}
|
|
1011
|
+
|
|
954
1012
|
/// @notice Unpack a ruleset's packed stored values into an easy-to-work-with ruleset struct.
|
|
955
1013
|
/// @param projectId The ID of the project the ruleset belongs to.
|
|
956
1014
|
/// @param rulesetId The ID of the ruleset to get the full struct for.
|
|
@@ -1067,8 +1125,8 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
1067
1125
|
// Get a reference to the ID of the project's latest ruleset.
|
|
1068
1126
|
rulesetId = latestRulesetIdOf[projectId];
|
|
1069
1127
|
|
|
1070
|
-
// Get the struct for the latest ruleset.
|
|
1071
|
-
JBRuleset memory ruleset =
|
|
1128
|
+
// Get the struct for the latest ruleset (metadata not needed — only traversal fields are checked).
|
|
1129
|
+
JBRuleset memory ruleset = _getStructWithoutMetadataFor({projectId: projectId, rulesetId: rulesetId});
|
|
1072
1130
|
|
|
1073
1131
|
// There is no upcoming ruleset if the latest ruleset has already started.
|
|
1074
1132
|
// slither-disable-next-line incorrect-equality
|
|
@@ -1086,7 +1144,7 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
1086
1144
|
|
|
1087
1145
|
// Find the base ruleset that is not still queued.
|
|
1088
1146
|
while (true) {
|
|
1089
|
-
baseRuleset =
|
|
1147
|
+
baseRuleset = _getStructWithoutMetadataFor({projectId: projectId, rulesetId: basedOnId});
|
|
1090
1148
|
|
|
1091
1149
|
// If the base ruleset starts in the future,
|
|
1092
1150
|
if (block.timestamp < baseRuleset.start) {
|
|
@@ -1100,8 +1158,8 @@ contract JBRulesets is JBControlled, IJBRulesets {
|
|
|
1100
1158
|
}
|
|
1101
1159
|
}
|
|
1102
1160
|
|
|
1103
|
-
// Get the ruleset struct for the ID found.
|
|
1104
|
-
ruleset =
|
|
1161
|
+
// Get the ruleset struct for the ID found (metadata not needed — only `start` and `duration` are checked).
|
|
1162
|
+
ruleset = _getStructWithoutMetadataFor({projectId: projectId, rulesetId: rulesetId});
|
|
1105
1163
|
|
|
1106
1164
|
// If the latest ruleset doesn't start until after another base ruleset return 0.
|
|
1107
1165
|
if (baseRuleset.duration != 0 && block.timestamp < ruleset.start - baseRuleset.duration) {
|
package/src/JBSplits.sol
CHANGED
|
@@ -97,15 +97,13 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
97
97
|
// Cache whether the controller check has already passed to avoid repeated external calls.
|
|
98
98
|
bool controllerChecked;
|
|
99
99
|
|
|
100
|
-
// Set each grouped splits.
|
|
101
|
-
for (uint256 i; i < splitGroups.length;
|
|
102
|
-
// Get a reference to the grouped split being iterated on.
|
|
103
|
-
JBSplitGroup memory splitGroup = splitGroups[i];
|
|
104
|
-
|
|
100
|
+
// Set each grouped splits. Access calldata directly to avoid copying each split group to memory.
|
|
101
|
+
for (uint256 i; i < splitGroups.length;) {
|
|
105
102
|
// Self-auth: lower 160 bits must match msg.sender AND upper 96 bits must be non-zero.
|
|
106
103
|
// GroupIds with zero upper bits are reserved for protocol use (e.g. terminal payout groups)
|
|
107
104
|
// and always require controller authorization to prevent token contracts from hijacking payouts.
|
|
108
|
-
bool isSelfManaged =
|
|
105
|
+
bool isSelfManaged =
|
|
106
|
+
splitGroups[i].groupId >> 160 != 0 && address(uint160(splitGroups[i].groupId)) == msg.sender;
|
|
109
107
|
|
|
110
108
|
if (!isSelfManaged && !controllerChecked) {
|
|
111
109
|
_onlyControllerOf(projectId);
|
|
@@ -114,8 +112,14 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
114
112
|
|
|
115
113
|
// Set the splits for the group.
|
|
116
114
|
_setSplitsOf({
|
|
117
|
-
projectId: projectId,
|
|
115
|
+
projectId: projectId,
|
|
116
|
+
rulesetId: rulesetId,
|
|
117
|
+
groupId: splitGroups[i].groupId,
|
|
118
|
+
splits: splitGroups[i].splits
|
|
118
119
|
});
|
|
120
|
+
unchecked {
|
|
121
|
+
++i;
|
|
122
|
+
}
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -168,7 +172,7 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
168
172
|
uint256 numberOfCurrentSplits = currentSplits.length;
|
|
169
173
|
|
|
170
174
|
// Check to see if all locked splits are included in the array of splits which is being set.
|
|
171
|
-
for (uint256 i; i < numberOfCurrentSplits;
|
|
175
|
+
for (uint256 i; i < numberOfCurrentSplits;) {
|
|
172
176
|
// If not locked, continue.
|
|
173
177
|
if (
|
|
174
178
|
block.timestamp < currentSplits[i].lockedUntil
|
|
@@ -176,6 +180,9 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
176
180
|
) {
|
|
177
181
|
revert JBSplits_PreviousLockedSplitsNotIncluded(projectId, rulesetId);
|
|
178
182
|
}
|
|
183
|
+
unchecked {
|
|
184
|
+
++i;
|
|
185
|
+
}
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
// Add up all the `percent`s to make sure their total is under 100%.
|
|
@@ -184,7 +191,7 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
184
191
|
// Keep a reference to the number of splits to set.
|
|
185
192
|
uint256 numberOfSplits = splits.length;
|
|
186
193
|
|
|
187
|
-
for (uint256 i; i < numberOfSplits;
|
|
194
|
+
for (uint256 i; i < numberOfSplits;) {
|
|
188
195
|
// Set the split being iterated on.
|
|
189
196
|
JBSplit memory split = splits[i];
|
|
190
197
|
|
|
@@ -228,6 +235,9 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
228
235
|
emit SetSplit({
|
|
229
236
|
projectId: projectId, rulesetId: rulesetId, groupId: groupId, split: split, caller: msg.sender
|
|
230
237
|
});
|
|
238
|
+
unchecked {
|
|
239
|
+
++i;
|
|
240
|
+
}
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
// Store the number of splits for the project, ruleset, and group.
|
|
@@ -235,9 +245,12 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
235
245
|
|
|
236
246
|
// Clean up stale storage slots if the new split count is less than the previous count.
|
|
237
247
|
// This zeroes out leftover packed data to reclaim gas via storage refunds.
|
|
238
|
-
for (uint256 i = numberOfSplits; i < numberOfCurrentSplits;
|
|
248
|
+
for (uint256 i = numberOfSplits; i < numberOfCurrentSplits;) {
|
|
239
249
|
delete _packedSplitParts1Of[projectId][rulesetId][groupId][i];
|
|
240
250
|
delete _packedSplitParts2Of[projectId][rulesetId][groupId][i];
|
|
251
|
+
unchecked {
|
|
252
|
+
++i;
|
|
253
|
+
}
|
|
241
254
|
}
|
|
242
255
|
}
|
|
243
256
|
|
|
@@ -267,7 +280,7 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
267
280
|
JBSplit[] memory splits = new JBSplit[](splitCount);
|
|
268
281
|
|
|
269
282
|
// Loop through each split and unpack the values into structs.
|
|
270
|
-
for (uint256 i; i < splitCount;
|
|
283
|
+
for (uint256 i; i < splitCount;) {
|
|
271
284
|
// Get a reference to the first part of the split's packed data.
|
|
272
285
|
uint256 packedSplitPart1 = _packedSplitParts1Of[projectId][rulesetId][groupId][i];
|
|
273
286
|
|
|
@@ -301,6 +314,9 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
301
314
|
|
|
302
315
|
// Add the split to the value being returned.
|
|
303
316
|
splits[i] = split;
|
|
317
|
+
unchecked {
|
|
318
|
+
++i;
|
|
319
|
+
}
|
|
304
320
|
}
|
|
305
321
|
|
|
306
322
|
return splits;
|
|
@@ -314,7 +330,7 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
314
330
|
// Keep a reference to the number of splits.
|
|
315
331
|
uint256 numberOfSplits = splits.length;
|
|
316
332
|
|
|
317
|
-
for (uint256 i; i < numberOfSplits;
|
|
333
|
+
for (uint256 i; i < numberOfSplits;) {
|
|
318
334
|
// Set the split being iterated on.
|
|
319
335
|
JBSplit memory split = splits[i];
|
|
320
336
|
|
|
@@ -326,6 +342,9 @@ contract JBSplits is JBControlled, IJBSplits {
|
|
|
326
342
|
&& split.preferAddToBalance == lockedSplit.preferAddToBalance
|
|
327
343
|
&& split.lockedUntil >= lockedSplit.lockedUntil
|
|
328
344
|
) return true;
|
|
345
|
+
unchecked {
|
|
346
|
+
++i;
|
|
347
|
+
}
|
|
329
348
|
}
|
|
330
349
|
|
|
331
350
|
return false;
|
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) {
|