Skip to content

Commit fed7913

Browse files
committed
feat: add Rewards Eligibility Oracle (REO)
1 parent 804da6c commit fed7913

36 files changed

+2833
-10
lines changed

packages/contracts/contracts/rewards/RewardsManager.sol

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ pragma abicoder v2;
77
// solhint-disable gas-increment-by-one, gas-indexed-events, gas-small-strings, gas-strict-inequalities
88

99
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
10+
import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol";
1011

1112
import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol";
1213
import { Managed } from "../governance/Managed.sol";
1314
import { MathUtils } from "../staking/libs/MathUtils.sol";
1415
import { IGraphToken } from "../token/IGraphToken.sol";
1516

16-
import { RewardsManagerV5Storage } from "./RewardsManagerStorage.sol";
17+
import { RewardsManagerV6Storage } from "./RewardsManagerStorage.sol";
1718
import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol";
1819
import { IRewardsIssuer } from "./IRewardsIssuer.sol";
20+
import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";
1921

2022
/**
2123
* @title Rewards Manager Contract
@@ -37,7 +39,7 @@ import { IRewardsIssuer } from "./IRewardsIssuer.sol";
3739
* until the actual takeRewards function is called.
3840
* custom:security-contact Please email security+contracts@ thegraph.com (remove space) if you find any bugs. We might have an active bug bounty program.
3941
*/
40-
contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsManager {
42+
contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IRewardsManager {
4143
using SafeMath for uint256;
4244

4345
/// @dev Fixed point scaling factor used for decimals in reward calculations
@@ -61,6 +63,14 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa
6163
*/
6264
event RewardsDenied(address indexed indexer, address indexed allocationID);
6365

66+
/**
67+
* @notice Emitted when rewards are denied to an indexer due to eligibility
68+
* @param indexer Address of the indexer being denied rewards
69+
* @param allocationID Address of the allocation being denied rewards
70+
* @param amount Amount of rewards that would have been assigned
71+
*/
72+
event RewardsDeniedDueToEligibility(address indexed indexer, address indexed allocationID, uint256 amount);
73+
6474
/**
6575
* @notice Emitted when a subgraph is denied for claiming rewards
6676
* @param subgraphDeploymentID Subgraph deployment ID being denied
@@ -75,6 +85,16 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa
7585
*/
7686
event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService);
7787

88+
/**
89+
* @notice Emitted when the rewards eligibility oracle contract is set
90+
* @param oldRewardsEligibilityOracle Previous rewards eligibility oracle address
91+
* @param newRewardsEligibilityOracle New rewards eligibility oracle address
92+
*/
93+
event RewardsEligibilityOracleSet(
94+
address indexed oldRewardsEligibilityOracle,
95+
address indexed newRewardsEligibilityOracle
96+
);
97+
7898
// -- Modifiers --
7999

80100
/**
@@ -151,6 +171,28 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa
151171
emit SubgraphServiceSet(oldSubgraphService, _subgraphService);
152172
}
153173

174+
/**
175+
* @inheritdoc IRewardsManager
176+
* @dev Note that the rewards eligibility oracle can be set to the zero address to disable use of an oracle, in
177+
* which case no indexers will be denied rewards due to eligibility.
178+
*/
179+
function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external override onlyGovernor {
180+
if (address(rewardsEligibilityOracle) != newRewardsEligibilityOracle) {
181+
// Check that the contract supports the IRewardsEligibilityOracle interface
182+
// Allow zero address to disable the oracle
183+
if (newRewardsEligibilityOracle != address(0)) {
184+
require(
185+
IERC165(newRewardsEligibilityOracle).supportsInterface(type(IRewardsEligibilityOracle).interfaceId),
186+
"Contract does not support IRewardsEligibilityOracle interface"
187+
);
188+
}
189+
190+
address oldRewardsEligibilityOracle = address(rewardsEligibilityOracle);
191+
rewardsEligibilityOracle = IRewardsEligibilityOracle(newRewardsEligibilityOracle);
192+
emit RewardsEligibilityOracleSet(oldRewardsEligibilityOracle, newRewardsEligibilityOracle);
193+
}
194+
}
195+
154196
// -- Denylist --
155197

156198
/**
@@ -404,6 +446,13 @@ contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsMa
404446
rewards = accRewardsPending.add(
405447
_calcRewards(tokens, accRewardsPerAllocatedToken, updatedAccRewardsPerAllocatedToken)
406448
);
449+
450+
// Do not reward if indexer is not eligible based on rewards eligibility
451+
if (address(rewardsEligibilityOracle) != address(0) && !rewardsEligibilityOracle.isEligible(indexer)) {
452+
emit RewardsDeniedDueToEligibility(indexer, _allocationID, rewards);
453+
return 0;
454+
}
455+
407456
if (rewards > 0) {
408457
// Mint directly to rewards issuer for the reward amount
409458
// The rewards issuer contract will do bookkeeping of the reward and

packages/contracts/contracts/rewards/RewardsManagerStorage.sol

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
pragma solidity ^0.7.6 || 0.8.27;
99

10+
import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";
1011
import { IRewardsIssuer } from "./IRewardsIssuer.sol";
1112
import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol";
1213
import { Managed } from "../governance/Managed.sol";
@@ -75,3 +76,13 @@ contract RewardsManagerV5Storage is RewardsManagerV4Storage {
7576
/// @notice Address of the subgraph service
7677
IRewardsIssuer public subgraphService;
7778
}
79+
80+
/**
81+
* @title RewardsManagerV5Storage
82+
* @author Edge & Node
83+
* @notice Storage layout for RewardsManager V6
84+
*/
85+
contract RewardsManagerV6Storage is RewardsManagerV5Storage {
86+
/// @notice Address of the rewards eligibility oracle contract
87+
IRewardsEligibilityOracle public rewardsEligibilityOracle;
88+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
// solhint-disable named-parameters-mapping
4+
5+
pragma solidity 0.7.6;
6+
7+
import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";
8+
import { ERC165 } from "@openzeppelin/contracts/introspection/ERC165.sol";
9+
10+
/**
11+
* @title MockRewardsEligibilityOracle
12+
* @author Edge & Node
13+
* @notice A simple mock contract for the RewardsEligibilityOracle interface
14+
* @dev A simple mock contract for the RewardsEligibilityOracle interface
15+
*/
16+
contract MockRewardsEligibilityOracle is IRewardsEligibilityOracle, ERC165 {
17+
/// @dev Mapping to store eligibility status for each indexer
18+
mapping(address => bool) private eligible;
19+
20+
/// @dev Mapping to track which indexers have been explicitly set
21+
mapping(address => bool) private isSet;
22+
23+
/// @dev Default response for indexers not explicitly set
24+
bool private defaultResponse;
25+
26+
/**
27+
* @notice Constructor
28+
* @param newDefaultResponse Default response for isEligible
29+
*/
30+
constructor(bool newDefaultResponse) {
31+
defaultResponse = newDefaultResponse;
32+
}
33+
34+
/**
35+
* @notice Set whether a specific indexer is eligible
36+
* @param indexer The indexer address
37+
* @param eligibility Whether the indexer is eligible
38+
*/
39+
function setIndexerEligible(address indexer, bool eligibility) external {
40+
eligible[indexer] = eligibility;
41+
isSet[indexer] = true;
42+
}
43+
44+
/**
45+
* @notice Set the default response for indexers not explicitly set
46+
* @param newDefaultResponse The default response
47+
*/
48+
function setDefaultResponse(bool newDefaultResponse) external {
49+
defaultResponse = newDefaultResponse;
50+
}
51+
52+
/**
53+
* @inheritdoc IRewardsEligibilityOracle
54+
*/
55+
function isEligible(address indexer) external view override returns (bool) {
56+
// If the indexer has been explicitly set, return that value
57+
if (isSet[indexer]) {
58+
return eligible[indexer];
59+
}
60+
61+
// Otherwise return the default response
62+
return defaultResponse;
63+
}
64+
65+
/**
66+
* @inheritdoc ERC165
67+
*/
68+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
69+
return interfaceId == type(IRewardsEligibilityOracle).interfaceId || super.supportsInterface(interfaceId);
70+
}
71+
}

packages/contracts/test/tests/unit/rewards/rewards.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ describe('Rewards', () => {
190190
})
191191
})
192192

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

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

