Skip to content

Commit dd6c72f

Browse files
committed
feat: indexer rewards eligibility oracle
1 parent ec0c984 commit dd6c72f

File tree

11 files changed

+1574
-0
lines changed

11 files changed

+1574
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6 || ^0.8.0;
4+
5+
import { IRewardsEligibilityEvents } from "./IRewardsEligibilityEvents.sol";
6+
7+
/**
8+
* @title IRewardsEligibilityAdministration
9+
* @author Edge & Node
10+
* @notice Interface for administrative operations on rewards eligibility
11+
* @dev Functions in this interface are restricted to accounts with OPERATOR_ROLE
12+
*/
13+
interface IRewardsEligibilityAdministration is IRewardsEligibilityEvents {
14+
/**
15+
* @notice Set the eligibility period for indexers
16+
* @dev Only callable by accounts with the OPERATOR_ROLE
17+
* @param eligibilityPeriod New eligibility period in seconds
18+
* @return True if the state is as requested (eligibility period is set to the specified value)
19+
*/
20+
function setEligibilityPeriod(uint256 eligibilityPeriod) external returns (bool);
21+
22+
/**
23+
* @notice Set the oracle update timeout
24+
* @dev Only callable by accounts with the OPERATOR_ROLE
25+
* @param oracleUpdateTimeout New timeout period in seconds
26+
* @return True if the state is as requested (timeout is set to the specified value)
27+
*/
28+
function setOracleUpdateTimeout(uint256 oracleUpdateTimeout) external returns (bool);
29+
30+
/**
31+
* @notice Set eligibility validation state
32+
* @dev Only callable by accounts with the OPERATOR_ROLE
33+
* @param enabled True to enable eligibility validation, false to disable
34+
* @return True if successfully set (always the case for current code)
35+
*/
36+
function setEligibilityValidation(bool enabled) external returns (bool);
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6 || ^0.8.0;
4+
5+
/**
6+
* @title IRewardsEligibilityEvents
7+
* @author Edge & Node
8+
* @notice Shared events for rewards eligibility interfaces
9+
*/
10+
interface IRewardsEligibilityEvents {
11+
/// @notice Emitted when an oracle submits eligibility data
12+
/// @param oracle The address of the oracle that submitted the data
13+
/// @param data The eligibility data submitted by the oracle
14+
event IndexerEligibilityData(address indexed oracle, bytes data);
15+
16+
/// @notice Emitted when an indexer's eligibility is renewed by an oracle
17+
/// @param indexer The address of the indexer whose eligibility was renewed
18+
/// @param oracle The address of the oracle that renewed the indexer's eligibility
19+
event IndexerEligibilityRenewed(address indexed indexer, address indexed oracle);
20+
21+
/// @notice Emitted when the eligibility period is updated
22+
/// @param oldPeriod The previous eligibility period in seconds
23+
/// @param newPeriod The new eligibility period in seconds
24+
event EligibilityPeriodUpdated(uint256 indexed oldPeriod, uint256 indexed newPeriod);
25+
26+
/// @notice Emitted when eligibility validation is enabled or disabled
27+
/// @param enabled True if eligibility validation is enabled, false if disabled
28+
event EligibilityValidationUpdated(bool indexed enabled);
29+
30+
/// @notice Emitted when the oracle update timeout is updated
31+
/// @param oldTimeout The previous timeout period in seconds
32+
/// @param newTimeout The new timeout period in seconds
33+
event OracleUpdateTimeoutUpdated(uint256 indexed oldTimeout, uint256 indexed newTimeout);
34+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6 || ^0.8.0;
4+
5+
import { IRewardsEligibilityEvents } from "./IRewardsEligibilityEvents.sol";
6+
7+
/**
8+
* @title IRewardsEligibilityReporting
9+
* @author Edge & Node
10+
* @notice Interface for oracle reporting of indexer eligibility
11+
* @dev Functions in this interface are restricted to accounts with ORACLE_ROLE
12+
*/
13+
interface IRewardsEligibilityReporting is IRewardsEligibilityEvents {
14+
/**
15+
* @notice Renew eligibility for provided indexers to receive rewards
16+
* @param indexers Array of indexer addresses. Zero addresses are ignored.
17+
* @param data Arbitrary calldata for future extensions
18+
* @return Number of indexers whose eligibility renewal timestamp was updated
19+
*/
20+
function renewIndexerEligibility(address[] calldata indexers, bytes calldata data) external returns (uint256);
21+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
3+
pragma solidity ^0.7.6 || ^0.8.0;
4+
5+
/**
6+
* @title IRewardsEligibilityStatus
7+
* @author Edge & Node
8+
* @notice Interface for querying rewards eligibility status and configuration
9+
* @dev All functions are view-only and can be called by anyone
10+
*/
11+
interface IRewardsEligibilityStatus {
12+
/**
13+
* @notice Get the last eligibility renewal timestamp for an indexer
14+
* @param indexer Address of the indexer
15+
* @return The last eligibility renewal timestamp, or 0 if the indexer's eligibility has never been renewed
16+
*/
17+
function getEligibilityRenewalTime(address indexer) external view returns (uint256);
18+
19+
/**
20+
* @notice Get the eligibility period
21+
* @return The current eligibility period in seconds
22+
*/
23+
function getEligibilityPeriod() external view returns (uint256);
24+
25+
/**
26+
* @notice Get the oracle update timeout
27+
* @return The current oracle update timeout in seconds
28+
*/
29+
function getOracleUpdateTimeout() external view returns (uint256);
30+
31+
/**
32+
* @notice Get the last oracle update time
33+
* @return The timestamp of the last oracle update
34+
*/
35+
function getLastOracleUpdateTime() external view returns (uint256);
36+
37+
/**
38+
* @notice Get eligibility validation state
39+
* @return True if eligibility validation is enabled, false otherwise
40+
*/
41+
function getEligibilityValidation() external view returns (bool);
42+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# RewardsEligibilityOracle
2+
3+
The RewardsEligibilityOracle is a smart contract that manages indexer eligibility for receiving rewards. It implements a time-based eligibility system where indexers must be explicitly marked as eligible by authorized oracles to receive rewards.
4+
5+
## Overview
6+
7+
The contract operates on a "deny by default" principle - all indexers are initially ineligible for rewards until their eligibility is explicitly renewed by an authorized oracle. Once eligibility is renewed, indexers remain eligible for a configurable period before their eligibility expires and needs to be renewed again.
8+
9+
## Key Features
10+
11+
- **Time-based Eligibility**: Indexers are eligible for a configurable period (default: 14 days)
12+
- **Oracle-based Renewal**: Only authorized oracles can renew indexer eligibility
13+
- **Global Toggle**: Eligibility validation can be globally enabled/disabled
14+
- **Timeout Mechanism**: If oracles don't update for too long, all indexers are automatically eligible
15+
- **Role-based Access Control**: Uses hierarchical roles for governance and operations
16+
17+
## Architecture
18+
19+
### Roles
20+
21+
The contract uses four main roles:
22+
23+
- **GOVERNOR_ROLE**: Can grant/revoke operator roles and perform governance actions
24+
- **OPERATOR_ROLE**: Can configure contract parameters and manage oracle roles
25+
- **ORACLE_ROLE**: Can approve indexers for rewards
26+
- **PAUSE_ROLE**: Can pause contract operations (inherited from BaseUpgradeable)
27+
28+
### Storage
29+
30+
The contract uses ERC-7201 namespaced storage to prevent storage collisions in upgradeable contracts:
31+
32+
- `indexerEligibilityTimestamps`: Maps indexer addresses to their last eligibility timestamp
33+
- `eligibilityPeriod`: Duration (in seconds) for which eligibility lasts (default: 14 days)
34+
- `eligibilityValidationEnabled`: Global flag to enable/disable eligibility validation (default: false, to be enabled by operator when ready)
35+
- `oracleUpdateTimeout`: Timeout after which all indexers are automatically eligible (default: 7 days)
36+
- `lastOracleUpdateTime`: Timestamp of the last oracle update
37+
38+
## Core Functions
39+
40+
### Oracle Management
41+
42+
Oracle roles are managed through the standard AccessControl functions inherited from BaseUpgradeable:
43+
44+
- **`grantRole(bytes32 role, address account)`**: Grant oracle privileges to an account (OPERATOR_ROLE only)
45+
- **`revokeRole(bytes32 role, address account)`**: Revoke oracle privileges from an account (OPERATOR_ROLE only)
46+
- **`hasRole(bytes32 role, address account)`**: Check if an account has oracle privileges
47+
48+
The `ORACLE_ROLE` constant can be used as the role parameter for these functions.
49+
50+
### Configuration
51+
52+
#### `setEligibilityPeriod(uint256 eligibilityPeriod) → bool`
53+
54+
- **Access**: OPERATOR_ROLE only
55+
- **Purpose**: Set how long indexer eligibility lasts
56+
- **Parameters**: `eligibilityPeriod` - Duration in seconds
57+
- **Returns**: Always true for current implementation
58+
- **Events**: Emits `EligibilityPeriodUpdated` if value changes
59+
60+
#### `setOracleUpdateTimeout(uint256 oracleUpdateTimeout) → bool`
61+
62+
- **Access**: OPERATOR_ROLE only
63+
- **Purpose**: Set timeout after which all indexers are automatically eligible
64+
- **Parameters**: `oracleUpdateTimeout` - Timeout duration in seconds
65+
- **Returns**: Always true for current implementation
66+
- **Events**: Emits `OracleUpdateTimeoutUpdated` if value changes
67+
68+
#### `setEligibilityValidation(bool enabled) → bool`
69+
70+
- **Access**: OPERATOR_ROLE only
71+
- **Purpose**: Enable or disable eligibility validation globally
72+
- **Parameters**: `enabled` - True to enable, false to disable
73+
- **Returns**: Always true for current implementation
74+
- **Events**: Emits `EligibilityValidationUpdated` if state changes
75+
76+
### Indexer Management
77+
78+
#### `renewIndexerEligibility(address[] calldata indexers, bytes calldata data) → uint256`
79+
80+
- **Access**: ORACLE_ROLE only
81+
- **Purpose**: Renew eligibility for indexers to receive rewards
82+
- **Parameters**:
83+
- `indexers` - Array of indexer addresses (zero addresses ignored)
84+
- `data` - Arbitrary calldata for future extensions
85+
- **Returns**: Number of indexers whose eligibility renewal timestamp was updated
86+
- **Events**:
87+
- Emits `IndexerEligibilityData` with oracle and data
88+
- Emits `IndexerEligibilityRenewed` for each indexer whose eligibility was renewed
89+
- **Notes**:
90+
- Updates `lastOracleUpdateTime` to current block timestamp
91+
- Only updates timestamp if less than current block timestamp
92+
- Ignores zero addresses and duplicate updates within same block
93+
94+
### View Functions
95+
96+
#### `isEligible(address indexer) → bool`
97+
98+
- **Purpose**: Check if an indexer is eligible for rewards
99+
- **Logic**:
100+
1. If eligibility validation is disabled → return true
101+
2. If oracle timeout exceeded → return true
102+
3. Otherwise → check if indexer's eligibility is still valid
103+
- **Returns**: True if indexer is eligible, false otherwise
104+
105+
#### `getEligibilityRenewalTime(address indexer) → uint256`
106+
107+
- **Purpose**: Get the timestamp when indexer's eligibility was last renewed
108+
- **Returns**: Timestamp or 0 if eligibility was never renewed
109+
110+
#### `getEligibilityPeriod() → uint256`
111+
112+
- **Purpose**: Get the current eligibility period
113+
- **Returns**: Duration in seconds
114+
115+
#### `getOracleUpdateTimeout() → uint256`
116+
117+
- **Purpose**: Get the current oracle update timeout
118+
- **Returns**: Duration in seconds
119+
120+
#### `getLastOracleUpdateTime() → uint256`
121+
122+
- **Purpose**: Get when oracles last updated
123+
- **Returns**: Timestamp of last oracle update
124+
125+
#### `getEligibilityValidation() → bool`
126+
127+
- **Purpose**: Get eligibility validation state
128+
- **Returns**: True if enabled, false if disabled
129+
130+
## Eligibility Logic
131+
132+
An indexer is considered eligible if ANY of the following conditions are met:
133+
134+
1. **Valid eligibility** (`block.timestamp < indexerEligibilityTimestamps[indexer] + eligibilityPeriod`)
135+
2. **Oracle timeout exceeded** (`lastOracleUpdateTime + oracleUpdateTimeout < block.timestamp`)
136+
3. **Eligibility validation is disabled** (`eligibilityValidationEnabled = false`)
137+
138+
This design ensures that:
139+
140+
- The system fails open if oracles stop updating
141+
- Operators can disable eligibility validation entirely if needed
142+
- Individual indexer eligibility has time limits
143+
144+
In normal operation, the first condition is expected to be the only one that applies. The other two conditions provide fail-safes for oracle failures, or in extreme cases an operator override. For normal operational failure of oracles, the system gracefully degrades into a "allow all" mode. This mechanism is not perfect in that oracles could still be updating but allowing far fewer indexers than they should. However this is regarded as simple mechanism that is good enough to start with and provide a foundation for future improvements and decentralization.
145+
146+
While this simple model allows the criteria for providing good service to evolve over time (which is essential for the long-term health of the network), it captures sufficient information on-chain for indexers to be able to monitor their eligibility. This is important to ensure that even in the absence of other sources of information regarding observed indexer service, indexers have a good transparency about if they are being observed to be providing good service, and for how long their current approval is valid.
147+
148+
It might initially seem safer to allow indexers by default unless an oracle explicitly denies an indexer. While that might seem safer from the perspective of the RewardsEligibilityOracle in isolation, in the absence of a more sophisticated voting system it would make the system vulnerable to a single bad oracle denying many indexers. The design of deny by default is better suited to allowing redundant oracles to be working in parallel, where only one needs to be successfully detecting indexers that are providing quality service, as well as eventually allowing different oracles to have different approval criteria and/or inputs. Therefore deny by default facilitates a more resilient and open oracle system that is less vulnerable to a single points of failure, and more open to increasing decentralization over time.
149+
150+
In general to be rewarded for providing service on The Graph, there is expected to be proof provided of good operation (such as for proof of indexing). While proof should be required to receive rewards, the system is designed for participants to have confidence is being able to adequately prove good operation (and in the case of oracles, be seen by at least one observer) that is sufficient to allow the indexer to receive rewards. The oracle model is in general far more suited to collecting evidence of good operation, from multiple independent observers, rather than any observer being able to establish that an indexer is not providing good service.
151+
152+
## Events
153+
154+
```solidity
155+
event IndexerEligibilityData(address indexed oracle, bytes data);
156+
event IndexerEligibilityRenewed(address indexed indexer, address indexed oracle);
157+
event EligibilityPeriodUpdated(uint256 indexed oldPeriod, uint256 indexed newPeriod);
158+
event EligibilityValidationUpdated(bool indexed enabled);
159+
event OracleUpdateTimeoutUpdated(uint256 indexed oldTimeout, uint256 indexed newTimeout);
160+
```
161+
162+
## Default Configuration
163+
164+
- **Eligibility Period**: 14 days (1,209,600 seconds)
165+
- **Oracle Update Timeout**: 7 days (604,800 seconds)
166+
- **Eligibility Validation**: Disabled (false)
167+
- **Last Oracle Update Time**: 0 (never updated)
168+
169+
The system is deployed with reasonable defaults but can be adjusted as required. Eligibility validation is disabled by default as the expectation is to first see oracles successfully marking indexers as eligible and having suitably established eligible indexers before enabling.
170+
171+
## Usage Patterns
172+
173+
### Initial Setup
174+
175+
1. Deploy contract with Graph Token address
176+
2. Initialize with governor address
177+
3. Governor grants OPERATOR_ROLE to operational accounts
178+
4. Operators grant ORACLE_ROLE to oracle services using `grantRole(ORACLE_ROLE, oracleAddress)`
179+
5. Configure eligibility period and timeout as needed
180+
6. After demonstration of successful oracle operation and having established indexers with renewed eligibility, eligibility checking is enabled
181+
182+
### Normal Operation
183+
184+
1. Oracles periodically call `renewIndexerEligibility()` to renew eligibility for indexers
185+
2. Reward systems call `isEligible()` to check indexer eligibility
186+
3. Operators adjust parameters as needed via configuration functions
187+
4. The operation of the system is monitored and adjusted as needed
188+
189+
### Emergency Scenarios
190+
191+
- **Oracle failure**: System automatically reports all indexers as eligible after timeout
192+
- **Eligibility issues**: Operators can disable eligibility checking globally
193+
- **Parameter changes**: Operators can adjust periods and timeouts
194+
195+
## Integration
196+
197+
The contract implements four focused interfaces (`IRewardsEligibility`, `IRewardsEligibilityAdministration`, `IRewardsEligibilityReporting`, and `IRewardsEligibilityStatus`) and can be integrated with any system that needs to verify indexer eligibility status. The primary integration point is the `isEligible(address)` function which returns a simple boolean indicating eligibility.

0 commit comments

Comments
 (0)