diff --git a/docs/IndexerRewardsUpgradeGuide.md b/docs/IndexerRewardsUpgradeGuide.md new file mode 100644 index 000000000..1e95c0a32 --- /dev/null +++ b/docs/IndexerRewardsUpgradeGuide.md @@ -0,0 +1,152 @@ +# Indexer Guide: What's Changing With the REO + Issuance Upgrade + +## What is NOT changing + +- The allocation lifecycle: open → present POIs → close. +- How and when POIs are presented, and the POI cadence. +- The staleness model (allocations still go stale if you stop presenting POIs). +- Delegation, provisions, and stake mechanics. +- **Rewards rate at launch** — issuance to indexing rewards is the same as before the upgrade. + +--- + +## What IS changing + +### 1. Issuance routing + +```mermaid +flowchart TB + subgraph Before["Before — direct issuance"] + direction LR + P1["Protocol issuance"] --> R1["Indexing rewards pool"] --> I1["Indexers & delegators"] + end + subgraph After["After — routed through the allocator"] + direction LR + P2["Protocol issuance"] --> A{"Issuance Allocator"} + A -->|100% at launch| R2["Indexing rewards pool"] + A -.->|0% — dormant / future| D["DIPs (indexing-agreements path)"] + R2 --> I2["Indexers & delegators"] + end + Before ~~~ After +``` + +Issuance now flows through a central allocator that can split rewards across multiple destinations. +At launch the effective flow is **identical** to today — 100% still reaches the indexing rewards pool. The allocator only adds the _ability_ to split issuance later; it doesn't divert anything on day one. + +**What to watch:** when Indexer Agreements are activated later, expect a communicated change to the indexing rewards rate at that time. + +### 2. Rewards Eligibility Oracle + +The protocol now takes input from an **eligibility oracle** to decide whether an indexer is eligible for rewards at claim time: + +- The oracle will be enabled at launch with a very low threshold to qualify for eligibility. +- If the REO contract stops receiving updates, it treats everyone as eligible again, so a stalled oracle can't block rewards. + +Useful links + +- +- + +### 3. Reward conditions and reclaiming + +Today, when rewards can't be paid to an indexer (stale POI, denied subgraph, etc.), those tokens are **silently dropped**, they are never minted to anyone. After this upgrade, those same rewards become **reclaimed**: minted to a protocol reclaim address instead of being dropped. + +**Important:** reclaiming does **not** take indexing rewards that were previously earned by indexers, only rewards that were already being forfeited are now captured. + +| Condition | Trigger | Scope | Rewards outcome | Notes | +| ---------------------- | ------------------------------------------------------- | ---------- | -------------------------- | --------------------------------------------------------------------- | +| `NONE` | Valid POI on a non-denied subgraph, eligible allocation | Allocation | **Collected** | | +| `NO_SIGNAL` | Zero total curation signal globally | Global | **Reclaimed** | | +| `SUBGRAPH_DENIED` | Subgraph is on the denylist | Subgraph | **Preserved** | Claimable if the subgraph is undenied (and allocation was not closed) | +| `BELOW_MINIMUM_SIGNAL` | Subgraph signal below `minimumSubgraphSignal` | Subgraph | **Reclaimed** | | +| `NO_ALLOCATED_TOKENS` | Subgraph has signal but zero allocated tokens | Subgraph | **Reclaimed** | | +| `STALE_POI` | POI presented after staleness deadline | Allocation | **Reclaimed** | | +| `ZERO_POI` | POI is `bytes32(0)` | Allocation | **Reclaimed** | | +| `ALLOCATION_TOO_YOUNG` | Allocation created in the current epoch | Allocation | **Preserved** | Claimable next epoch (assuming allocation was not closed) | +| `CLOSE_ALLOCATION` | Allocation being closed with uncollected rewards | Allocation | **Reclaimed** | | +| `INDEXER_INELIGIBLE` | Indexer fails eligibility oracle check at claim time | Indexer | **Preserved** — tx reverts | Claimable if you regain eligibility before staleness | + +**Note**: Remember that by default the indexer agent combines "present POI" with "close allocation" when closing allocations. + +### 4. POI Observability + +Every POI presentation now records the **reward condition** that applied. If a POI didn't pay what you expected, the condition tells you exactly why instead of being invisible. This is surfaced through the network subgraph: + +```graphql +{ + allocations { + id + latestPoiCondition + } +} +``` + +```json +{ + "data": { + "allocations": [ + { + "id": "0x001a42df17784fe5efcd14dcb138b91df116aa62", + "latestPoiCondition": "StalePoi" + }, + { + "id": "0x01d1522d4646b6119a2623d9b784cb9f1dfb924b", + "latestPoiCondition": "StalePoi" + }, + { + "id": "0x0869e64ed4dbc73d961c3920e34ee7a3adf82fca", + "latestPoiCondition": "StalePoi" + }, + { + "id": "0x13d53c7ddffbd00565f8ff7a0f2df0f19099075a", + "latestPoiCondition": "StalePoi" + }, + { + "id": "0x144fb38b1c1418e1e0ba3d9f39e956e7e142ae37", + "latestPoiCondition": "StalePoi" + } + ] + } +} +``` + +### 5. Eligibility enforcement revert + +If your indexer is marked **ineligible** by the REO and you present a **normal, reward-bearing POI**, the presentation **reverts** (the transaction fails) instead of paying out. + +Key points: + +- **Your rewards are not burned.** They stay pending and remain claimable if you become eligible again **before the allocation goes stale**. +- **The revert only affects reward-bearing POIs** (a valid, non-zero POI, on an old-enough allocation, on a non-denied subgraph). It does **not** block you from operating the allocation in other ways. +- **You are never locked into an allocation.** While ineligible you can still: + - **Present a zero POI** — succeeds, resets the staleness clock (keeps the allocation alive), but forfeits that period's rewards. + - **Close the allocation** — succeeds; any uncollected rewards are reclaimed (forfeited), and you exit cleanly. + +--- + +## Apendix: Reward criteria decision tree + +The protocol evaluates these criteria in order when you present a POI, the first match wins: + +```mermaid +flowchart TD + Start(["Present POI
(first match wins)"]) + Start -->|"1 · stale"| CStale["STALE_POI
Reclaimed"] + Start -->|"2 · POI is 0x0"| CZero["ZERO_POI
Reclaimed"] + Start -->|"3 · created this epoch"| CYoung["ALLOCATION_TOO_YOUNG
Preserved"] + Start -->|"4 · subgraph denied"| CDenied["SUBGRAPH_DENIED
Preserved"] + Start -->|"5 · no rewards accrued"| CFrozen["NO_SIGNAL
BELOW_MINIMUM_SIGNAL
NO_ALLOCATED_TOKENS
Preserved"] + Start -->|"6 · ineligible"| CHeld["INDEXER_INELIGIBLE
Preserved — tx reverts"] + Start -->|"7 · otherwise"| CColl["NONE
Collected"] + + classDef collected fill:#1a7f37,stroke:#0b4a1f,color:#ffffff; + classDef preserve fill:#0969da,stroke:#053a80,color:#ffffff; + classDef reclaim fill:#9a6700,stroke:#5c3d00,color:#ffffff; + classDef held fill:#cf222e,stroke:#82101c,color:#ffffff; + classDef neutral fill:#57606a,stroke:#32383f,color:#ffffff; + class CColl collected; + class CYoung,CDenied preserve; + class CStale,CZero reclaim; + class CHeld held; + class CFrozen neutral; +``` diff --git a/docs/RewardsBehaviourChanges.md b/docs/RewardsBehaviourChanges.md index 63c17c4c2..610521ebc 100644 --- a/docs/RewardsBehaviourChanges.md +++ b/docs/RewardsBehaviourChanges.md @@ -8,9 +8,16 @@ Changes fall into two categories: - **Automatic on upgrade:** New logic that activates immediately when the upgraded contracts are deployed behind their proxies. No governance action required. These include: zero-signal detection, zero-allocated-tokens reclaim, POI presentation paths (claim/reclaim/defer), allocation resize staleness check, allocation close reclaim, and the `POIPresented` event. -- **Governance-gated:** Features that require explicit governance transactions after upgrade. Until configured, the system preserves legacy behaviour (rewards are dropped, not reclaimed). These include: setting the issuance allocator, configuring reclaim addresses (per-condition and default), setting the eligibility oracle, and changing the minimum subgraph signal threshold. +- **Governance-gated:** Features that require explicit governance transactions after upgrade. In the abstract, until configured the system preserves legacy behaviour (rewards are dropped, not reclaimed). These include: setting the issuance allocator, configuring reclaim addresses (per-condition and default), setting the eligibility oracle, and changing the minimum subgraph signal threshold. -This two-phase approach allows a safe upgrade with the new infrastructure in place, while governance coordinates separate activation steps for each optional feature. +**What this deployment configures.** The governance batches executed alongside this upgrade turn several of the gated features on immediately, so their legacy behaviour does _not_ persist: + +- **Issuance allocator — set.** The RewardsManager self-mints 100% of the rate (120.73 GRT/block); the RecurringAgreementManager gets 0 issuance (DIPs dormant at launch). Effective issuance rate is unchanged from before the upgrade. +- **Default reclaim address — set** (no per-condition addresses). Reclaiming is active for **every** condition via the catch-all fallback the moment the batch executes — previously-dropped rewards are now minted to the reclaim address. +- **Eligibility oracle — wired, then enabled.** The oracle is set on the RewardsManager (and RecurringAgreementManager), and a subsequent governance transaction in the plan sets the oracle's validation flag to true; both switches are required before `INDEXER_INELIGIBLE` denials can occur. +- **Minimum subgraph signal — unchanged.** The threshold is not modified. + +The per-feature `Activates:` notes below reflect this deployment's configuration, not just the abstract gating. ## Issuance Rate @@ -44,7 +51,7 @@ A new `RewardsCondition` library defines typed `bytes32` identifiers for every s **After:** Undistributable rewards are _reclaimed_ by minting them to a configurable address. Governance can set a per-condition address via `setReclaimAddress(condition, address)` and a catch-all fallback via `setDefaultReclaimAddress(address)`. If neither is configured for a given condition, rewards are still not minted (preserving the old drop behaviour). Every reclaim emits a `RewardsReclaimed` event with the condition, amount, indexer, allocation, and subgraph. -**Activates:** Governance-gated — requires `setReclaimAddress()` and/or `setDefaultReclaimAddress()` for each condition. Until configured, rewards are dropped (preserving legacy behaviour). +**Activates:** Governance-gated — requires `setReclaimAddress()` and/or `setDefaultReclaimAddress()`. This deployment configures the **default** reclaim address (no per-condition addresses), so reclaiming is active for **every** condition via the catch-all fallback from the moment the batch executes: rewards that were previously dropped are now minted to the reclaim address. ## Zero Global Signal @@ -52,7 +59,7 @@ A new `RewardsCondition` library defines typed `bytes32` identifiers for every s **After:** Detected in `updateAccRewardsPerSignal()` and reclaimed as `NO_SIGNAL`. -**Activates:** Automatic on upgrade — detection is built into the accumulator update. Reclaim requires a configured address for `NO_SIGNAL`. +**Activates:** Automatic on upgrade — detection is built into the accumulator update. Reclaim requires a configured address for `NO_SIGNAL`, which this deployment supplies via the default reclaim address, so zero-signal issuance is reclaimed (minted) rather than dropped. ## Subgraph-Level Denial @@ -86,9 +93,16 @@ A new `RewardsCondition` library defines typed `bytes32` identifiers for every s **Before:** No per-indexer eligibility checks existed. -**After:** An optional `rewardsEligibilityOracle` can be set by governance. When set, `takeRewards()` checks `isEligible(indexer)` at claim time. If the indexer is ineligible, rewards are denied (emitting `RewardsDeniedDueToEligibility`) and reclaimed to the `INDEXER_INELIGIBLE` address. Subgraph denial takes precedence: if a subgraph is denied, eligibility is not checked. +**After:** An optional `rewardsEligibilityOracle` can be set by governance. When set, the eligibility check runs at claim time — but **only on the claim path** (`takeRewards()`, condition `NONE`): a valid, reward-bearing POI that is not stale, non-zero, old enough, and on a non-denied subgraph. Behaviour when the indexer is ineligible depends on the `revertOnIneligible` flag: + +- **`revertOnIneligible = true` (this deployment):** the POI presentation **reverts** (`"Indexer not eligible for rewards"`). Rewards are neither minted nor reclaimed — they stay pending and become collectable if the indexer regains eligibility before the allocation goes stale. +- **`revertOnIneligible = false`:** rewards are denied (emitting `RewardsDeniedDueToEligibility`) and reclaimed to the `INDEXER_INELIGIBLE` address (or the default reclaim address) — permanently forfeited. + +Subgraph denial takes precedence: if a subgraph is denied, eligibility is not checked. + +Because the check guards only the claim path, an ineligible indexer is **never locked out of the allocation**. A **zero POI** (`ZERO_POI`), a stale allocation (`STALE_POI`), and closing via `stopService` (`CLOSE_ALLOCATION`) all take reclaim/defer paths that skip the eligibility check and succeed — forfeiting the affected rewards to the reclaim address rather than reverting (and a zero POI still resets the staleness clock, keeping the allocation alive). Under `revertOnIneligible = true`, the only action ineligibility blocks is **collecting rewards via a valid POI**. -**Activates:** Governance-gated — requires `setRewardsEligibilityOracle()`. Until called, no eligibility checks are performed. +**Activates:** Governance-gated by **two** switches: `setRewardsEligibilityOracle()` on the RewardsManager, and `setEligibilityValidation(true)` on the oracle itself (it ships with validation disabled, so `isEligible` returns true for everyone until enabled). This deployment does both — the oracle is wired at upgrade and validation is enabled by a subsequent governance transaction in the plan — so eligibility enforcement becomes active. With `revertOnIneligible = true` (this deployment) that enforcement is a **revert** on the claim path, not an `INDEXER_INELIGIBLE` reclaim (see above). Note the oracle also fail-opens (returns eligible) if it receives no updates within its configured timeout. ## POI Presentation (AllocationManager)