@bananapus/core-v6 0.0.9 → 0.0.11

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.
Files changed (44) hide show
  1. package/ADMINISTRATION.md +327 -0
  2. package/ARCHITECTURE.md +115 -0
  3. package/RISKS.md +68 -0
  4. package/SKILLS.md +5 -0
  5. package/STYLE_GUIDE.md +465 -0
  6. package/foundry.toml +1 -2
  7. package/package.json +2 -2
  8. package/src/JBChainlinkV3PriceFeed.sol +1 -5
  9. package/src/JBChainlinkV3SequencerPriceFeed.sol +1 -1
  10. package/src/JBController.sol +277 -277
  11. package/src/JBDeadline.sol +1 -1
  12. package/src/JBDirectory.sol +93 -93
  13. package/src/JBERC20.sol +43 -39
  14. package/src/JBFeelessAddresses.sol +12 -12
  15. package/src/JBFundAccessLimits.sol +82 -82
  16. package/src/JBMultiTerminal.sol +313 -313
  17. package/src/JBPermissions.sol +104 -100
  18. package/src/JBPrices.sol +68 -68
  19. package/src/JBProjects.sol +31 -31
  20. package/src/JBRulesets.sol +422 -422
  21. package/src/JBSplits.sol +116 -116
  22. package/src/JBTerminalStore.sol +651 -651
  23. package/src/JBTokens.sol +41 -41
  24. package/src/interfaces/IJBCashOutTerminal.sol +25 -7
  25. package/src/interfaces/IJBController.sol +78 -3
  26. package/src/interfaces/IJBDirectory.sol +25 -0
  27. package/src/interfaces/IJBFeeTerminal.sol +31 -0
  28. package/src/interfaces/IJBFeelessAddresses.sol +4 -0
  29. package/src/interfaces/IJBFundAccessLimits.sol +5 -0
  30. package/src/interfaces/IJBMigratable.sol +12 -8
  31. package/src/interfaces/IJBPayoutTerminal.sol +56 -9
  32. package/src/interfaces/IJBPermissions.sol +14 -7
  33. package/src/interfaces/IJBPermitTerminal.sol +4 -0
  34. package/src/interfaces/IJBPrices.sol +6 -0
  35. package/src/interfaces/IJBProjects.sol +8 -0
  36. package/src/interfaces/IJBRulesetApprovalHook.sol +1 -1
  37. package/src/interfaces/IJBRulesetDataHook.sol +23 -23
  38. package/src/interfaces/IJBRulesets.sol +54 -33
  39. package/src/interfaces/IJBSplits.sol +6 -0
  40. package/src/interfaces/IJBTerminal.sol +36 -0
  41. package/src/interfaces/IJBTerminalStore.sol +63 -63
  42. package/src/interfaces/IJBToken.sol +5 -5
  43. package/src/interfaces/IJBTokens.sol +50 -8
  44. package/test/TestDurationUnderflow.sol +3 -2