926-
it.skip('should allow rewards when rewards eligibility oracle approves', async function () {
926+
it('should allow rewards when rewards eligibility oracle approves', async function () {
927927
// Setup rewards eligibility oracle that allows rewards for indexer1
928928
const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory(
929929
'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle',

packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ interface IRewardsManager {
4343
*/
4444
function setSubgraphService(address subgraphService) external;
4545

46+
/**
47+
* @notice Set the rewards eligibility oracle address
48+
* @param newRewardsEligibilityOracle The address of the rewards eligibility oracle
49+
*/
50+
function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external;
51+
4652
// -- Denylist --
4753

4854
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6 || ^0.8.0;
4+
5+
/**
6+
* @title IRewardsEligibilityOracle
7+
* @author Edge & Node
8+
* @notice Interface to check if an indexer is eligible to receive rewards
9+
*/
10+
interface IRewardsEligibilityOracle {
11+
/**
12+
* @notice Check if an indexer is eligible to receive rewards
13+
* @param indexer Address of the indexer
14+
* @return True if the indexer is eligible to receive rewards, false otherwise
15+
*/
16+
function isEligible(address indexer) external view returns (bool);
17+
}

packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ interface IRewardsManagerToolshed is IRewardsManager {
3636
event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService);
3737

3838
function subgraphService() external view returns (address);
39+
40+
/// @inheritdoc IRewardsManager
41+
function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external;
3942
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../.markdownlint.json"
3+
}

packages/issuance/.solcover.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports = {
2+
skipFiles: ['test/'],
3+
providerOptions: {
4+
mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect',
5+
network_id: 1337,
6+
},
7+
istanbulFolder: './test/reports/coverage',
8+
configureYulOptimizer: true,
9+
mocha: {
10+
grep: '@skip-on-coverage',
11+
invert: true,
12+
},
13+
reporter: ['html', 'lcov', 'text'],
14+
reporterOptions: {
15+
html: {
16+
directory: './test/reports/coverage/html',
17+
},
18+
},
19+
}

packages/issuance/.solhint.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["solhint:recommended", "./../../.solhint.json"]
3+
}

0 commit comments

Comments
 (0)