Skip to content

Commit 5a71fcd

Browse files
committed
docs: rewards behaviour changes summary
Functional summary of before/after reward behaviour changes between the Horizon mainnet baseline and the issuance upgrade: activation overview, reclaim system, view function semantics, and provenance.
1 parent d9cb3a0 commit 5a71fcd

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

docs/RewardsBehaviourChanges.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Rewards Behaviour Changes
2+
3+
Functional summary of how reward behaviour changed between the Horizon mainnet baseline and the current issuance upgrade.
4+
5+
## Activation Overview
6+
7+
Changes fall into two categories:
8+
9+
- **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.
10+
11+
- **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.
12+
13+
This two-phase approach allows a safe upgrade with the new infrastructure in place, while governance coordinates separate activation steps for each optional feature.
14+
15+
## Issuance Rate
16+
17+
**Before:** A single `issuancePerBlock` storage variable, set by governance via `setIssuancePerBlock()`, determined all reward issuance.
18+
19+
**After:** An optional `issuanceAllocator` contract can be set by governance. When set, the effective issuance rate comes from the allocator (which can distribute issuance across multiple targets). When unset, the legacy `issuancePerBlock` value is used as a fallback. The allocator calls `beforeIssuanceAllocationChange()` on the RewardsManager before changing rates, ensuring accumulators are snapshotted first.
20+
21+
**Activates:** Governance-gated — requires `setIssuanceAllocator()`. Until called, the legacy `issuancePerBlock` value continues to apply.
22+
23+
## Reward Conditions
24+
25+
A new `RewardsCondition` library defines typed `bytes32` identifiers for every situation where rewards cannot be distributed normally:
26+
27+
| Condition | Trigger |
28+
| ---------------------- | ---------------------------------------------------- |
29+
| `NO_SIGNAL` | Zero total curation signal globally |
30+
| `SUBGRAPH_DENIED` | Subgraph is on the denylist |
31+
| `BELOW_MINIMUM_SIGNAL` | Subgraph signal below `minimumSubgraphSignal` |
32+
| `NO_ALLOCATED_TOKENS` | Subgraph has signal but zero allocated tokens |
33+
| `INDEXER_INELIGIBLE` | Indexer fails eligibility oracle check at claim time |
34+
| `STALE_POI` | POI presented after staleness deadline |
35+
| `ZERO_POI` | POI is `bytes32(0)` |
36+
| `ALLOCATION_TOO_YOUNG` | Allocation created in the current epoch |
37+
| `CLOSE_ALLOCATION` | Allocation being closed with uncollected rewards |
38+
39+
**Activates:** Automatic on upgrade — the library and all condition checks are available immediately once the upgraded contracts are deployed.
40+
41+
## Reclaim System
42+
43+
**Before:** When rewards could not be distributed (denied subgraph, below-signal subgraph, stale POI, etc.), the tokens were silently lost -- never minted to anyone.
44+
45+
**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.
46+
47+
**Activates:** Governance-gated — requires `setReclaimAddress()` and/or `setDefaultReclaimAddress()` for each condition. Until configured, rewards are dropped (preserving legacy behaviour).
48+
49+
## Zero Global Signal
50+
51+
**Before:** Issuance during periods with zero total curation signal was silently lost.
52+
53+
**After:** Detected in `updateAccRewardsPerSignal()` and reclaimed as `NO_SIGNAL`.
54+
55+
**Activates:** Automatic on upgrade — detection is built into the accumulator update. Reclaim requires a configured address for `NO_SIGNAL`.
56+
57+
## Subgraph-Level Denial
58+
59+
**Before:** Denial was a binary gate checked only at `takeRewards()` time. When a subgraph was denied, `takeRewards()` returned 0 and emitted `RewardsDenied`. The calling AllocationManager still advanced the allocation's reward snapshot, permanently dropping those rewards.
60+
61+
**After:** Denial is handled at two levels:
62+
63+
- **RewardsManager (accumulator level):** When `onSubgraphSignalUpdate` or `onSubgraphAllocationUpdate` is called for a denied subgraph, `accRewardsForSubgraph` and `accRewardsPerAllocatedToken` freeze (stop increasing). New rewards accruing during the denial period are reclaimed immediately rather than accumulated. `setDenied()` now snapshots accumulators before changing denial state so the boundary is clean.
64+
65+
- **AllocationManager (claim level):** POI presentation for a denied subgraph is _deferred_ -- returns 0 **without advancing the allocation's snapshot**. This preserves uncollected pre-denial rewards. When the subgraph is later un-denied, those preserved rewards become claimable again.
66+
67+
**Activates:** Automatic on upgrade — the accumulator-level freeze and claim-level deferral apply immediately. Denial state itself is set via `setDenied()` (Governor or SubgraphAvailabilityOracle).
68+
69+
## Below-Minimum Signal
70+
71+
**Before:** `getAccRewardsForSubgraph()` silently excluded rewards for subgraphs below `minimumSubgraphSignal`. Those rewards were lost.
72+
73+
**After:** The same exclusion occurs, but excluded rewards are reclaimed to the `BELOW_MINIMUM_SIGNAL` address instead of being lost. Changes to `minimumSubgraphSignal` apply retroactively to all pending rewards at the next accumulator update, so governance should call `onSubgraphSignalUpdate()` on affected subgraphs before changing the threshold.
74+
75+
**Activates:** Automatic on upgrade for the reclaim path. Threshold changes via `setMinimumSubgraphSignal()` are retroactive — governance should call `onSubgraphSignalUpdate()` on affected subgraphs before changing the threshold.
76+
77+
## Zero Allocated Tokens
78+
79+
**Before:** When a subgraph had signal but no allocations, `getAccRewardsPerAllocatedToken()` returned 0 for per-token rewards. The subgraph-level accumulator still grew, but the rewards were stranded -- distributable to no one.
80+
81+
**After:** Detected as `NO_ALLOCATED_TOKENS` and reclaimed. When allocations resume, `accRewardsPerAllocatedToken` resumes from its stored value rather than resetting to zero.
82+
83+
**Activates:** Automatic on upgrade — detection is built into the accumulator update.
84+
85+
## Indexer Eligibility
86+
87+
**Before:** No per-indexer eligibility checks existed.
88+
89+
**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.
90+
91+
**Activates:** Governance-gated — requires `setRewardsEligibilityOracle()`. Until called, no eligibility checks are performed.
92+
93+
## POI Presentation (AllocationManager)
94+
95+
**Before:** A single conditional expression decided whether `takeRewards()` was called. If any condition failed (stale, zero POI, too young, altruistic), rewards were set to 0. The allocation's reward snapshot always advanced and pending rewards were always cleared, permanently dropping any undistributable rewards.
96+
97+
**After:** Three distinct paths based on the determined condition:
98+
99+
1. **Claim** (`NONE`): `takeRewards()` mints tokens, distributed to indexer and delegators. Snapshot advances.
100+
2. **Reclaim** (`STALE_POI`, `ZERO_POI`): `reclaimRewards()` mints tokens to the reclaim address. Snapshot advances and pending rewards are cleared.
101+
3. **Defer** (`ALLOCATION_TOO_YOUNG`, `SUBGRAPH_DENIED`): Returns 0 **without advancing the snapshot or clearing pending rewards**. Rewards are preserved for later collection. Accumulators are still updated via `onSubgraphAllocationUpdate()` to keep reclaim tracking current.
102+
103+
The POI presentation timestamp is now recorded immediately on entry (before condition evaluation), so the staleness clock resets regardless of reward outcome. Over-delegation force-close is skipped on the deferred path to avoid closing allocations with preserved uncollected rewards.
104+
105+
**Activates:** Automatic on upgrade — the three-path logic applies to all POI presentations immediately.
106+
107+
## Allocation Resize
108+
109+
**Before:** Resizing always accumulated pending rewards for the delta period, regardless of allocation staleness.
110+
111+
**After:** If the allocation is stale at resize time, pending rewards are reclaimed as `STALE_POI` and cleared. This prevents stale allocations from silently accumulating pending rewards through repeated resizes.
112+
113+
**Activates:** Automatic on upgrade — applies to all resize operations immediately.
114+
115+
## Allocation Close
116+
117+
**Before:** Closing an allocation advanced the snapshot and closed it. Any uncollected rewards were permanently lost.
118+
119+
**After:** Before closing, `reclaimRewards(CLOSE_ALLOCATION, allocationId)` is called to mint uncollected rewards to the reclaim address.
120+
121+
**Activates:** Automatic on upgrade — applies to all close operations immediately.
122+
123+
## Observability
124+
125+
A new `POIPresented` event is emitted on every POI presentation, including the determined `condition` as a `bytes32` field. This provides off-chain visibility into why a given presentation did or did not result in rewards, which was previously invisible.
126+
127+
**Activates:** Automatic on upgrade — emitted on every POI presentation immediately.
128+
129+
## View Functions
130+
131+
Several view functions were added or changed to expose the new reward state.
132+
133+
### Accumulator Views Freeze for Non-Claimable Subgraphs
134+
135+
The existing accumulator view functions now exclude rewards for subgraphs that are not claimable (denied, below minimum signal, or with zero allocated tokens). Previously these accumulators always grew; callers reading them as continuously-increasing counters need to account for the new freeze behaviour.
136+
137+
**`getAccRewardsForSubgraph()`** — Previously always returned a growing value regardless of subgraph state. Now returns a frozen value when the subgraph is not claimable: the internal helper `_getSubgraphRewardsState()` determines a `RewardsCondition`, and when the condition is anything other than `NONE`, new rewards are excluded from the returned total. The accumulator resumes growing when the subgraph becomes claimable again.
138+
139+
**`getAccRewardsPerAllocatedToken()`** — Derives from `getAccRewardsForSubgraph()`, so it inherits the freeze. When the subgraph is not claimable, new per-token rewards are zero because the subgraph-level delta is zero. At snapshot points the implementation zeroes `undistributedRewards` and reclaims them instead of adding them to `accRewardsPerAllocatedToken`.
140+
141+
**`getRewards()`** — Returns the claimable reward estimate for an allocation. Because it reads `getAccRewardsPerAllocatedToken()`, it now returns a frozen value for allocations on non-claimable subgraphs. Pre-existing `accRewardsPending` from prior resizes is still included. Note: indexer eligibility is _not_ checked here (only at `takeRewards()` time), so the view does not reflect eligibility-based denial.
142+
143+
**`getNewRewardsPerSignal()`** — No visible change in return value. Internally it now separates claimable from unclaimable issuance (zero-signal periods), but the public view still returns only the claimable portion. The unclaimable portion is reclaimed as `NO_SIGNAL` at the next `updateAccRewardsPerSignal()` call.
144+
145+
### New Getters on IRewardsManager
146+
147+
| Function | Returns | Purpose |
148+
| ----------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
149+
| `getIssuanceAllocator()` | `IIssuanceAllocationDistribution` | Current allocator contract (zero if unset) |
150+
| `getReclaimAddress(bytes32 reason)` | `address` | Per-condition reclaim address (zero if unconfigured) |
151+
| `getDefaultReclaimAddress()` | `address` | Fallback reclaim address |
152+
| `getRewardsEligibilityOracle()` | `IRewardsEligibility` | Current eligibility oracle (zero if unset) |
153+
| `getAllocatedIssuancePerBlock()` | `uint256` | Effective issuance rate — returns the allocator rate when set, otherwise falls back to storage. Replaces the legacy `getRewardsIssuancePerBlock()` for callers that need the protocol rate |
154+
| `getRawIssuancePerBlock()` | `uint256` | Raw storage value, ignoring the allocator. Useful for debugging allocator configuration |
155+
156+
### Changed Return Semantics
157+
158+
**`getAllocationData()`** (IRewardsIssuer, implemented by SubgraphService) now returns a sixth value, `accRewardsPending`, representing accumulated rewards from allocation resizing that have not yet been claimed. Callers that destructure the return tuple need updating.
159+
160+
**`IAllocation.State`** struct adds two fields: `accRewardsPending` (pending rewards from resize) and `createdAtEpoch` (epoch when the allocation was created). Both affect the return value of `getAllocation()`.
161+
162+
## Provenance
163+
164+
Merge commits into `main` that introduced the changes described above, in chronological order.
165+
166+
| Date | Merge | PR | Scope |
167+
| ---------- | ----------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
168+
| 2025-12-16 | `ff2f00a62` | #1265 | Eligibility oracle audit doc fixes (TRST-L-1, TRST-L-2) |
169+
| 2025-12-16 | `48be37a20` | #1267 | Issuance allocator audit fix — default allocation, `setReclaimAddress` |
170+
| 2025-12-31 | `89f1321c4` | #1272 | Issuance allocator audit fix v3 — forced reclaim, PPM-to-absolute migration |
171+
| 2026-01-08 | `3d274a4f1` | #1255 | Issuance baseline — RewardsManager extensions, eligibility interface, test suites |
172+
| 2026-01-08 | `363924149` | #1256 | Rewards Eligibility Oracle — full oracle implementation |
173+
| 2026-01-08 | `cdef9b5fd` | #1257 | Issuance Allocator — full allocator, RewardsReclaim library, allocation close reclaim |
174+
| 2026-02-17 | `ada315500` | #1279 | Rewards reclaiming (audited) — RewardsCondition rename, `setDefaultReclaimAddress`, subgraph denial accumulator handling, zero-signal reclaim, POI three-path logic, `POIPresented` event |
175+
| 2026-02-19 | `127b7ef6f` | #1280 | Issuance umbrella merge — all prior work plus stale-allocation-resize reclaim (TRST-R-1) |

0 commit comments

Comments
 (0)