|
| 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