-
Notifications
You must be signed in to change notification settings - Fork 176
Expand file tree
/
Copy pathIssuanceAllocator.sol
More file actions
1216 lines (1066 loc) · 62.5 KB
/
Copy pathIssuanceAllocator.sol
File metadata and controls
1216 lines (1066 loc) · 62.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.27;
import {
TargetIssuancePerBlock,
Allocation,
AllocationTarget,
DistributionState,
SelfMintingEventMode
} from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocatorTypes.sol";
import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationDistribution.sol";
import { IIssuanceAllocationAdministration } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationAdministration.sol";
import { IIssuanceAllocationStatus } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationStatus.sol";
import { IIssuanceAllocationData } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationData.sol";
import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol";
import { BaseUpgradeable } from "../common/BaseUpgradeable.sol";
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
// solhint-disable-next-line no-unused-import
import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; // Used by @inheritdoc
/**
* @title IssuanceAllocator
* @author Edge & Node
* @notice This contract is responsible for allocating token issuance to different components
* of the protocol. It calculates issuance for all targets based on their configured rates
* (tokens per block) and handles minting for allocator-minting targets.
*
* @dev The contract maintains a 100% allocation invariant through a default target mechanism:
* - A default target exists at targetAddresses[0] (initialized to address(0))
* - The default target automatically receives any unallocated portion of issuance
* - Total allocation across all targets always equals issuancePerBlock (tracked as absolute rates)
* - The default target address can be changed via setDefaultTarget()
* - When the default address is address(0), this 'unallocated' portion is not minted
* - Regular targets cannot be set as the default target address
*
* @dev The contract supports two types of allocation for each target:
* 1. Allocator-minting allocation: The IssuanceAllocator calculates and mints tokens directly to targets
* for this portion of their allocation.
*
* 2. Self-minting allocation: The IssuanceAllocator calculates issuance but does not mint tokens directly.
* Instead, targets are expected to call `getTargetIssuancePerBlock` to determine their self-minting
* issuance amount and mint tokens themselves. This feature is primarily intended for backwards
* compatibility with existing contracts like the RewardsManager.
*
* Each target can have both allocator-minting and self-minting allocations. New targets are expected
* to use allocator-minting allocation to provide more robust control over token issuance through
* the IssuanceAllocator. The self-minting allocation is intended only for backwards compatibility
* with existing contracts.
*
* @dev Pause Behavior:
* - Allocator-minting: Completely suspended during pause. No tokens minted, lastDistributionBlock frozen.
* When unpaused, distributes retroactively using current rates for entire undistributed period. (Distribution will be triggered by calling distributeIssuance() when not paused.)
* - Self-minting: Continues tracking via events and accumulation during pause. Accumulated self-minting
* reduces allocator-minting budget when distribution resumes, ensuring total issuance conservation.
* - Ongoing accumulation: Once accumulation starts (during pause), continues through any unpaused
* periods until distribution clears it, preventing loss of self-minting allowances across pause cycles.
* - Tracking divergence: lastSelfMintingBlock advances during pause (for allowance tracking) while
* lastDistributionBlock stays frozen (no allocator-minting). This is intentional and correct.
*
* @dev Issuance Accounting Invariants:
* The contract maintains strict accounting to ensure total token issuance never exceeds the configured
* issuancePerBlock rate over any time period. This section provides the mathematical foundation for
* understanding the relationship between self-minting and allocator-minting.
*
* Key Invariants:
* 1. Allocation Completeness: For all blocks b, totalAllocatorRate_b + totalSelfMintingRate_b = issuancePerBlock_b
* This ensures 100% of issuance is always allocated across all targets.
*
* 2. Self-Minting Accumulation: For any undistributed block range [fromBlock, toBlock]:
* selfMintingOffset = Σ(totalSelfMintingRate_b) for all b in range
* where totalSelfMintingRate_b is the end-state rate for block b.
*
* 3. Rate Constraint: For all blocks b, totalSelfMintingRate_b ≤ issuancePerBlock_b
* This follows from invariant (1) since 0 ≤ totalAllocatorRate_b.
*
* 4. Issuance Upper Bound: For any distribution period with blocks = toBlock - fromBlock + 1:
* Let issuancePerBlock_final = current issuancePerBlock at distribution time
*
* From invariants (2) and (3):
* selfMintingOffset ≤ Σ(issuancePerBlock_b)
*
* Allocator-minting budget for period:
* available = max(0, issuancePerBlock_final * blocks - selfMintingOffset)
*
* Total minted (self + allocator) for period:
* ≤ max(selfMintingOffset, issuancePerBlock_final * blocks)
* ≤ Σ(issuancePerBlock_b)
*
* Therefore, total issuance never exceeds the sum of configured rates during the period.
*
* 5. Offset Reconciliation: During pending distribution, selfMintingOffset is adjusted to account for
* the period's issuance budget. When distribution catches up to current block, the offset is cleared.
* Any remaining offset when cleared represents self-minting that occurred beyond what the final
* issuancePerBlock rate would allow for the period. This is acceptable because:
* a) Self-minting targets were operating under rates that were valid at the time
* b) The total minted still respects the Σ(issuancePerBlock_b) bound (invariant 4)
* c) Clearing the offset prevents it from affecting future distributions
* d) The SelfMintingOffsetReconciled event provides visibility into all offset adjustments
*
* This design ensures that even when issuancePerBlock or allocation rates change over time, and even
* when self-minting targets mint independently, the total tokens minted never exceeds the sum of
* configured issuance rates during the period.
*
* @dev There are a number of scenarios where the IssuanceAllocator could run into issues, including:
* 1. The targetAddresses array could grow large enough that it exceeds the gas limit when calling distributeIssuance.
* 2. When notifying targets of allocation changes the calls to `beforeIssuanceAllocationChange` could exceed the gas limit.
* 3. Target contracts could revert when notifying them of changes via `beforeIssuanceAllocationChange`.
* While in practice the IssuanceAllocator is expected to have a relatively small number of trusted targets, and the
* gas limit is expected to be high enough to handle the above scenarios, the following would allow recovery:
* 1. The contract can be paused, which can help make the recovery process easier to manage.
* 2. The GOVERNOR_ROLE can directly trigger change notification to individual targets. As there is per target
* tracking of the lastChangeNotifiedBlock, this can reduce the gas cost of other operations and allow
* for graceful recovery.
* 3. If a target reverts when notifying it of changes or notifying it is too expensive, the GOVERNOR_ROLE can use `forceTargetNoChangeNotificationBlock()`
* to skip notifying that particular target of changes.
*
* In combination these should allow recovery from gas limit issues or malfunctioning targets, with fine-grained control over
* which targets are notified of changes and when.
*
* @dev Reentrancy Protection:
* The contract code is designed to be reentrant-safe and should be carefully reviewed and maintained
* to preserve this property. However, reentrancy guards (using transient storage per EIP-1153) are
* applied to governance functions that modify configuration or state as an additional layer of defense.
* This provides protection against potential issues if the multi-sig governor role were to have known
* signatures that could be exploited by malicious actors to trigger reentrant calls.
*
* The `distributeIssuance()` function intentionally does NOT have a reentrancy guard to allow
* legitimate use cases where targets call it during notifications (e.g., to claim pending issuance
* before allocation changes). This is safe because distributeIssuance() has built-in block-tracking
* protection (preventing double-distribution in the same block), makes no external calls that could
* expose inconsistent state, and does not modify allocations.
* @custom:security-contact Please email security+contracts@thegraph.com if you find any bugs. We might have an active bug bounty program.
*/
contract IssuanceAllocator is
BaseUpgradeable,
ReentrancyGuardTransient,
IIssuanceAllocationDistribution,
IIssuanceAllocationAdministration,
IIssuanceAllocationStatus,
IIssuanceAllocationData
{
// -- Namespaced Storage --
/// @notice ERC-7201 storage location for IssuanceAllocator
bytes32 private constant ISSUANCE_ALLOCATOR_STORAGE_LOCATION =
// solhint-disable-next-line gas-small-strings
keccak256(abi.encode(uint256(keccak256("graphprotocol.storage.IssuanceAllocator")) - 1)) &
~bytes32(uint256(0xff));
/// @notice Main storage structure for IssuanceAllocator using ERC-7201 namespaced storage
/// @param issuancePerBlock Total issuance per block across all targets
/// @param lastDistributionBlock Last block when allocator-minting issuance was distributed
/// @param lastSelfMintingBlock Last block when self-minting was advanced
/// @param selfMintingOffset Self-minting that offsets allocator-minting budget (accumulates during pause, clears on distribution)
/// @param allocationTargets Mapping of target addresses to their allocation data
/// @param targetAddresses Array of all target addresses (including default target at index 0)
/// @param totalSelfMintingRate Total self-minting rate (tokens per block) across all targets
/// @param selfMintingEventMode Controls self-minting event emission behavior (PerTarget, Aggregate, or None)
/// @dev Design invariant: totalAllocatorRate + totalSelfMintingRate == issuancePerBlock (always 100% allocated)
/// @dev Design invariant: targetAddresses[0] is always the default target address
/// @dev Design invariant: 1 <= targetAddresses.length (default target always exists)
/// @dev Design invariant: default target (targetAddresses[0]) is automatically adjusted to maintain 100% total
/// @custom:storage-location erc7201:graphprotocol.storage.IssuanceAllocator
struct IssuanceAllocatorData {
uint256 issuancePerBlock;
uint256 lastDistributionBlock;
uint256 lastSelfMintingBlock;
uint256 selfMintingOffset;
mapping(address => AllocationTarget) allocationTargets;
address[] targetAddresses;
uint256 totalSelfMintingRate;
SelfMintingEventMode selfMintingEventMode;
}
/**
* @notice Returns the storage struct for IssuanceAllocator
* @return $ contract storage
*/
function _getIssuanceAllocatorStorage() private pure returns (IssuanceAllocatorData storage $) {
// solhint-disable-previous-line use-natspec
// Solhint does not support $ return variable in natspec
bytes32 slot = ISSUANCE_ALLOCATOR_STORAGE_LOCATION;
// solhint-disable-next-line no-inline-assembly
assembly {
$.slot := slot
}
}
// -- Custom Errors --
/// @notice Thrown when attempting to add a target with zero address
error TargetAddressCannotBeZero();
/// @notice Thrown when the total allocation would exceed available budget
/// @param requested The total requested allocation (allocator + self minting)
/// @param available The available budget for this target
error InsufficientAllocationAvailable(uint256 requested, uint256 available);
/// @notice Thrown when attempting to decrease issuance rate without sufficient unallocated budget
/// @param oldRate The current issuance rate
/// @param newRate The proposed new issuance rate
/// @param unallocated The unallocated budget available to absorb the decrease
error InsufficientUnallocatedForRateDecrease(uint256 oldRate, uint256 newRate, uint256 unallocated);
/// @notice Thrown when a target does not support the IIssuanceTarget interface
/// @param target The target address that doesn't support the interface
error TargetDoesNotSupportIIssuanceTarget(address target);
/// @notice Thrown when toBlockNumber is out of valid range for accumulation
/// @param toBlock The invalid block number provided
/// @param minBlock The minimum valid block number (lastDistributionBlock)
/// @param maxBlock The maximum valid block number (current block)
error ToBlockOutOfRange(uint256 toBlock, uint256 minBlock, uint256 maxBlock);
/// @notice Thrown when attempting to set allocation for the default target
/// @param defaultTarget The address of the default target
error CannotSetAllocationForDefaultTarget(address defaultTarget);
/// @notice Thrown when attempting to set default target address to a normally allocated target
/// @param target The target address that already has an allocation
error CannotSetDefaultToAllocatedTarget(address target);
// -- Events --
/// @notice Emitted when issuance is distributed to a target
/// @param target The address of the target that received issuance
/// @param amount The amount of tokens distributed
/// @param fromBlock First block included in this distribution (inclusive)
/// @param toBlock Last block included in this distribution (inclusive). Range is [fromBlock, toBlock]
event IssuanceDistributed(
address indexed target,
uint256 amount,
uint256 indexed fromBlock,
uint256 indexed toBlock
); // solhint-disable-line gas-indexed-events
/// @notice Emitted when a target's allocation is updated
/// @param target The address of the target whose allocation was updated
/// @param newAllocatorMintingRate The new allocator-minting rate (tokens per block) for the target
/// @param newSelfMintingRate The new self-minting rate (tokens per block) for the target
event TargetAllocationUpdated(address indexed target, uint256 newAllocatorMintingRate, uint256 newSelfMintingRate); // solhint-disable-line gas-indexed-events
// Do not need to index rate values
/// @notice Emitted when the issuance per block is updated
/// @param oldIssuancePerBlock The previous issuance per block amount
/// @param newIssuancePerBlock The new issuance per block amount
event IssuancePerBlockUpdated(uint256 oldIssuancePerBlock, uint256 newIssuancePerBlock); // solhint-disable-line gas-indexed-events
// Do not need to index issuance per block values
/// @notice Emitted when the default target is updated
/// @param oldAddress The previous default target address
/// @param newAddress The new default target address
event DefaultTargetUpdated(address indexed oldAddress, address indexed newAddress);
/// @notice Emitted when self-minting allowance is calculated for a target
/// @param target The address of the target with self-minting allocation
/// @param amount The amount of tokens available for self-minting
/// @param fromBlock First block included in this allowance period (inclusive)
/// @param toBlock Last block included in this allowance period (inclusive). Range is [fromBlock, toBlock]
event IssuanceSelfMintAllowance(
address indexed target,
uint256 amount,
uint256 indexed fromBlock,
uint256 indexed toBlock
); // solhint-disable-line gas-indexed-events
/* solhint-disable gas-indexed-events */
/// @notice Emitted when self-minting offset is reconciled during pending distribution
/// @param offsetBefore The self-minting offset before reconciliation
/// @param offsetAfter The self-minting offset after reconciliation (0 when caught up to current block)
/// @param totalForPeriod The total issuance budget for the distributed period
/// @param fromBlock First block in the distribution period (inclusive)
/// @param toBlock Last block in the distribution period (inclusive)
/// @dev This event provides visibility into the accounting reconciliation between self-minting
/// and allocator-minting budgets during pending distribution. When offsetAfter is 0, the contract
/// has fully caught up with distribution. When offsetAfter > 0, there remains accumulated offset
/// that will be applied to future distributions.
event SelfMintingOffsetReconciled(
uint256 offsetBefore,
uint256 offsetAfter,
uint256 totalForPeriod,
uint256 indexed fromBlock,
uint256 indexed toBlock
);
/* solhint-enable gas-indexed-events */
/* solhint-disable gas-indexed-events */
/// @notice Emitted when self-minting offset accumulates during pause or catch-up
/// @param offsetBefore The self-minting offset before accumulation
/// @param offsetAfter The self-minting offset after accumulation
/// @param fromBlock First block in the accumulation period (inclusive)
/// @param toBlock Last block in the accumulation period (inclusive)
/// @dev This event provides visibility into offset growth during pause periods or while catching up
/// after unpause. Together with SelfMintingOffsetReconciled, provides complete accounting of all
/// offset changes.
event SelfMintingOffsetAccumulated(
uint256 offsetBefore,
uint256 offsetAfter,
uint256 indexed fromBlock,
uint256 indexed toBlock
);
/* solhint-enable gas-indexed-events */
/// @notice Emitted when self-minting allowance is calculated in aggregate mode
/// @param totalAmount The total amount of tokens available for self-minting across all targets
/// @param fromBlock First block included in this allowance period (inclusive)
/// @param toBlock Last block included in this allowance period (inclusive)
/// @dev This event is emitted when selfMintingEventMode is Aggregate, providing a single event
/// instead of per-target events to reduce gas costs
event IssuanceSelfMintAllowanceAggregate(uint256 totalAmount, uint256 indexed fromBlock, uint256 indexed toBlock); // solhint-disable-line gas-indexed-events
/// @notice Emitted when self-minting event mode is changed
/// @param oldMode The previous event emission mode
/// @param newMode The new event emission mode
event SelfMintingEventModeUpdated(SelfMintingEventMode oldMode, SelfMintingEventMode newMode);
// -- Constructor --
/**
* @notice Constructor for the IssuanceAllocator contract
* @dev This contract is upgradeable, but we use the constructor to pass the Graph Token address
* to the base contract.
* @param _graphToken Address of the Graph Token contract
* @custom:oz-upgrades-unsafe-allow constructor
*/
constructor(address _graphToken) BaseUpgradeable(_graphToken) {}
// -- Initialization --
/**
* @notice Initialize the IssuanceAllocator contract
* @param _governor Address that will have the GOVERNOR_ROLE
* @dev Initializes with a default target at index 0 set to address(0)
* @dev Default target will receive all unallocated issuance (initially 0 until rate is set)
* @dev lastDistributionBlock is set to block.number as a safety guard against pausing before
* configuration. lastSelfMintingBlock defaults to 0. issuancePerBlock is 0. Once
* setIssuancePerBlock() is called, it triggers _distributeIssuance() which updates
* lastDistributionBlock to current block, establishing the starting point for issuance tracking.
* @dev selfMintingEventMode is initialized to PerTarget
*/
function initialize(address _governor) external virtual initializer {
__BaseUpgradeable_init(_governor);
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
// Initialize default target at index 0 with address(0)
// Rates are 0 initially; default gets remainder when issuancePerBlock is set
$.targetAddresses.push(address(0));
$.selfMintingEventMode = SelfMintingEventMode.PerTarget;
// To guard against extreme edge case of pausing before setting issuancePerBlock, we initialize
// lastDistributionBlock to block.number. This should be updated to the correct starting block
// during configuration by governance.
$.lastDistributionBlock = block.number;
}
// -- Core Functionality --
/**
* @inheritdoc ERC165Upgradeable
* @dev Supports the four IssuanceAllocator sub-interfaces
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == type(IIssuanceAllocationDistribution).interfaceId ||
interfaceId == type(IIssuanceAllocationAdministration).interfaceId ||
interfaceId == type(IIssuanceAllocationStatus).interfaceId ||
interfaceId == type(IIssuanceAllocationData).interfaceId ||
super.supportsInterface(interfaceId);
}
/**
* @inheritdoc IIssuanceAllocationDistribution
* @dev Implementation details:
* - For allocator-minting targets, tokens are minted and transferred directly to targets based on their allocation rate
* - For self-minting targets (like the legacy RewardsManager), it does not mint tokens directly. Instead, these contracts are expected to handle minting themselves
* - The self-minting allocation is intended only for backwards compatibility with existing contracts and should not be used for new targets. New targets should use allocator-minting allocation to ensure robust control of token issuance by the IssuanceAllocator
* @dev Pause behavior:
* - When paused: Self-minting allowances tracked via events/accumulation, but no allocator-minting tokens distributed.
* Returns lastDistributionBlock (frozen at pause point). lastSelfMintingBlock advances to current block.
* - When unpaused: Normal distribution if no accumulated self-minting, otherwise retroactive distribution
* using current rates for entire undistributed period, with accumulated self-minting reducing allocator budget.
* - Unless paused, always advances lastDistributionBlock to block.number, even if no issuance to distribute.
* @dev Reentrancy: This function intentionally does NOT have a reentrancy guard to allow targets to
* legitimately call it during notifications (e.g., to claim pending issuance before their allocation changes).
* This is safe because the function has built-in block-tracking protection that prevents double-distribution
* within the same block, makes no external calls that could expose inconsistent state, and does not modify allocations.
*/
function distributeIssuance() external override returns (uint256) {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
// Optimize common case: if already distributed this block, return immediately (~60% gas savings).
// Multiple targets may call this in the same block; first call distributes, rest are no-ops.
return $.lastDistributionBlock == block.number ? block.number : _distributeIssuance();
}
/**
* @notice Advances self-minting block and emits allowance events
* @dev When paused, accumulates self-minting amounts. This accumulation reduces the allocator-minting
* budget when distribution resumes, ensuring total issuance stays within bounds.
* When not paused, emits self-minting allowance events based on selfMintingEventMode.
* Called by _distributeIssuance() which anyone can call.
* Optimized for no-op cases: very cheap when already at current block.
*/
function _advanceSelfMintingBlock() private {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
uint256 previousBlock = $.lastSelfMintingBlock;
if (previousBlock == block.number) return;
uint256 blocks = block.number - previousBlock;
uint256 fromBlock = previousBlock + 1;
// Accumulate if currently paused OR if there's existing accumulated balance.
// Once accumulation starts (during pause), continue through any unpaused periods
// until distribution clears the accumulation. This is conservative and allows
// better recovery when distribution is delayed through pause/unpause cycles.
uint256 offsetBefore = $.selfMintingOffset;
if (paused() || 0 < offsetBefore) {
$.selfMintingOffset += $.totalSelfMintingRate * blocks;
// Emit accumulation event whenever offset changes
if (offsetBefore != $.selfMintingOffset) {
emit SelfMintingOffsetAccumulated(offsetBefore, $.selfMintingOffset, fromBlock, block.number);
}
}
$.lastSelfMintingBlock = block.number;
// Emit self-minting allowance events based on mode
if (0 < $.totalSelfMintingRate) {
if ($.selfMintingEventMode == SelfMintingEventMode.PerTarget) {
// Emit per-target events (highest gas cost)
for (uint256 i = 0; i < $.targetAddresses.length; ++i) {
address target = $.targetAddresses[i];
AllocationTarget storage targetData = $.allocationTargets[target];
if (0 < targetData.selfMintingRate) {
uint256 amount = targetData.selfMintingRate * blocks;
emit IssuanceSelfMintAllowance(target, amount, fromBlock, block.number);
}
}
} else if ($.selfMintingEventMode == SelfMintingEventMode.Aggregate) {
// Emit single aggregated event (lower gas cost)
uint256 totalAmount = $.totalSelfMintingRate * blocks;
emit IssuanceSelfMintAllowanceAggregate(totalAmount, fromBlock, block.number);
}
// else None: skip event emission entirely (lowest gas cost)
}
}
/**
* @notice Internal implementation for `distributeIssuance`
* @dev Handles the actual distribution logic.
* @dev Always calls _advanceSelfMintingBlock() first (advances lastSelfMintingBlock, tracks self-minting).
* @dev If paused: Returns lastDistributionBlock without distributing allocator-minting (frozen state).
* @dev If unpaused: Chooses distribution path based on accumulated self-minting:
* - With accumulation: retroactive distribution path (current rates, reduced allocator budget)
* - Without accumulation: normal distribution path (simple per-block minting)
* @return Block number distributed to
*/
function _distributeIssuance() private returns (uint256) {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
_advanceSelfMintingBlock();
if (paused()) return $.lastDistributionBlock;
return 0 < $.selfMintingOffset ? _distributePendingIssuance(block.number) : _performNormalDistribution();
}
/**
* @notice Performs normal (non-pending) issuance distribution
* @dev Distributes allocator-minting issuance to all targets based on their rates
* @dev Assumes contract is not paused and pending issuance has already been distributed
* @return Block number distributed to
*/
function _performNormalDistribution() private returns (uint256) {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
uint256 blocks = block.number - $.lastDistributionBlock;
if (blocks == 0) return $.lastDistributionBlock;
uint256 fromBlock = $.lastDistributionBlock + 1;
for (uint256 i = 0; i < $.targetAddresses.length; ++i) {
address target = $.targetAddresses[i];
if (target == address(0)) continue;
AllocationTarget storage targetData = $.allocationTargets[target];
if (0 < targetData.allocatorMintingRate) {
uint256 amount = targetData.allocatorMintingRate * blocks;
GRAPH_TOKEN.mint(target, amount);
emit IssuanceDistributed(target, amount, fromBlock, block.number);
}
}
$.lastDistributionBlock = block.number;
return block.number;
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function distributePendingIssuance() external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (uint256) {
return _distributePendingIssuance(block.number);
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function distributePendingIssuance(
uint256 toBlockNumber
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (uint256) {
return _distributePendingIssuance(toBlockNumber);
}
/**
* @notice Internal implementation for distributing pending accumulated allocator-minting issuance
* @param toBlockNumber Block number to distribute up to
* @dev Distributes allocator-minting issuance for undistributed period using current rates,
* retroactively applied from lastDistributionBlock to toBlockNumber (inclusive).
* Called when 0 < selfMintingOffset, which occurs after pause periods or delayed distribution.
* @dev Available budget = max(0, issuancePerBlock * blocks - selfMintingOffset).
* Distribution cases:
* (1) available < allocatedTotal: proportional distribution to non-default, default gets zero
* (2) allocatedTotal <= available: full rates to non-default, remainder to default
* Where allocatedTotal is sum of non-default allocator rates * blocks.
* @return Block number that issuance was distributed up to
*/
function _distributePendingIssuance(uint256 toBlockNumber) private returns (uint256) {
_advanceSelfMintingBlock();
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
require(
$.lastDistributionBlock <= toBlockNumber && toBlockNumber <= block.number, // solhint-disable-line gas-strict-inequalities
ToBlockOutOfRange(toBlockNumber, $.lastDistributionBlock, block.number)
);
uint256 blocks = toBlockNumber - $.lastDistributionBlock;
if (blocks == 0) return toBlockNumber;
// Overflow is not possible with reasonable parameters. For example, with issuancePerBlock
// at 1e24 (1 million GRT with 18 decimals) and blocks at 1e9 (hundreds of years), the product is
// ~1e33, well below uint256 max (~1e77). Similar multiplications throughout this contract operate
// under the same range assumptions.
uint256 totalForPeriod = $.issuancePerBlock * blocks;
uint256 selfMintingOffset = $.selfMintingOffset;
uint256 available = selfMintingOffset < totalForPeriod ? totalForPeriod - selfMintingOffset : 0;
if (0 < available) {
// Calculate non-default allocated rate using the allocation invariant.
// Since totalAllocatorRate + totalSelfMintingRate == issuancePerBlock (100% invariant),
// and default target is part of totalAllocatorRate, we can derive:
// allocatedRate = issuancePerBlock - totalSelfMintingRate - defaultAllocatorRate
address defaultAddress = $.targetAddresses[0];
AllocationTarget storage defaultTarget = $.allocationTargets[defaultAddress];
uint256 allocatedRate = $.issuancePerBlock - $.totalSelfMintingRate - defaultTarget.allocatorMintingRate;
uint256 allocatedTotal = allocatedRate * blocks;
if (available < allocatedTotal) _distributePendingProportionally(available, allocatedRate, toBlockNumber);
else _distributePendingWithFullRate(blocks, available, allocatedTotal, toBlockNumber);
}
$.lastDistributionBlock = toBlockNumber;
_reconcileSelfMintingOffset(toBlockNumber, blocks, totalForPeriod, selfMintingOffset);
return toBlockNumber;
}
/**
* @notice Reconciles self-minting offset after distribution and emits event if changed
* @param toBlockNumber Block number distributed to
* @param blocks Number of blocks in the distribution period
* @param totalForPeriod Total issuance budget for the period
* @param selfMintingOffset Self-minting offset before reconciliation
* @dev Updates accumulated self-minting after distribution.
* Subtracts the period budget used (min of accumulated and totalForPeriod).
* When caught up to current block, clears all since nothing remains to distribute.
*/
function _reconcileSelfMintingOffset(
uint256 toBlockNumber,
uint256 blocks,
uint256 totalForPeriod,
uint256 selfMintingOffset
) private {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
uint256 newOffset = toBlockNumber == block.number
? 0
: (totalForPeriod < selfMintingOffset ? selfMintingOffset - totalForPeriod : 0);
// Emit reconciliation event whenever offset changes during pending distribution
if (selfMintingOffset != newOffset) {
emit SelfMintingOffsetReconciled(
selfMintingOffset,
newOffset,
totalForPeriod,
toBlockNumber - blocks + 1,
toBlockNumber
);
}
$.selfMintingOffset = newOffset;
}
/**
* @notice Distribute pending issuance with full rates to non-default targets
* @param blocks Number of blocks in the distribution period
* @param available Total available allocator-minting budget for the period
* @param allocatedTotal Total amount allocated to non-default targets at full rate
* @param toBlockNumber Block number distributing to
* @dev Sufficient budget: non-default targets get full rates, default gets remainder
*/
function _distributePendingWithFullRate(
uint256 blocks,
uint256 available,
uint256 allocatedTotal,
uint256 toBlockNumber
) internal {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
uint256 fromBlock = $.lastDistributionBlock + 1;
// Give non-default targets their full rates
for (uint256 i = 1; i < $.targetAddresses.length; ++i) {
address target = $.targetAddresses[i];
AllocationTarget storage targetData = $.allocationTargets[target];
if (0 < targetData.allocatorMintingRate) {
uint256 amount = targetData.allocatorMintingRate * blocks;
GRAPH_TOKEN.mint(target, amount);
emit IssuanceDistributed(target, amount, fromBlock, toBlockNumber);
}
}
// Default target gets remainder (may be 0 if exactly matched)
uint256 remainingForDefault = available - allocatedTotal;
if (0 < remainingForDefault) {
address defaultAddress = $.targetAddresses[0];
if (defaultAddress != address(0)) {
GRAPH_TOKEN.mint(defaultAddress, remainingForDefault);
emit IssuanceDistributed(defaultAddress, remainingForDefault, fromBlock, toBlockNumber);
}
}
}
/**
* @notice Distribute pending issuance proportionally among non-default targets
* @param available Total available allocator-minting budget for the period
* @param allocatedRate Total rate allocated to non-default targets
* @param toBlockNumber Block number distributing to
* @dev Insufficient budget: non-default targets get proportional shares, default gets zero
* @dev Proportional distribution may result in rounding loss (dust), which is acceptable
*/
function _distributePendingProportionally(
uint256 available,
uint256 allocatedRate,
uint256 toBlockNumber
) internal {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
// Defensive: prevent division by zero and handle edge cases. Should not be reachable based on
// caller logic (only called when available < allocatedTotal and both available > 0, blocks > 0).
if (allocatedRate == 0 || available == 0) return;
uint256 fromBlock = $.lastDistributionBlock + 1;
// Non-default targets get proportional shares (reduced amounts)
// Default is excluded (receives zero)
for (uint256 i = 1; i < $.targetAddresses.length; ++i) {
address target = $.targetAddresses[i];
AllocationTarget storage targetData = $.allocationTargets[target];
if (0 < targetData.allocatorMintingRate) {
// Proportional distribution using integer division causes rounding loss.
// Since Solidity division always floors (truncates toward zero), this can ONLY lose tokens,
// never over-distribute. The lost tokens (dust) remain unallocated.
// This is acceptable because:
// 1. The amount is negligible (< number of targets)
// 2. It maintains safety (never over-mint)
// 3. Alternative of tracking and distributing dust adds complexity without significant benefit
uint256 amount = (available * targetData.allocatorMintingRate) / allocatedRate;
GRAPH_TOKEN.mint(target, amount);
emit IssuanceDistributed(target, amount, fromBlock, toBlockNumber);
}
}
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function setIssuancePerBlock(
uint256 newIssuancePerBlock
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _setIssuancePerBlock(newIssuancePerBlock, block.number);
}
/**
* @inheritdoc IIssuanceAllocationAdministration
* @dev Implementation details:
* - Requires distribution to have reached at least minDistributedBlock
* - This allows configuration changes after calling distributePendingIssuance(blockNumber) while paused
* - Only the default target is notified (target rates don't change, only default target changes)
* - Target rates stay fixed; default target absorbs the change
* - Whenever the rate is changed, the updateL2MintAllowance function _must_ be called on the L1GraphTokenGateway in L1, to ensure the bridge can mint the right amount of tokens
* @dev Rate changes while paused: The new rate applies retroactively to the entire undistributed
* period when distribution resumes. Governance must exercise caution to ensure rates are applied
* to the correct block range. Use distributePendingIssuance(blockNumber) to control precisely
* which block the new rate applies from.
*/
function setIssuancePerBlock(
uint256 newIssuancePerBlock,
uint256 minDistributedBlock
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _setIssuancePerBlock(newIssuancePerBlock, minDistributedBlock);
}
/**
* @notice Internal implementation for setting issuance per block
* @param newIssuancePerBlock New issuance per block
* @param minDistributedBlock Minimum block number that distribution must have reached
* @return True if the value is applied
*/
function _setIssuancePerBlock(uint256 newIssuancePerBlock, uint256 minDistributedBlock) private returns (bool) {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
uint256 oldIssuancePerBlock = $.issuancePerBlock;
if (newIssuancePerBlock == oldIssuancePerBlock) return true;
if (_distributeIssuance() < minDistributedBlock) return false;
_notifyTarget($.targetAddresses[0]);
AllocationTarget storage defaultTarget = $.allocationTargets[$.targetAddresses[0]];
uint256 unallocated = defaultTarget.allocatorMintingRate;
require(
oldIssuancePerBlock <= newIssuancePerBlock + unallocated, // solhint-disable-line gas-strict-inequalities
InsufficientUnallocatedForRateDecrease(oldIssuancePerBlock, newIssuancePerBlock, unallocated)
);
defaultTarget.allocatorMintingRate = unallocated + newIssuancePerBlock - oldIssuancePerBlock;
$.issuancePerBlock = newIssuancePerBlock;
emit IssuancePerBlockUpdated(oldIssuancePerBlock, newIssuancePerBlock);
return true;
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function setSelfMintingEventMode(SelfMintingEventMode newMode) external onlyRole(GOVERNOR_ROLE) returns (bool) {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
SelfMintingEventMode oldMode = $.selfMintingEventMode;
if (newMode == oldMode) return true;
$.selfMintingEventMode = newMode;
emit SelfMintingEventModeUpdated(oldMode, newMode);
return true;
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function getSelfMintingEventMode() external view override returns (SelfMintingEventMode) {
return _getIssuanceAllocatorStorage().selfMintingEventMode;
}
// -- Target Management --
/**
* @notice Internal function to notify a target about an upcoming allocation change
* @dev Uses per-target lastChangeNotifiedBlock to prevent reentrancy and duplicate notifications.
*
* Will revert if the target's beforeIssuanceAllocationChange call fails.
* Use forceTargetNoChangeNotificationBlock to skip notification for malfunctioning targets.
*
* @param target Address of the target to notify
* @return True if notification was sent or already sent for this block. Always returns true for address(0) without notifying.
*/
function _notifyTarget(address target) private returns (bool) {
// Skip notification for zero address (default target when unset)
if (target == address(0)) return true;
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
AllocationTarget storage targetData = $.allocationTargets[target];
// Check-effects-interactions pattern: check if already notified this block
// solhint-disable-next-line gas-strict-inequalities
if (block.number <= targetData.lastChangeNotifiedBlock) return true;
// Effect: update the notification block before external calls
targetData.lastChangeNotifiedBlock = block.number;
// Interactions: make external call after state changes
// This will revert if the target's notification fails
IIssuanceTarget(target).beforeIssuanceAllocationChange();
return true;
}
/**
* @inheritdoc IIssuanceAllocationAdministration
* @dev Implementation details:
* - The target will be notified at most once per block to prevent reentrancy looping
* - Will revert if target notification reverts
*/
function notifyTarget(address target) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _notifyTarget(target);
}
/**
* @inheritdoc IIssuanceAllocationAdministration
* @dev Implementation details:
* - This can be used to enable notification to be sent again (by setting to a past block) or to prevent notification until a future block (by setting to current or future block)
* - Returns the block number that was set, always equal to blockNumber in current implementation
*/
function forceTargetNoChangeNotificationBlock(
address target,
uint256 blockNumber
) external override onlyRole(GOVERNOR_ROLE) returns (uint256) {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
AllocationTarget storage targetData = $.allocationTargets[target];
// Note: No bounds checking on blockNumber is intentional. Governance might need to set
// very high values in unanticipated edge cases or for recovery scenarios. Constraining
// governance flexibility is deemed unnecessary and perhaps counterproductive.
targetData.lastChangeNotifiedBlock = blockNumber;
return blockNumber;
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function setTargetAllocation(
IIssuanceTarget target,
uint256 allocatorMintingRate
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _setTargetAllocation(address(target), allocatorMintingRate, 0, block.number);
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function setTargetAllocation(
IIssuanceTarget target,
uint256 allocatorMintingRate,
uint256 selfMintingRate
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _setTargetAllocation(address(target), allocatorMintingRate, selfMintingRate, block.number);
}
/**
* @inheritdoc IIssuanceAllocationAdministration
* @dev Implementation details:
* - Requires distribution has reached at least minDistributedBlock issuance to change allocation
* - This allows configuration changes while paused by being deliberate about which block to distribute to
* - If the new allocations are the same as the current allocations, this function is a no-op
* - If both allocations are 0 and the target doesn't exist, this function is a no-op
* - If both allocations are 0 and the target exists, the target will be removed
* - If any allocation is non-zero and the target doesn't exist, the target will be added
* - Will revert if the total allocation would exceed available capacity (default target + current target allocation)
* - Will revert if attempting to add a target that doesn't support IIssuanceTarget
* @dev Self-minting targets must call getTargetIssuancePerBlock to determine their issuance and mint
* accordingly. See contract header for details on self-minting vs allocator-minting allocation.
*/
function setTargetAllocation(
IIssuanceTarget target,
uint256 allocatorMintingRate,
uint256 selfMintingRate,
uint256 minDistributedBlock
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _setTargetAllocation(address(target), allocatorMintingRate, selfMintingRate, minDistributedBlock);
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function setDefaultTarget(
address newAddress
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _setDefaultTarget(newAddress, block.number);
}
/**
* @inheritdoc IIssuanceAllocationAdministration
*/
function setDefaultTarget(
address newAddress,
uint256 minDistributedBlock
) external override onlyRole(GOVERNOR_ROLE) nonReentrant returns (bool) {
return _setDefaultTarget(newAddress, minDistributedBlock);
}
/**
* @notice Internal implementation for setting default target
* @param newAddress The address to set as the new default target
* @param minDistributedBlock Minimum block number that distribution must have reached
* @return True if the value is applied (including if already the case), false if not applied due to paused state
* @dev The default target automatically receives the portion of issuance not allocated to other targets
* @dev This maintains the invariant that total allocation always equals issuancePerBlock
* @dev Reverts if attempting to set to an address that has a normal (non-default) allocation
* @dev Allocation data is copied from the old default to the new default, including lastChangeNotifiedBlock
* @dev No-op if setting to the same address
*/
function _setDefaultTarget(address newAddress, uint256 minDistributedBlock) internal returns (bool) {
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();
address oldAddress = $.targetAddresses[0];
if (newAddress == oldAddress) return true;
// Cannot set default target to a normally allocated target
// Check if newAddress is in targetAddresses (excluding index 0 which is the default)
// Note: This is O(n) for the number of targets, which could become expensive as targets increase.
// However, distribution operations already loop through all targets and
// would encounter gas issues first. Recovery mechanisms exist.
for (uint256 i = 1; i < $.targetAddresses.length; ++i) {
require($.targetAddresses[i] != newAddress, CannotSetDefaultToAllocatedTarget(newAddress));
}
if (_distributeIssuance() < minDistributedBlock) return false;
// Notify both old and new addresses of the allocation change
_notifyTarget(oldAddress);
_notifyTarget(newAddress);
// Preserve the notification block of newAddress before copying old address data
uint256 newAddressNotificationBlock = $.allocationTargets[newAddress].lastChangeNotifiedBlock;
// Update the default target at index 0
// This copies allocation data from old to new, including allocatorMintingRate and selfMintingRate
$.targetAddresses[0] = newAddress;
$.allocationTargets[newAddress] = $.allocationTargets[oldAddress];
delete $.allocationTargets[oldAddress];
// Restore the notification block for newAddress (regard as target-specific, not about default)
$.allocationTargets[newAddress].lastChangeNotifiedBlock = newAddressNotificationBlock;
emit DefaultTargetUpdated(oldAddress, newAddress);
return true;
}
/**
* @notice Internal implementation for setting target allocation
* @param target Address of the target to update
* @param allocatorMintingRate Allocator-minting rate for the target (tokens per block)
* @param selfMintingRate Self-minting rate for the target (tokens per block)
* @param minDistributedBlock Minimum block number that distribution must have reached
* @return True if the value is applied (including if already the case), false if not applied due to paused state
*/
function _setTargetAllocation(
address target,
uint256 allocatorMintingRate,
uint256 selfMintingRate,
uint256 minDistributedBlock
) internal returns (bool) {
if (!_validateAllocationChange(target, allocatorMintingRate, selfMintingRate)) return true;
if (_distributeIssuance() < minDistributedBlock) return false;
_notifyTarget(target);
_notifyTarget(_getIssuanceAllocatorStorage().targetAddresses[0]);
// Total allocation calculation and check is delayed until after notifications.
// Distributing and notifying unnecessarily is harmless, but we need to prevent
// reentrancy from looping and changing allocations mid-calculation.
// (Would not be likely to be exploitable due to only governor being able to
// make a call to set target allocation, but better to be paranoid.)
// Validate totals and auto-adjust default allocation BEFORE updating target data
// so we can read the old allocation values
_validateAndUpdateTotalAllocations(target, allocatorMintingRate, selfMintingRate);
// Then update the target's allocation data
_updateTargetAllocationData(target, allocatorMintingRate, selfMintingRate);
emit TargetAllocationUpdated(target, allocatorMintingRate, selfMintingRate);
return true;
}
/**
* @notice Validates allocation change for a target
* @param target Address of the target to validate
* @param allocatorMintingRate Allocator-minting rate for the target (tokens per block)
* @param selfMintingRate Self-minting rate for the target (tokens per block)
* @return True if validation passes and allocation change is needed, false if allocation is already set to these values
* @dev Reverts if target is address(0), default target, or doesn't support IIssuanceTarget (for non-zero rates)
*/
function _validateAllocationChange(
address target,
uint256 allocatorMintingRate,
uint256 selfMintingRate
) private view returns (bool) {
require(target != address(0), TargetAddressCannotBeZero());
IssuanceAllocatorData storage $ = _getIssuanceAllocatorStorage();