Skip to content

Commit cdef9b5

Browse files
authored
Merge pull request #1257 from graphprotocol/issuance-allocator-3
Issuance Allocator (IA) (rebased)
2 parents 3639241 + 89f1321 commit cdef9b5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+9738
-87
lines changed

packages/contracts/contracts/rewards/RewardsManager.sol

Lines changed: 197 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/r
2020
import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationDistribution.sol";
2121
import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol";
2222
import { 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
}

packages/contracts/contracts/rewards/RewardsManagerStorage.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,14 @@ contract RewardsManagerV5Storage is RewardsManagerV4Storage {
8383
* @title RewardsManagerV6Storage
8484
* @author Edge & Node
8585
* @notice Storage layout for RewardsManager V6
86-
* Includes support for Rewards Eligibility Oracle and Issuance Allocator.
86+
* Includes support for Rewards Eligibility Oracle, Issuance Allocator, and reclaim addresses.
8787
*/
8888
contract RewardsManagerV6Storage is RewardsManagerV5Storage {
8989
/// @notice Address of the rewards eligibility oracle contract
9090
IRewardsEligibility public rewardsEligibilityOracle;
9191
/// @notice Address of the issuance allocator
9292
IIssuanceAllocationDistribution public issuanceAllocator;
93+
/// @notice Mapping of reclaim reason identifiers to reclaim addresses
94+
/// @dev Uses bytes32 for extensibility. See RewardsReclaim library for canonical reasons.
95+
mapping(bytes32 => address) public reclaimAddresses;
9396
}

packages/contracts/contracts/tests/MockIssuanceAllocator.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ contract MockIssuanceAllocator is IERC165, IIssuanceAllocationDistribution {
5858
IIssuanceTarget(target).beforeIssuanceAllocationChange();
5959
}
6060
_targetIssuance[target] = TargetIssuancePerBlock({
61-
allocatorIssuancePerBlock: allocatorIssuance,
61+
allocatorIssuanceRate: allocatorIssuance,
6262
allocatorIssuanceBlockAppliedTo: block.number,
63-
selfIssuancePerBlock: selfIssuance,
63+
selfIssuanceRate: selfIssuance,
6464
selfIssuanceBlockAppliedTo: block.number
6565
});
6666
}

packages/contracts/contracts/tests/MockSubgraphService.sol

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,28 @@ contract MockSubgraphService is IRewardsIssuer {
102102
function getSubgraphAllocatedTokens(bytes32 subgraphDeploymentId) external view override returns (uint256) {
103103
return subgraphAllocatedTokens[subgraphDeploymentId];
104104
}
105+
106+
/**
107+
* @notice Helper function to call reclaimRewards on RewardsManager for testing
108+
* @param rewardsManager Address of the RewardsManager contract
109+
* @param reason Reason identifier for reclaiming rewards
110+
* @param allocationId The allocation ID
111+
* @param contextData Additional context data for the reclaim
112+
* @return Amount of rewards reclaimed
113+
*/
114+
function callReclaimRewards(
115+
address rewardsManager,
116+
bytes32 reason,
117+
address allocationId,
118+
bytes calldata contextData
119+
) external returns (uint256) {
120+
// Call reclaimRewards on the RewardsManager
121+
// solhint-disable-next-line avoid-low-level-calls
122+
(bool success, bytes memory data) = rewardsManager.call(
123+
// solhint-disable-next-line gas-small-strings
124+
abi.encodeWithSignature("reclaimRewards(bytes32,address,bytes)", reason, allocationId, contextData)
125+
);
126+
require(success, "reclaimRewards call failed");
127+
return abi.decode(data, (uint256));
128+
}
105129
}

0 commit comments

Comments
 (0)