Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions packages/contracts/contracts/rewards/RewardsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
// TODO: Re-enable and fix issues when publishing a new version
// solhint-disable gas-increment-by-one, gas-indexed-events, gas-small-strings, gas-strict-inequalities

import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";

Check warning on line 9 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @openzeppelin/contracts/math/SafeMath.sol
import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol";

Check warning on line 10 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @openzeppelin/contracts/introspection/IERC165.sol

import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol";
import { Managed } from "../governance/Managed.sol";
import { MathUtils } from "../staking/libs/MathUtils.sol";
import { IGraphToken } from "../token/IGraphToken.sol";

import { RewardsManagerV5Storage } from "./RewardsManagerStorage.sol";
import { RewardsManagerV6Storage } from "./RewardsManagerStorage.sol";
import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol";

Check warning on line 18 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol
import { IRewardsIssuer } from "./IRewardsIssuer.sol";
import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";

Check warning on line 20 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol

/**
* @title Rewards Manager Contract
Expand All @@ -37,7 +39,7 @@
* until the actual takeRewards function is called.
* custom:security-contact Please email security+contracts@ thegraph.com (remove space) if you find any bugs. We might have an active bug bounty program.
*/
contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsManager {
contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IRewardsManager {
using SafeMath for uint256;

/// @dev Fixed point scaling factor used for decimals in reward calculations
Expand All @@ -61,6 +63,14 @@
*/
event RewardsDenied(address indexed indexer, address indexed allocationID);

/**
* @notice Emitted when rewards are denied to an indexer due to eligibility
* @param indexer Address of the indexer being denied rewards
* @param allocationID Address of the allocation being denied rewards
* @param amount Amount of rewards that would have been assigned
*/
event RewardsDeniedDueToEligibility(address indexed indexer, address indexed allocationID, uint256 amount);

/**
* @notice Emitted when a subgraph is denied for claiming rewards
* @param subgraphDeploymentID Subgraph deployment ID being denied
Expand All @@ -75,6 +85,16 @@
*/
event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService);

/**
* @notice Emitted when the rewards eligibility oracle contract is set
* @param oldRewardsEligibilityOracle Previous rewards eligibility oracle address
* @param newRewardsEligibilityOracle New rewards eligibility oracle address
*/
event RewardsEligibilityOracleSet(
address indexed oldRewardsEligibilityOracle,
address indexed newRewardsEligibilityOracle
);

// -- Modifiers --

/**
Expand Down Expand Up @@ -151,6 +171,28 @@
emit SubgraphServiceSet(oldSubgraphService, _subgraphService);
}

/**
* @inheritdoc IRewardsManager
* @dev Note that the rewards eligibility oracle can be set to the zero address to disable use of an oracle, in
* which case no indexers will be denied rewards due to eligibility.
*/
function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external override onlyGovernor {
if (address(rewardsEligibilityOracle) != newRewardsEligibilityOracle) {
// Check that the contract supports the IRewardsEligibilityOracle interface
// Allow zero address to disable the oracle
if (newRewardsEligibilityOracle != address(0)) {
require(
IERC165(newRewardsEligibilityOracle).supportsInterface(type(IRewardsEligibilityOracle).interfaceId),
"Contract does not support IRewardsEligibilityOracle interface"
);
Comment on lines +184 to +187
Copy link
Preview

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message could be more descriptive by including the address that failed the interface check. Consider changing to something like 'Contract at {address} does not support IRewardsEligibilityOracle interface' to aid in debugging.

Copilot uses AI. Check for mistakes.

}

address oldRewardsEligibilityOracle = address(rewardsEligibilityOracle);
rewardsEligibilityOracle = IRewardsEligibilityOracle(newRewardsEligibilityOracle);
emit RewardsEligibilityOracleSet(oldRewardsEligibilityOracle, newRewardsEligibilityOracle);
}
}

// -- Denylist --

/**
Expand Down Expand Up @@ -404,6 +446,13 @@
rewards = accRewardsPending.add(
_calcRewards(tokens, accRewardsPerAllocatedToken, updatedAccRewardsPerAllocatedToken)
);

// Do not reward if indexer is not eligible based on rewards eligibility
if (address(rewardsEligibilityOracle) != address(0) && !rewardsEligibilityOracle.isEligible(indexer)) {
emit RewardsDeniedDueToEligibility(indexer, _allocationID, rewards);
return 0;
}

if (rewards > 0) {
// Mint directly to rewards issuer for the reward amount
// The rewards issuer contract will do bookkeeping of the reward and
Expand Down
11 changes: 11 additions & 0 deletions packages/contracts/contracts/rewards/RewardsManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

pragma solidity ^0.7.6 || 0.8.27;

import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";

Check warning on line 10 in packages/contracts/contracts/rewards/RewardsManagerStorage.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManagerStorage.sol doesn't exist in: @graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol
import { IRewardsIssuer } from "./IRewardsIssuer.sol";
import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol";

Check warning on line 12 in packages/contracts/contracts/rewards/RewardsManagerStorage.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManagerStorage.sol doesn't exist in: @graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol
import { Managed } from "../governance/Managed.sol";

/**
Expand Down Expand Up @@ -75,3 +76,13 @@
/// @notice Address of the subgraph service
IRewardsIssuer public subgraphService;
}

/**
* @title RewardsManagerV5Storage
* @author Edge & Node
* @notice Storage layout for RewardsManager V6
*/
contract RewardsManagerV6Storage is RewardsManagerV5Storage {
/// @notice Address of the rewards eligibility oracle contract
IRewardsEligibilityOracle public rewardsEligibilityOracle;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-or-later

// solhint-disable named-parameters-mapping

pragma solidity 0.7.6;

import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";

Check warning on line 7 in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol doesn't exist in: @graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol
import { ERC165 } from "@openzeppelin/contracts/introspection/ERC165.sol";

Check warning on line 8 in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol doesn't exist in: @openzeppelin/contracts/introspection/ERC165.sol

/**
* @title MockRewardsEligibilityOracle
* @author Edge & Node
* @notice A simple mock contract for the RewardsEligibilityOracle interface
* @dev A simple mock contract for the RewardsEligibilityOracle interface
*/
contract MockRewardsEligibilityOracle is IRewardsEligibilityOracle, ERC165 {
/// @dev Mapping to store eligibility status for each indexer
mapping(address => bool) private eligible;

/// @dev Mapping to track which indexers have been explicitly set
mapping(address => bool) private isSet;

/// @dev Default response for indexers not explicitly set
bool private defaultResponse;

/**
* @notice Constructor
* @param newDefaultResponse Default response for isEligible
*/
constructor(bool newDefaultResponse) {
defaultResponse = newDefaultResponse;
}

/**
* @notice Set whether a specific indexer is eligible
* @param indexer The indexer address
* @param eligibility Whether the indexer is eligible
*/
function setIndexerEligible(address indexer, bool eligibility) external {
eligible[indexer] = eligibility;
isSet[indexer] = true;
}

/**
* @notice Set the default response for indexers not explicitly set
* @param newDefaultResponse The default response
*/
function setDefaultResponse(bool newDefaultResponse) external {
defaultResponse = newDefaultResponse;
}

/**
* @inheritdoc IRewardsEligibilityOracle
*/
function isEligible(address indexer) external view override returns (bool) {
// If the indexer has been explicitly set, return that value
if (isSet[indexer]) {
return eligible[indexer];
}

// Otherwise return the default response
return defaultResponse;
}

/**
* @inheritdoc ERC165
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IRewardsEligibilityOracle).interfaceId || super.supportsInterface(interfaceId);
}
}
6 changes: 3 additions & 3 deletions packages/contracts/test/tests/unit/rewards/rewards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ describe('Rewards', () => {
})
})

describe.skip('rewards eligibility oracle', function () {
describe('rewards eligibility oracle', function () {
it('should reject setRewardsEligibilityOracle if unauthorized', async function () {
const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory(
'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle',
Expand Down Expand Up @@ -893,7 +893,7 @@ describe('Rewards', () => {
await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1)
})

it.skip('should deny rewards due to rewards eligibility oracle', async function () {
it('should deny rewards due to rewards eligibility oracle', async function () {
// Setup rewards eligibility oracle that denies rewards for indexer1
const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory(
'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle',
Expand Down Expand Up @@ -923,7 +923,7 @@ describe('Rewards', () => {
.withArgs(indexer1.address, allocationID1, expectedIndexingRewards)
})

it.skip('should allow rewards when rewards eligibility oracle approves', async function () {
it('should allow rewards when rewards eligibility oracle approves', async function () {
// Setup rewards eligibility oracle that allows rewards for indexer1
const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory(
'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ interface IRewardsManager {
*/
function setSubgraphService(address subgraphService) external;

/**
* @notice Set the rewards eligibility oracle address
* @param newRewardsEligibilityOracle The address of the rewards eligibility oracle
*/
function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external;

// -- Denylist --

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6 || ^0.8.0;

/**
* @title IRewardsEligibilityOracle
* @author Edge & Node
* @notice Interface to check if an indexer is eligible to receive rewards
*/
interface IRewardsEligibilityOracle {
/**
* @notice Check if an indexer is eligible to receive rewards
* @param indexer Address of the indexer
* @return True if the indexer is eligible to receive rewards, false otherwise
*/
function isEligible(address indexer) external view returns (bool);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ interface IRewardsManagerToolshed is IRewardsManager {
event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService);

function subgraphService() external view returns (address);

/// @inheritdoc IRewardsManager
function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external;
}
3 changes: 3 additions & 0 deletions packages/issuance/.markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../.markdownlint.json"
}
19 changes: 19 additions & 0 deletions packages/issuance/.solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
skipFiles: ['test/'],
providerOptions: {
mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect',
network_id: 1337,
},
istanbulFolder: './test/reports/coverage',
configureYulOptimizer: true,
mocha: {
grep: '@skip-on-coverage',
invert: true,
},
reporter: ['html', 'lcov', 'text'],
reporterOptions: {
html: {
directory: './test/reports/coverage/html',
},
},
}
3 changes: 3 additions & 0 deletions packages/issuance/.solhint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["solhint:recommended", "./../../.solhint.json"]
}
Loading
Loading