@@ -20,6 +20,7 @@ import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/r
2020import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationDistribution.sol " ;
2121import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol " ;
2222import { IRewardsEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibility.sol " ;
23+ import { RewardsReclaim } from "@graphprotocol/interfaces/contracts/contracts/rewards/RewardsReclaim.sol " ;
2324
2425/**
2526 * @title Rewards Manager Contract
@@ -108,6 +109,32 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I
108109 address indexed newRewardsEligibilityOracle
109110 );
110111
112+ /**
113+ * @notice Emitted when a reclaim address is set
114+ * @param reason The reclaim reason identifier
115+ * @param oldAddress Previous address
116+ * @param newAddress New address
117+ */
118+ event ReclaimAddressSet (bytes32 indexed reason , address indexed oldAddress , address indexed newAddress );
119+
120+ /**
121+ * @notice Emitted when rewards are reclaimed to a configured address
122+ * @param reason The reclaim reason identifier
123+ * @param amount Amount of rewards reclaimed
124+ * @param indexer Address of the indexer
125+ * @param allocationID Address of the allocation
126+ * @param subgraphDeploymentID Subgraph deployment ID for the allocation
127+ * @param data Additional context data for the reclaim
128+ */
129+ event RewardsReclaimed (
130+ bytes32 indexed reason ,
131+ uint256 amount ,
132+ address indexed indexer ,
133+ address indexed allocationID ,
134+ bytes32 subgraphDeploymentID ,
135+ bytes data
136+ );
137+
111138 // -- Modifiers --
112139
113140 /**
@@ -264,6 +291,26 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I
264291 }
265292 }
266293
294+ /**
295+ * @inheritdoc IRewardsManager
296+ * @dev bytes32(0) is reserved as an invalid reason to prevent accidental misconfiguration
297+ * and catch uninitialized reason identifiers.
298+ *
299+ * IMPORTANT: Changes take effect immediately and retroactively. All unclaimed rewards from
300+ * previous periods will be sent to the new reclaim address when they are eventually reclaimed,
301+ * regardless of which address was configured when the rewards were originally accrued.
302+ */
303+ function setReclaimAddress (bytes32 reason , address newAddress ) external override onlyGovernor {
304+ require (reason != bytes32 (0 ), "Cannot set reclaim address for (bytes32(0)) " );
305+
306+ address oldAddress = reclaimAddresses[reason];
307+
308+ if (oldAddress != newAddress) {
309+ reclaimAddresses[reason] = newAddress;
310+ emit ReclaimAddressSet (reason, oldAddress, newAddress);
311+ }
312+ }
313+
267314 // -- Denylist --
268315
269316 /**
@@ -297,10 +344,10 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I
297344 * @dev Gets the effective issuance per block, taking into account the IssuanceAllocator if set
298345 */
299346 function getRewardsIssuancePerBlock () public view override returns (uint256 ) {
300- if ( address (issuanceAllocator) != address ( 0 )) {
301- return issuanceAllocator. getTargetIssuancePerBlock ( address (this )).selfIssuancePerBlock;
302- }
303- return issuancePerBlock;
347+ return
348+ address (issuanceAllocator) != address (0 )
349+ ? issuanceAllocator. getTargetIssuancePerBlock ( address ( this )).selfIssuanceRate
350+ : issuancePerBlock;
304351 }
305352
306353 /**
@@ -495,58 +542,169 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I
495542 }
496543
497544 /**
498- * @inheritdoc IRewardsManager
499- * @dev This function can only be called by an authorized rewards issuer which are
500- * the staking contract (for legacy allocations), and the subgraph service (for new allocations).
501- * Mints 0 tokens if the allocation is not active.
545+ * @notice Calculate rewards for an allocation
546+ * @param rewardsIssuer Address of the rewards issuer calling the function
547+ * @param allocationID Address of the allocation
548+ * @return rewards Amount of rewards calculated
549+ * @return indexer Address of the indexer
550+ * @return subgraphDeploymentID Subgraph deployment ID
502551 */
503- function takeRewards (address _allocationID ) external override returns (uint256 ) {
504- address rewardsIssuer = msg .sender ;
505- require (
506- rewardsIssuer == address (staking ()) || rewardsIssuer == address (subgraphService),
507- "Caller must be a rewards issuer "
508- );
509-
552+ function _calcAllocationRewards (
553+ address rewardsIssuer ,
554+ address allocationID
555+ ) private returns (uint256 rewards , address indexer , bytes32 subgraphDeploymentID ) {
510556 (
511557 bool isActive ,
512- address indexer ,
513- bytes32 subgraphDeploymentID ,
558+ address _indexer ,
559+ bytes32 _subgraphDeploymentID ,
514560 uint256 tokens ,
515561 uint256 accRewardsPerAllocatedToken ,
516562 uint256 accRewardsPending
517- ) = IRewardsIssuer (rewardsIssuer).getAllocationData (_allocationID );
563+ ) = IRewardsIssuer (rewardsIssuer).getAllocationData (allocationID );
518564
519- uint256 updatedAccRewardsPerAllocatedToken = onSubgraphAllocationUpdate (subgraphDeploymentID );
565+ uint256 updatedAccRewardsPerAllocatedToken = onSubgraphAllocationUpdate (_subgraphDeploymentID );
520566
521- // Do not do rewards on denied subgraph deployments ID
522- if (isDenied (subgraphDeploymentID)) {
523- emit RewardsDenied (indexer, _allocationID);
524- return 0 ;
525- }
526-
527- uint256 rewards = 0 ;
528- if (isActive) {
529- // Calculate rewards accrued by this allocation
530- rewards = accRewardsPending.add (
567+ rewards = isActive
568+ ? accRewardsPending.add (
531569 _calcRewards (tokens, accRewardsPerAllocatedToken, updatedAccRewardsPerAllocatedToken)
532- );
570+ )
571+ : 0 ;
572+
573+ indexer = _indexer;
574+ subgraphDeploymentID = _subgraphDeploymentID;
575+ }
533576
534- // Do not reward if indexer is not eligible based on rewards eligibility
535- if (address (rewardsEligibilityOracle) != address (0 ) && ! rewardsEligibilityOracle.isEligible (indexer)) {
536- emit RewardsDeniedDueToEligibility (indexer, _allocationID, rewards);
537- return 0 ;
577+ /**
578+ * @notice Common function to reclaim rewards to a configured address
579+ * @param reason The reclaim reason identifier
580+ * @param rewards Amount of rewards to reclaim
581+ * @param indexer Address of the indexer
582+ * @param allocationID Address of the allocation
583+ * @param subgraphDeploymentID Subgraph deployment ID for the allocation
584+ * @param data Additional context data for the reclaim
585+ * @return reclaimed The amount of rewards that were reclaimed (0 if no reclaim address set)
586+ */
587+ function _reclaimRewards (
588+ bytes32 reason ,
589+ uint256 rewards ,
590+ address indexer ,
591+ address allocationID ,
592+ bytes32 subgraphDeploymentID ,
593+ bytes memory data
594+ ) private returns (uint256 reclaimed ) {
595+ address target = reclaimAddresses[reason];
596+ if (0 < rewards && target != address (0 )) {
597+ graphToken ().mint (target, rewards);
598+ emit RewardsReclaimed (reason, rewards, indexer, allocationID, subgraphDeploymentID, data);
599+ reclaimed = rewards;
600+ }
601+ }
602+
603+ /**
604+ * @notice Check if rewards should be denied and attempt to reclaim them
605+ * @param rewards Amount of rewards to check
606+ * @param indexer Address of the indexer
607+ * @param allocationID Address of the allocation
608+ * @param subgraphDeploymentID Subgraph deployment ID for the allocation
609+ * @return denied True if rewards should be denied (either reclaimed or dropped), false if they should be minted
610+ * @dev First successful reclaim wins - checks performed in order with short-circuit on reclaim:
611+ * 1. Subgraph deny list: emit RewardsDenied. If reclaim address set → reclaim and return (STOP, eligibility not checked)
612+ * 2. Indexer eligibility: Checked if subgraph not denied OR denied without reclaim address. Emit RewardsDeniedDueToEligibility. If reclaim address set → reclaim and return
613+ * Multiple denial events may be emitted only when multiple checks fail without reclaim addresses configured.
614+ * Any failing check without a reclaim address still denies rewards (drops them without minting).
615+ */
616+ function _deniedRewards (
617+ uint256 rewards ,
618+ address indexer ,
619+ address allocationID ,
620+ bytes32 subgraphDeploymentID
621+ ) private returns (bool denied ) {
622+ if (isDenied (subgraphDeploymentID)) {
623+ emit RewardsDenied (indexer, allocationID);
624+ if (
625+ 0 <
626+ _reclaimRewards (
627+ RewardsReclaim.SUBGRAPH_DENIED,
628+ rewards,
629+ indexer,
630+ allocationID,
631+ subgraphDeploymentID,
632+ ""
633+ )
634+ ) {
635+ return true ; // Successfully reclaimed, deny rewards
538636 }
637+ denied = true ; // Denied but no reclaim address
638+ }
539639
540- if (rewards > 0 ) {
541- // Mint directly to rewards issuer for the reward amount
542- // The rewards issuer contract will do bookkeeping of the reward and
543- // assign in proportion to each stakeholder incentive
544- graphToken ().mint (rewardsIssuer, rewards);
640+ if (address (rewardsEligibilityOracle) != address (0 ) && ! rewardsEligibilityOracle.isEligible (indexer)) {
641+ emit RewardsDeniedDueToEligibility (indexer, allocationID, rewards);
642+ if (
643+ 0 <
644+ _reclaimRewards (
645+ RewardsReclaim.INDEXER_INELIGIBLE,
646+ rewards,
647+ indexer,
648+ allocationID,
649+ subgraphDeploymentID,
650+ ""
651+ )
652+ ) {
653+ return true ; // Successfully reclaimed, deny rewards
545654 }
655+ denied = true ; // Denied but no reclaim address
546656 }
657+ }
658+
659+ /**
660+ * @inheritdoc IRewardsManager
661+ * @dev This function can only be called by an authorized rewards issuer which are
662+ * the staking contract (for legacy allocations), and the subgraph service (for new allocations).
663+ * Mints 0 tokens if the allocation is not active.
664+ * @dev First successful reclaim wins - short-circuits on reclaim:
665+ * - If subgraph denied with reclaim address → reclaim to SUBGRAPH_DENIED address (eligibility NOT checked)
666+ * - If subgraph not denied OR denied without address, then check eligibility → reclaim to INDEXER_INELIGIBLE if configured
667+ * - Subsequent denial emitted only when earlier denial has no reclaim address
668+ * - Any denial without reclaim address drops rewards (no minting)
669+ */
670+ function takeRewards (address _allocationID ) external override returns (uint256 ) {
671+ address rewardsIssuer = msg .sender ;
672+ require (
673+ rewardsIssuer == address (staking ()) || rewardsIssuer == address (subgraphService),
674+ "Caller must be a rewards issuer "
675+ );
676+
677+ (uint256 rewards , address indexer , bytes32 subgraphDeploymentID ) = _calcAllocationRewards (
678+ rewardsIssuer,
679+ _allocationID
680+ );
681+
682+ if (rewards == 0 ) return 0 ;
683+ if (_deniedRewards (rewards, indexer, _allocationID, subgraphDeploymentID)) return 0 ;
547684
685+ graphToken ().mint (rewardsIssuer, rewards);
548686 emit HorizonRewardsAssigned (indexer, _allocationID, rewards);
549687
550688 return rewards;
551689 }
690+
691+ /**
692+ * @inheritdoc IRewardsManager
693+ * @dev bytes32(0) as a reason is reserved as a no-op and will not be reclaimed.
694+ */
695+ function reclaimRewards (
696+ bytes32 reason ,
697+ address allocationID ,
698+ bytes calldata data
699+ ) external override returns (uint256 ) {
700+ address rewardsIssuer = msg .sender ;
701+ require (rewardsIssuer == address (subgraphService), "Not a rewards issuer " );
702+
703+ (uint256 rewards , address indexer , bytes32 subgraphDeploymentID ) = _calcAllocationRewards (
704+ rewardsIssuer,
705+ allocationID
706+ );
707+
708+ return _reclaimRewards (reason, rewards, indexer, allocationID, subgraphDeploymentID, data);
709+ }
552710}
0 commit comments