@@ -82,6 +82,193 @@ contract JBRulesets is JBControlled, IJBRulesets {
82
82
  // solhint-disable-next-line no-empty-blocks
83
83
  constructor(IJBDirectory directory) JBControlled(directory) {}
84
84
 
85
+ //*********************************************************************//
86
+ // ---------------------- external transactions ---------------------- //
87
+ //*********************************************************************//
88
+
89
+ /// @notice Queues the upcoming approvable ruleset for the specified project.
90
+ /// @dev Only a project's current controller can queue its rulesets.
91
+ /// @param projectId The ID of the project to queue the ruleset for.
92
+ /// @param duration The number of seconds the ruleset lasts for, after which a new ruleset starts.
93
+ /// - A `duration` of 0 means this ruleset will remain active until the project owner queues a new ruleset. That new
94
+ /// ruleset will start immediately.
95
+ /// - A ruleset with a non-zero `duration` applies until the duration ends – any newly queued rulesets will be
96
+ /// *queued* to take effect afterwards.
97
+ /// - If a duration ends and no new rulesets are queued, the ruleset rolls over to a new ruleset with the same rules
98
+ /// (except for a new `start` timestamp and a cut `weight`).
99
+ /// @param weight A fixed point number with 18 decimals that contracts can use to base arbitrary calculations on.
100
+ /// Payment terminals generally use this to determine how many tokens should be minted when the project is paid.
101
+ /// @param weightCutPercent A fraction (out of `JBConstants.MAX_WEIGHT_CUT_PERCENT`) to reduce the next ruleset's
102
+ /// `weight`
103
+ /// by.
104
+ /// - If a ruleset specifies a non-zero `weight`, the `weightCutPercent` does not apply.
105
+ /// - If the `weightCutPercent` is 0, the `weight` stays the same.
106
+ /// - If the `weightCutPercent` is 10% of `JBConstants.MAX_WEIGHT_CUT_PERCENT`, next ruleset's `weight` will be 90%
107
+ /// of the
108
+ /// current
109
+ /// one.
110
+ /// @param approvalHook A contract which dictates whether a proposed ruleset should be accepted or rejected. It can
111
+ /// be used to constrain a project owner's ability to change ruleset parameters over time.
112
+ /// @param metadata Arbitrary extra data to associate with this ruleset. This metadata is not used by `JBRulesets`.
113
+ /// @param mustStartAtOrAfter The earliest time the ruleset can start. The ruleset cannot start before this
114
+ /// timestamp.
115
+ /// @return The struct of the new ruleset.
116
+ function queueFor(
117
+ uint256 projectId,
118
+ uint256 duration,
119
+ uint256 weight,
120
+ uint256 weightCutPercent,
121
+ IJBRulesetApprovalHook approvalHook,
122
+ uint256 metadata,
123
+ uint256 mustStartAtOrAfter
124
+ )
125
+ external
126
+ override
127
+ onlyControllerOf(projectId)
128
+ returns (JBRuleset memory)
129
+ {
130
+ // Duration must fit in a uint32.
131
+ if (duration > type(uint32).max) revert JBRulesets_InvalidRulesetDuration(duration, type(uint32).max);
132
+
133
+ // Weight cut percent must be less than or equal to 100%.
134
+ if (weightCutPercent > JBConstants.MAX_WEIGHT_CUT_PERCENT) {
135
+ revert JBRulesets_InvalidWeightCutPercent(weightCutPercent);
136
+ }
137
+
138
+ // Weight must fit into a uint112.
139
+ if (weight > type(uint112).max) revert JBRulesets_InvalidWeight(weight, type(uint112).max);
140
+
141
+ // If the start date is not set, set it to be the current timestamp.
142
+ if (mustStartAtOrAfter == 0) {
143
+ mustStartAtOrAfter = block.timestamp;
144
+ }
145
+
146
+ // Make sure the min start date fits in a uint48, and that the start date of the following ruleset will also fit
147
+ // within the max.
148
+ if (mustStartAtOrAfter + duration > type(uint48).max) {
149
+ revert JBRulesets_InvalidRulesetEndTime(mustStartAtOrAfter + duration, type(uint48).max);
150
+ }
151
+
152
+ // Approval hook should be a valid contract, supporting the correct interface
153
+ if (approvalHook != IJBRulesetApprovalHook(address(0))) {
154
+ // Revert if there isn't a contract at the address
155
+ if (address(approvalHook).code.length == 0) revert JBRulesets_InvalidRulesetApprovalHook(approvalHook);
156
+
157
+ // Make sure the approval hook supports the expected interface.
158
+ try approvalHook.supportsInterface(type(IJBRulesetApprovalHook).interfaceId) returns (bool doesSupport) {
159
+ if (!doesSupport) revert JBRulesets_InvalidRulesetApprovalHook(approvalHook); // Contract exists at the
160
+ // address but
161
+ // with the
162
+ // wrong interface
163
+ } catch {
164
+ revert JBRulesets_InvalidRulesetApprovalHook(approvalHook); // No ERC165 support
165
+ }
166
+ }
167
+
168
+ // Get a reference to the latest ruleset's ID.
169
+ uint256 latestId = latestRulesetIdOf[projectId];
170
+
171
+ // The new rulesetId timestamp is now, or an increment from now if the current timestamp is taken.
172
+ uint256 rulesetId = latestId >= block.timestamp ? latestId + 1 : block.timestamp;
173
+
174
+ // Set up the ruleset by configuring intrinsic properties.
175
+ _configureIntrinsicPropertiesFor({
176
+ projectId: projectId, rulesetId: rulesetId, weight: weight, mustStartAtOrAfter: mustStartAtOrAfter
177
+ });
178
+
179
+ // Efficiently stores the ruleset's user-defined properties.
180
+ // If all user config properties are zero, no need to store anything as the default value will have the same
181
+ // outcome.
182
+ if (approvalHook != IJBRulesetApprovalHook(address(0)) || duration > 0 || weightCutPercent > 0) {
183
+ // approval hook in bits 0-159 bytes.
184
+ uint256 packed = uint160(address(approvalHook));
185
+
186
+ // duration in bits 160-191 bytes.
187
+ packed |= duration << 160;
188
+
189
+ // weightCutPercent in bits 192-223 bytes.
190
+ packed |= weightCutPercent << 192;
191
+
192
+ // Set in storage.
193
+ _packedUserPropertiesOf[projectId][rulesetId] = packed;
194
+ }
195
+
196
+ // Set the metadata if needed.
197
+ if (metadata > 0) _metadataOf[projectId][rulesetId] = metadata;
198
+
199
+ emit RulesetQueued({
200
+ rulesetId: rulesetId,
201
+ projectId: projectId,
202
+ duration: duration,
203
+ weight: weight,
204
+ weightCutPercent: weightCutPercent,
205
+ approvalHook: approvalHook,
206
+ metadata: metadata,
207
+ mustStartAtOrAfter: mustStartAtOrAfter,
208
+ caller: msg.sender
209
+ });
210
+
211
+ // Return the struct for the new ruleset's ID.
212
+ return _getStructFor({projectId: projectId, rulesetId: rulesetId});
213
+ }
214
+
215
+ /// @notice Cache the value of the ruleset weight for a specific ruleset.
216
+ /// @dev The caller should pass the ruleset ID that `currentOf()` actually uses. When a queued ruleset is rejected
217
+ /// by an approval hook, `currentOf()` falls back to the base ruleset — callers should pass that base ruleset's
218
+ /// ID,
219
+ /// not the rejected latest.
220
+ /// @param projectId The ID of the project having its ruleset weight cached.
221
+ /// @param rulesetId The ID of the ruleset to update the cache for.
222
+ function updateRulesetWeightCache(uint256 projectId, uint256 rulesetId) external override {
223
+ // Get the target ruleset.
224
+ JBRuleset memory targetRuleset = _getStructFor({projectId: projectId, rulesetId: rulesetId});
225
+
226
+ // Nothing to cache if the target ruleset doesn't have a duration or a weight cut percent.
227
+ // slither-disable-next-line incorrect-equality
228
+ if (targetRuleset.duration == 0 || targetRuleset.weightCutPercent == 0) return;
229
+
230
+ // Get a reference to the current cache.
231
+ JBRulesetWeightCache storage cache = _weightCacheOf[projectId][targetRuleset.id];
232
+
233
+ // Determine the largest start timestamp the cache can be filled to.
234
+ // Cap the advance to the cache lookup threshold per call to stay within the iteration limit in
235
+ // deriveWeightFrom.
236
+ // Multiple calls are needed to advance the cache for large cycle gaps.
237
+ uint256 maxStart = targetRuleset.start + (cache.weightCutMultiple + _WEIGHT_CUT_MULTIPLE_CACHE_LOOKUP_THRESHOLD)
238
+ * targetRuleset.duration;
239
+
240
+ // Determine the start timestamp to derive a weight from for the cache.
241
+ uint256 start = block.timestamp < maxStart ? block.timestamp : maxStart;
242
+
243
+ // The difference between the start of the latest queued ruleset and the start of the ruleset we're caching the
244
+ // weight of.
245
+ uint256 startDistance = start - targetRuleset.start;
246
+
247
+ // Calculate the weight cut multiple.
248
+ uint168 weightCutMultiple;
249
+ unchecked {
250
+ weightCutMultiple = uint168(startDistance / targetRuleset.duration);
251
+ }
252
+
253
+ // Store the new values.
254
+ cache.weight = uint112(
255
+ deriveWeightFrom({
256
+ projectId: projectId,
257
+ baseRulesetStart: targetRuleset.start,
258
+ baseRulesetDuration: targetRuleset.duration,
259
+ baseRulesetWeight: targetRuleset.weight,
260
+ baseRulesetWeightCutPercent: targetRuleset.weightCutPercent,
261
+ baseRulesetCacheId: targetRuleset.id,
262
+ start: start
263
+ })
264
+ );
265
+ cache.weightCutMultiple = weightCutMultiple;
266
+
267
+ emit WeightCacheUpdated({
268
+ projectId: projectId, weight: cache.weight, weightCutMultiple: weightCutMultiple, caller: msg.sender
269
+ });
270
+ }
271
+
85
272
  //*********************************************************************//
86
273
  // ------------------------- external views -------------------------- //
87
274
  //*********************************************************************//
@@ -495,51 +682,248 @@ contract JBRulesets is JBControlled, IJBRulesets {
495
682
  }
496
683
 
497
684
  //*********************************************************************//
498
- // -------------------------- internal views ------------------------- //
685
+ // ---------------------- internal transactions ---------------------- //
499
686
  //*********************************************************************//
500
687
 
501
- /// @notice The approval status of a given ruleset for a given project ID.
502
- /// @param projectId The ID of the project the ruleset belongs to.
503
- /// @param ruleset The ruleset to get the approval status of.
504
- /// @return The approval status of the project.
505
- function _approvalStatusOf(uint256 projectId, JBRuleset memory ruleset) internal view returns (JBApprovalStatus) {
506
- // If there is no ruleset ID to check the approval hook of, the approval hook is empty.
507
- // slither-disable-next-line incorrect-equality
508
- if (ruleset.basedOnId == 0) return JBApprovalStatus.Empty;
509
-
510
- // Get the struct of the ruleset with the approval hook.
511
- JBRuleset memory approvalHookRuleset = _getStructFor({projectId: projectId, rulesetId: ruleset.basedOnId});
688
+ /// @notice Updates the latest ruleset for this project if it exists. If there is no ruleset, initializes one.
689
+ /// @param projectId The ID of the project to update the latest ruleset for.
690
+ /// @param rulesetId The timestamp of when the ruleset was queued.
691
+ /// @param weight The weight to store in the queued ruleset.
692
+ /// @param mustStartAtOrAfter The earliest time the ruleset can start. The ruleset cannot start before this
693
+ /// timestamp.
694
+ function _configureIntrinsicPropertiesFor(
695
+ uint256 projectId,
696
+ uint256 rulesetId,
697
+ uint256 weight,
698
+ uint256 mustStartAtOrAfter
699
+ )
700
+ internal
701
+ {
702
+ // Keep a reference to the project's latest ruleset's ID.
703
+ uint256 latestId = latestRulesetIdOf[projectId];
512
704
 
513
- // If there is no approval hook, it's considered empty.
514
- if (approvalHookRuleset.approvalHook == IJBRulesetApprovalHook(address(0))) {
515
- return JBApprovalStatus.Empty;
705
+ // If the project doesn't have a ruleset yet, initialize one.
706
+ // slither-disable-next-line incorrect-equality
707
+ if (latestId == 0) {
708
+ // Use an empty ruleset as the base.
709
+ return _initializeRulesetFor({
710
+ projectId: projectId,
711
+ baseRuleset: _getStructFor({projectId: 0, rulesetId: 0}),
712
+ rulesetId: rulesetId,
713
+ mustStartAtOrAfter: mustStartAtOrAfter,
714
+ weight: weight
715
+ });
516
716
  }
517
717
 
518
- // Return the approval hook's approval status.
519
- // Wrap in try/catch to prevent a reverting approval hook from permanently freezing the project.
520
- // Note: A malicious hook that consumes all gas (e.g. infinite loop) could still DoS via gas exhaustion.
521
- // This is accepted risk since the project owner chose their own approval hook.
522
- // slither-disable-next-line calls-loop
523
- try approvalHookRuleset.approvalHook.approvalStatusOf({projectId: projectId, ruleset: ruleset}) returns (
524
- JBApprovalStatus status
525
- ) {
526
- return status;
527
- } catch {
528
- return JBApprovalStatus.Failed;
529
- }
530
- }
718
+ // Get a reference to the latest ruleset's struct.
719
+ JBRuleset memory baseRuleset = _getStructFor({projectId: projectId, rulesetId: latestId});
531
720
 
532
- /// @notice The ID of the ruleset which has started and hasn't expired yet, whether or not it has been approved, for
533
- /// a given project. If approved, this is the active ruleset.
534
- /// @dev A value of 0 is returned if no ruleset was found.
535
- /// @dev Assumes the project has a latest ruleset.
536
- /// @param projectId The ID of the project to check for a currently approvable ruleset.
537
- /// @return The ID of a currently approvable ruleset if one exists, or 0 if one doesn't exist.
538
- function _currentlyApprovableRulesetIdOf(uint256 projectId) internal view returns (uint256) {
539
- // Get a reference to the project's latest ruleset.
540
- uint256 rulesetId = latestRulesetIdOf[projectId];
721
+ // Get a reference to the approval status.
722
+ JBApprovalStatus approvalStatus = _approvalStatusOf({projectId: projectId, ruleset: baseRuleset});
541
723
 
542
- // Get the struct for the latest ruleset.
724
+ // If the base ruleset has started but wasn't approved if a approval hook exists
725
+ // OR it hasn't started but is currently approved
726
+ // OR it hasn't started but it is likely to be approved and takes place before the proposed one,
727
+ // set the struct to be the ruleset it's based on, which carries the latest approved ruleset.
728
+ if (
729
+ (block.timestamp >= baseRuleset.start
730
+ && approvalStatus != JBApprovalStatus.Approved
731
+ && approvalStatus != JBApprovalStatus.Empty)
732
+ || (block.timestamp < baseRuleset.start
733
+ && mustStartAtOrAfter < baseRuleset.start + baseRuleset.duration
734
+ && approvalStatus != JBApprovalStatus.Approved)
735
+ || (block.timestamp < baseRuleset.start
736
+ && mustStartAtOrAfter >= baseRuleset.start + baseRuleset.duration
737
+ && approvalStatus != JBApprovalStatus.Approved
738
+ && approvalStatus != JBApprovalStatus.ApprovalExpected
739
+ && approvalStatus != JBApprovalStatus.Empty)
740
+ ) {
741
+ baseRuleset = _getStructFor({projectId: projectId, rulesetId: baseRuleset.basedOnId});
742
+ }
743
+
744
+ // Make sure the ruleset starts after the base ruleset.
745
+ if (baseRuleset.start > mustStartAtOrAfter) mustStartAtOrAfter = baseRuleset.start;
746
+
747
+ // The time when the duration of the base ruleset's approval hook has finished.
748
+ // If the provided ruleset has no approval hook, return 0 (no constraint on start time).
749
+ uint256 timestampAfterApprovalHook;
750
+ if (baseRuleset.approvalHook != IJBRulesetApprovalHook(address(0))) {
751
+ try baseRuleset.approvalHook.DURATION() returns (uint256 duration) {
752
+ timestampAfterApprovalHook = rulesetId + duration;
753
+ } catch {
754
+ // If DURATION() reverts, treat as no approval hook constraint.
755
+ timestampAfterApprovalHook = 0;
756
+ }
757
+ }
758
+
759
+ _initializeRulesetFor({
760
+ projectId: projectId,
761
+ baseRuleset: baseRuleset,
762
+ rulesetId: rulesetId,
763
+ // Can only start after the approval hook.
764
+ mustStartAtOrAfter: timestampAfterApprovalHook > mustStartAtOrAfter
765
+ ? timestampAfterApprovalHook
766
+ : mustStartAtOrAfter,
767
+ weight: weight
768
+ });
769
+ }
770
+
771
+ /// @notice Initializes a ruleset with the specified properties.
772
+ /// @param projectId The ID of the project to initialize the ruleset for.
773
+ /// @param baseRuleset The ruleset struct to base the newly initialized one on.
774
+ /// @param rulesetId The `rulesetId` for the ruleset being initialized.
775
+ /// @param mustStartAtOrAfter The earliest time the ruleset can start. The ruleset cannot start before this
776
+ /// timestamp.
777
+ /// @param weight The weight to give the newly initialized ruleset.
778
+ function _initializeRulesetFor(
779
+ uint256 projectId,
780
+ JBRuleset memory baseRuleset,
781
+ uint256 rulesetId,
782
+ uint256 mustStartAtOrAfter,
783
+ uint256 weight
784
+ )
785
+ internal
786
+ {
787
+ // If there is no base, initialize a first ruleset.
788
+ // slither-disable-next-line incorrect-equality
789
+ if (baseRuleset.cycleNumber == 0) {
790
+ // Set fresh intrinsic properties.
791
+ _packAndStoreIntrinsicPropertiesOf({
792
+ rulesetId: rulesetId,
793
+ projectId: projectId,
794
+ rulesetCycleNumber: 1,
795
+ weight: weight,
796
+ basedOnId: baseRuleset.id,
797
+ start: mustStartAtOrAfter
798
+ });
799
+ } else {
800
+ // Derive the correct next start time from the base.
801
+ uint256 start = deriveStartFrom({
802
+ baseRulesetStart: baseRuleset.start,
803
+ baseRulesetDuration: baseRuleset.duration,
804
+ mustStartAtOrAfter: mustStartAtOrAfter
805
+ });
806
+
807
+ // A weight of 1 is a special case that represents inheriting the cut weight of the previous
808
+ // ruleset.
809
+ weight = weight == 1
810
+ ? deriveWeightFrom({
811
+ projectId: projectId,
812
+ baseRulesetStart: baseRuleset.start,
813
+ baseRulesetDuration: baseRuleset.duration,
814
+ baseRulesetWeight: baseRuleset.weight,
815
+ baseRulesetWeightCutPercent: baseRuleset.weightCutPercent,
816
+ baseRulesetCacheId: baseRuleset.id,
817
+ start: start
818
+ })
819
+ : weight;
820
+
821
+ // Derive the correct ruleset cycle number.
822
+ uint256 rulesetCycleNumber = deriveCycleNumberFrom({
823
+ baseRulesetCycleNumber: baseRuleset.cycleNumber,
824
+ baseRulesetStart: baseRuleset.start,
825
+ baseRulesetDuration: baseRuleset.duration,
826
+ start: start
827
+ });
828
+
829
+ // Update the intrinsic properties.
830
+ _packAndStoreIntrinsicPropertiesOf({
831
+ rulesetId: rulesetId,
832
+ projectId: projectId,
833
+ rulesetCycleNumber: rulesetCycleNumber,
834
+ weight: weight,
835
+ basedOnId: baseRuleset.id,
836
+ start: start
837
+ });
838
+ }
839
+
840
+ // Set the project's latest ruleset configuration.
841
+ latestRulesetIdOf[projectId] = rulesetId;
842
+
843
+ emit RulesetInitialized({
844
+ rulesetId: rulesetId, projectId: projectId, basedOnId: baseRuleset.id, caller: msg.sender
845
+ });
846
+ }
847
+
848
+ /// @notice Efficiently stores the provided intrinsic properties of a ruleset.
849
+ /// @param rulesetId The `rulesetId` of the ruleset to pack and store for.
850
+ /// @param projectId The ID of the project the ruleset belongs to.
851
+ /// @param rulesetCycleNumber The cycle number of the ruleset.
852
+ /// @param weight The weight of the ruleset.
853
+ /// @param basedOnId The `rulesetId` of the ruleset this ruleset was based on.
854
+ /// @param start The start time of this ruleset.
855
+ function _packAndStoreIntrinsicPropertiesOf(
856
+ uint256 rulesetId,
857
+ uint256 projectId,
858
+ uint256 rulesetCycleNumber,
859
+ uint256 weight,
860
+ uint256 basedOnId,
861
+ uint256 start
862
+ )
863
+ internal
864
+ {
865
+ // `weight` in bits 0-111.
866
+ uint256 packed = weight;
867
+
868
+ // `basedOnId` in bits 112-159.
869
+ packed |= basedOnId << 112;
870
+
871
+ // `start` in bits 160-207.
872
+ packed |= start << 160;
873
+
874
+ // cycle number in bits 208-255.
875
+ packed |= rulesetCycleNumber << 208;
876
+
877
+ // Store the packed value.
878
+ _packedIntrinsicPropertiesOf[projectId][rulesetId] = packed;
879
+ }
880
+
881
+ //*********************************************************************//
882
+ // -------------------------- internal views ------------------------- //
883
+ //*********************************************************************//
884
+
885
+ /// @notice The approval status of a given ruleset for a given project ID.
886
+ /// @param projectId The ID of the project the ruleset belongs to.
887
+ /// @param ruleset The ruleset to get the approval status of.
888
+ /// @return The approval status of the project.
889
+ function _approvalStatusOf(uint256 projectId, JBRuleset memory ruleset) internal view returns (JBApprovalStatus) {
890
+ // If there is no ruleset ID to check the approval hook of, the approval hook is empty.
891
+ // slither-disable-next-line incorrect-equality
892
+ if (ruleset.basedOnId == 0) return JBApprovalStatus.Empty;
893
+
894
+ // Get the struct of the ruleset with the approval hook.
895
+ JBRuleset memory approvalHookRuleset = _getStructFor({projectId: projectId, rulesetId: ruleset.basedOnId});
896
+
897
+ // If there is no approval hook, it's considered empty.
898
+ if (approvalHookRuleset.approvalHook == IJBRulesetApprovalHook(address(0))) {
899
+ return JBApprovalStatus.Empty;
900
+ }
901
+
902
+ // Return the approval hook's approval status.
903
+ // Wrap in try/catch to prevent a reverting approval hook from permanently freezing the project.
904
+ // Note: A malicious hook that consumes all gas (e.g. infinite loop) could still DoS via gas exhaustion.
905
+ // This is accepted risk since the project owner chose their own approval hook.
906
+ // slither-disable-next-line calls-loop
907
+ try approvalHookRuleset.approvalHook.approvalStatusOf({projectId: projectId, ruleset: ruleset}) returns (
908
+ JBApprovalStatus status
909
+ ) {
910
+ return status;
911
+ } catch {
912
+ return JBApprovalStatus.Failed;
913
+ }
914
+ }
915
+
916
+ /// @notice The ID of the ruleset which has started and hasn't expired yet, whether or not it has been approved, for
917
+ /// a given project. If approved, this is the active ruleset.
918
+ /// @dev A value of 0 is returned if no ruleset was found.
919
+ /// @dev Assumes the project has a latest ruleset.
920
+ /// @param projectId The ID of the project to check for a currently approvable ruleset.
921
+ /// @return The ID of a currently approvable ruleset if one exists, or 0 if one doesn't exist.
922
+ function _currentlyApprovableRulesetIdOf(uint256 projectId) internal view returns (uint256) {
923
+ // Get a reference to the project's latest ruleset.
924
+ uint256 rulesetId = latestRulesetIdOf[projectId];
925
+
926
+ // Get the struct for the latest ruleset.
543
927
  JBRuleset memory ruleset = _getStructFor({projectId: projectId, rulesetId: rulesetId});
544
928
 
545
929
  // Loop through all most recently queued rulesets until an approvable one is found, or we've proven one can't
@@ -709,388 +1093,4 @@ contract JBRulesets is JBControlled, IJBRulesets {
709
1093
  return 0;
710
1094
  }
711
1095
  }
712
-
713
- //*********************************************************************//
714
- // ---------------------- external transactions ---------------------- //
715
- //*********************************************************************//
716
-
717
- /// @notice Queues the upcoming approvable ruleset for the specified project.
718
- /// @dev Only a project's current controller can queue its rulesets.
719
- /// @param projectId The ID of the project to queue the ruleset for.
720
- /// @param duration The number of seconds the ruleset lasts for, after which a new ruleset starts.
721
- /// - A `duration` of 0 means this ruleset will remain active until the project owner queues a new ruleset. That new
722
- /// ruleset will start immediately.
723
- /// - A ruleset with a non-zero `duration` applies until the duration ends – any newly queued rulesets will be
724
- /// *queued* to take effect afterwards.
725
- /// - If a duration ends and no new rulesets are queued, the ruleset rolls over to a new ruleset with the same rules
726
- /// (except for a new `start` timestamp and a cut `weight`).
727
- /// @param weight A fixed point number with 18 decimals that contracts can use to base arbitrary calculations on.
728
- /// Payment terminals generally use this to determine how many tokens should be minted when the project is paid.
729
- /// @param weightCutPercent A fraction (out of `JBConstants.MAX_WEIGHT_CUT_PERCENT`) to reduce the next ruleset's
730
- /// `weight`
731
- /// by.
732
- /// - If a ruleset specifies a non-zero `weight`, the `weightCutPercent` does not apply.
733
- /// - If the `weightCutPercent` is 0, the `weight` stays the same.
734
- /// - If the `weightCutPercent` is 10% of `JBConstants.MAX_WEIGHT_CUT_PERCENT`, next ruleset's `weight` will be 90%
735
- /// of the
736
- /// current
737
- /// one.
738
- /// @param approvalHook A contract which dictates whether a proposed ruleset should be accepted or rejected. It can
739
- /// be used to constrain a project owner's ability to change ruleset parameters over time.
740
- /// @param metadata Arbitrary extra data to associate with this ruleset. This metadata is not used by `JBRulesets`.
741
- /// @param mustStartAtOrAfter The earliest time the ruleset can start. The ruleset cannot start before this
742
- /// timestamp.
743
- /// @return The struct of the new ruleset.
744
- function queueFor(
745
- uint256 projectId,
746
- uint256 duration,
747
- uint256 weight,
748
- uint256 weightCutPercent,
749
- IJBRulesetApprovalHook approvalHook,
750
- uint256 metadata,
751
- uint256 mustStartAtOrAfter
752
- )
753
- external
754
- override
755
- onlyControllerOf(projectId)
756
- returns (JBRuleset memory)
757
- {
758
- // Duration must fit in a uint32.
759
- if (duration > type(uint32).max) revert JBRulesets_InvalidRulesetDuration(duration, type(uint32).max);
760
-
761
- // Weight cut percent must be less than or equal to 100%.
762
- if (weightCutPercent > JBConstants.MAX_WEIGHT_CUT_PERCENT) {
763
- revert JBRulesets_InvalidWeightCutPercent(weightCutPercent);
764
- }
765
-
766
- // Weight must fit into a uint112.
767
- if (weight > type(uint112).max) revert JBRulesets_InvalidWeight(weight, type(uint112).max);
768
-
769
- // If the start date is not set, set it to be the current timestamp.
770
- if (mustStartAtOrAfter == 0) {
771
- mustStartAtOrAfter = block.timestamp;
772
- }
773
-
774
- // Make sure the min start date fits in a uint48, and that the start date of the following ruleset will also fit
775
- // within the max.
776
- if (mustStartAtOrAfter + duration > type(uint48).max) {
777
- revert JBRulesets_InvalidRulesetEndTime(mustStartAtOrAfter + duration, type(uint48).max);
778
- }
779
-
780
- // Approval hook should be a valid contract, supporting the correct interface
781
- if (approvalHook != IJBRulesetApprovalHook(address(0))) {
782
- // Revert if there isn't a contract at the address
783
- if (address(approvalHook).code.length == 0) revert JBRulesets_InvalidRulesetApprovalHook(approvalHook);
784
-
785
- // Make sure the approval hook supports the expected interface.
786
- try approvalHook.supportsInterface(type(IJBRulesetApprovalHook).interfaceId) returns (bool doesSupport) {
787
- if (!doesSupport) revert JBRulesets_InvalidRulesetApprovalHook(approvalHook); // Contract exists at the
788
- // address but
789
- // with the
790
- // wrong interface
791
- } catch {
792
- revert JBRulesets_InvalidRulesetApprovalHook(approvalHook); // No ERC165 support
793
- }
794
- }
795
-
796
- // Get a reference to the latest ruleset's ID.
797
- uint256 latestId = latestRulesetIdOf[projectId];
798
-
799
- // The new rulesetId timestamp is now, or an increment from now if the current timestamp is taken.
800
- uint256 rulesetId = latestId >= block.timestamp ? latestId + 1 : block.timestamp;
801
-
802
- // Set up the ruleset by configuring intrinsic properties.
803
- _configureIntrinsicPropertiesFor({
804
- projectId: projectId, rulesetId: rulesetId, weight: weight, mustStartAtOrAfter: mustStartAtOrAfter
805
- });
806
-
807
- // Efficiently stores the ruleset's user-defined properties.
808
- // If all user config properties are zero, no need to store anything as the default value will have the same
809
- // outcome.
810
- if (approvalHook != IJBRulesetApprovalHook(address(0)) || duration > 0 || weightCutPercent > 0) {
811
- // approval hook in bits 0-159 bytes.
812
- uint256 packed = uint160(address(approvalHook));
813
-
814
- // duration in bits 160-191 bytes.
815
- packed |= duration << 160;
816
-
817
- // weightCutPercent in bits 192-223 bytes.
818
- packed |= weightCutPercent << 192;
819
-
820
- // Set in storage.
821
- _packedUserPropertiesOf[projectId][rulesetId] = packed;
822
- }
823
-
824
- // Set the metadata if needed.
825
- if (metadata > 0) _metadataOf[projectId][rulesetId] = metadata;
826
-
827
- emit RulesetQueued({
828
- rulesetId: rulesetId,
829
- projectId: projectId,
830
- duration: duration,
831
- weight: weight,
832
- weightCutPercent: weightCutPercent,
833
- approvalHook: approvalHook,
834
- metadata: metadata,
835
- mustStartAtOrAfter: mustStartAtOrAfter,
836
- caller: msg.sender
837
- });
838
-
839
- // Return the struct for the new ruleset's ID.
840
- return _getStructFor({projectId: projectId, rulesetId: rulesetId});
841
- }
842
-
843
- /// @notice Cache the value of the ruleset weight for a specific ruleset.
844
- /// @dev The caller should pass the ruleset ID that `currentOf()` actually uses. When a queued ruleset is rejected
845
- /// by an approval hook, `currentOf()` falls back to the base ruleset — callers should pass that base ruleset's
846
- /// ID,
847
- /// not the rejected latest.
848
- /// @param projectId The ID of the project having its ruleset weight cached.
849
- /// @param rulesetId The ID of the ruleset to update the cache for.
850
- function updateRulesetWeightCache(uint256 projectId, uint256 rulesetId) external override {
851
- // Get the target ruleset.
852
- JBRuleset memory targetRuleset = _getStructFor({projectId: projectId, rulesetId: rulesetId});
853
-
854
- // Nothing to cache if the target ruleset doesn't have a duration or a weight cut percent.
855
- // slither-disable-next-line incorrect-equality
856
- if (targetRuleset.duration == 0 || targetRuleset.weightCutPercent == 0) return;
857
-
858
- // Get a reference to the current cache.
859
- JBRulesetWeightCache storage cache = _weightCacheOf[projectId][targetRuleset.id];
860
-
861
- // Determine the largest start timestamp the cache can be filled to.
862
- // Cap the advance to the cache lookup threshold per call to stay within the iteration limit in
863
- // deriveWeightFrom.
864
- // Multiple calls are needed to advance the cache for large cycle gaps.
865
- uint256 maxStart = targetRuleset.start + (cache.weightCutMultiple + _WEIGHT_CUT_MULTIPLE_CACHE_LOOKUP_THRESHOLD)
866
- * targetRuleset.duration;
867
-
868
- // Determine the start timestamp to derive a weight from for the cache.
869
- uint256 start = block.timestamp < maxStart ? block.timestamp : maxStart;
870
-
871
- // The difference between the start of the latest queued ruleset and the start of the ruleset we're caching the
872
- // weight of.
873
- uint256 startDistance = start - targetRuleset.start;
874
-
875
- // Calculate the weight cut multiple.
876
- uint168 weightCutMultiple;
877
- unchecked {
878
- weightCutMultiple = uint168(startDistance / targetRuleset.duration);
879
- }
880
-
881
- // Store the new values.
882
- cache.weight = uint112(
883
- deriveWeightFrom({
884
- projectId: projectId,
885
- baseRulesetStart: targetRuleset.start,
886
- baseRulesetDuration: targetRuleset.duration,
887
- baseRulesetWeight: targetRuleset.weight,
888
- baseRulesetWeightCutPercent: targetRuleset.weightCutPercent,
889
- baseRulesetCacheId: targetRuleset.id,
890
- start: start
891
- })
892
- );
893
- cache.weightCutMultiple = weightCutMultiple;
894
-
895
- emit WeightCacheUpdated({
896
- projectId: projectId, weight: cache.weight, weightCutMultiple: weightCutMultiple, caller: msg.sender
897
- });
898
- }
899
-
900
- //*********************************************************************//
901
- // ------------------------ internal functions ----------------------- //
902
- //*********************************************************************//
903
-
904
- /// @notice Updates the latest ruleset for this project if it exists. If there is no ruleset, initializes one.
905
- /// @param projectId The ID of the project to update the latest ruleset for.
906
- /// @param rulesetId The timestamp of when the ruleset was queued.
907
- /// @param weight The weight to store in the queued ruleset.
908
- /// @param mustStartAtOrAfter The earliest time the ruleset can start. The ruleset cannot start before this
909
- /// timestamp.
910
- function _configureIntrinsicPropertiesFor(
911
- uint256 projectId,
912
- uint256 rulesetId,
913
- uint256 weight,
914
- uint256 mustStartAtOrAfter
915
- )
916
- internal
917
- {
918
- // Keep a reference to the project's latest ruleset's ID.
919
- uint256 latestId = latestRulesetIdOf[projectId];
920
-
921
- // If the project doesn't have a ruleset yet, initialize one.
922
- // slither-disable-next-line incorrect-equality
923
- if (latestId == 0) {
924
- // Use an empty ruleset as the base.
925
- return _initializeRulesetFor({
926
- projectId: projectId,
927
- baseRuleset: _getStructFor({projectId: 0, rulesetId: 0}),
928
- rulesetId: rulesetId,
929
- mustStartAtOrAfter: mustStartAtOrAfter,
930
- weight: weight
931
- });
932
- }
933
-
934
- // Get a reference to the latest ruleset's struct.
935
- JBRuleset memory baseRuleset = _getStructFor({projectId: projectId, rulesetId: latestId});
936
-
937
- // Get a reference to the approval status.
938
- JBApprovalStatus approvalStatus = _approvalStatusOf({projectId: projectId, ruleset: baseRuleset});
939
-
940
- // If the base ruleset has started but wasn't approved if a approval hook exists
941
- // OR it hasn't started but is currently approved
942
- // OR it hasn't started but it is likely to be approved and takes place before the proposed one,
943
- // set the struct to be the ruleset it's based on, which carries the latest approved ruleset.
944
- if (
945
- (block.timestamp >= baseRuleset.start
946
- && approvalStatus != JBApprovalStatus.Approved
947
- && approvalStatus != JBApprovalStatus.Empty)
948
- || (block.timestamp < baseRuleset.start
949
- && mustStartAtOrAfter < baseRuleset.start + baseRuleset.duration
950
- && approvalStatus != JBApprovalStatus.Approved)
951
- || (block.timestamp < baseRuleset.start
952
- && mustStartAtOrAfter >= baseRuleset.start + baseRuleset.duration
953
- && approvalStatus != JBApprovalStatus.Approved
954
- && approvalStatus != JBApprovalStatus.ApprovalExpected
955
- && approvalStatus != JBApprovalStatus.Empty)
956
- ) {
957
- baseRuleset = _getStructFor({projectId: projectId, rulesetId: baseRuleset.basedOnId});
958
- }
959
-
960
- // Make sure the ruleset starts after the base ruleset.
961
- if (baseRuleset.start > mustStartAtOrAfter) mustStartAtOrAfter = baseRuleset.start;
962
-
963
- // The time when the duration of the base ruleset's approval hook has finished.
964
- // If the provided ruleset has no approval hook, return 0 (no constraint on start time).
965
- uint256 timestampAfterApprovalHook;
966
- if (baseRuleset.approvalHook != IJBRulesetApprovalHook(address(0))) {
967
- try baseRuleset.approvalHook.DURATION() returns (uint256 duration) {
968
- timestampAfterApprovalHook = rulesetId + duration;
969
- } catch {
970
- // If DURATION() reverts, treat as no approval hook constraint.
971
- timestampAfterApprovalHook = 0;
972
- }
973
- }
974
-
975
- _initializeRulesetFor({
976
- projectId: projectId,
977
- baseRuleset: baseRuleset,
978
- rulesetId: rulesetId,
979
- // Can only start after the approval hook.
980
- mustStartAtOrAfter: timestampAfterApprovalHook > mustStartAtOrAfter
981
- ? timestampAfterApprovalHook
982
- : mustStartAtOrAfter,
983
- weight: weight
984
- });
985
- }
986
-
987
- /// @notice Initializes a ruleset with the specified properties.
988
- /// @param projectId The ID of the project to initialize the ruleset for.
989
- /// @param baseRuleset The ruleset struct to base the newly initialized one on.
990
- /// @param rulesetId The `rulesetId` for the ruleset being initialized.
991
- /// @param mustStartAtOrAfter The earliest time the ruleset can start. The ruleset cannot start before this
992
- /// timestamp.
993
- /// @param weight The weight to give the newly initialized ruleset.
994
- function _initializeRulesetFor(
995
- uint256 projectId,
996
- JBRuleset memory baseRuleset,
997
- uint256 rulesetId,
998
- uint256 mustStartAtOrAfter,
999
- uint256 weight
1000
- )
1001
- internal
1002
- {
1003
- // If there is no base, initialize a first ruleset.
1004
- // slither-disable-next-line incorrect-equality
1005
- if (baseRuleset.cycleNumber == 0) {
1006
- // Set fresh intrinsic properties.
1007
- _packAndStoreIntrinsicPropertiesOf({
1008
- rulesetId: rulesetId,
1009
- projectId: projectId,
1010
- rulesetCycleNumber: 1,
1011
- weight: weight,
1012
- basedOnId: baseRuleset.id,
1013
- start: mustStartAtOrAfter
1014
- });
1015
- } else {
1016
- // Derive the correct next start time from the base.
1017
- uint256 start = deriveStartFrom({
1018
- baseRulesetStart: baseRuleset.start,
1019
- baseRulesetDuration: baseRuleset.duration,
1020
- mustStartAtOrAfter: mustStartAtOrAfter
1021
- });
1022
-
1023
- // A weight of 1 is a special case that represents inheriting the cut weight of the previous
1024
- // ruleset.
1025
- weight = weight == 1
1026
- ? deriveWeightFrom({
1027
- projectId: projectId,
1028
- baseRulesetStart: baseRuleset.start,
1029
- baseRulesetDuration: baseRuleset.duration,
1030
- baseRulesetWeight: baseRuleset.weight,
1031
- baseRulesetWeightCutPercent: baseRuleset.weightCutPercent,
1032
- baseRulesetCacheId: baseRuleset.id,
1033
- start: start
1034
- })
1035
- : weight;
1036
-
1037
- // Derive the correct ruleset cycle number.
1038
- uint256 rulesetCycleNumber = deriveCycleNumberFrom({
1039
- baseRulesetCycleNumber: baseRuleset.cycleNumber,
1040
- baseRulesetStart: baseRuleset.start,
1041
- baseRulesetDuration: baseRuleset.duration,
1042
- start: start
1043
- });
1044
-
1045
- // Update the intrinsic properties.
1046
- _packAndStoreIntrinsicPropertiesOf({
1047
- rulesetId: rulesetId,
1048
- projectId: projectId,
1049
- rulesetCycleNumber: rulesetCycleNumber,
1050
- weight: weight,
1051
- basedOnId: baseRuleset.id,
1052
- start: start
1053
- });
1054
- }
1055
-
1056
- // Set the project's latest ruleset configuration.
1057
- latestRulesetIdOf[projectId] = rulesetId;
1058
-
1059
- emit RulesetInitialized({
1060
- rulesetId: rulesetId, projectId: projectId, basedOnId: baseRuleset.id, caller: msg.sender
1061
- });
1062
- }
1063
-
1064
- /// @notice Efficiently stores the provided intrinsic properties of a ruleset.
1065
- /// @param rulesetId The `rulesetId` of the ruleset to pack and store for.
1066
- /// @param projectId The ID of the project the ruleset belongs to.
1067
- /// @param rulesetCycleNumber The cycle number of the ruleset.
1068
- /// @param weight The weight of the ruleset.
1069
- /// @param basedOnId The `rulesetId` of the ruleset this ruleset was based on.
1070
- /// @param start The start time of this ruleset.
1071
- function _packAndStoreIntrinsicPropertiesOf(
1072
- uint256 rulesetId,
1073
- uint256 projectId,
1074
- uint256 rulesetCycleNumber,
1075
- uint256 weight,
1076
- uint256 basedOnId,
1077
- uint256 start
1078
- )
1079
- internal
1080
- {
1081
- // `weight` in bits 0-111.
1082
- uint256 packed = weight;
1083
-
1084
- // `basedOnId` in bits 112-159.
1085
- packed |= basedOnId << 112;
1086
-
1087
- // `start` in bits 160-207.
1088
- packed |= start << 160;
1089
-
1090
- // cycle number in bits 208-255.
1091
- packed |= rulesetCycleNumber << 208;
1092
-
1093
- // Store the packed value.
1094
- _packedIntrinsicPropertiesOf[projectId][rulesetId] = packed;
1095
- }
1096
1096
  }