Skip to content

Commit 6015da5

Browse files
committed
fix: merge issuance-audit fixes (TRST-R-1/R-2/R-3) into dips-issuance-merge
1 parent 78b778d commit 6015da5

File tree

14 files changed

+176
-66
lines changed

14 files changed

+176
-66
lines changed

docs/RewardAccountingSafety.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ _allocations.snapshotRewards(..., onSubgraphAllocationUpdate()); // ③ Updates
8282

8383
**Why this order matters**:
8484

85-
- Step ① with zero allocations → triggers NO_ALLOCATION reclaim for gap period
85+
- Step ① with zero allocations → triggers NO_ALLOCATED_TOKENS reclaim for gap period
8686
- Step ② creates allocation → now allocatedTokens > 0
8787
- Step ③ same block → newRewards ≈ 0, just confirms snapshot
8888

@@ -112,7 +112,7 @@ Every reward path that cannot reach an allocation has a reclaim handler:
112112
| No global signal | `updateAccRewardsPerSignal()` with signalledTokens = 0 | `NO_SIGNAL` |
113113
| Subgraph denied | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `SUBGRAPH_DENIED` |
114114
| Below minimum signal | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `BELOW_MINIMUM_SIGNAL` |
115-
| No allocations | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `NO_ALLOCATION` |
115+
| No allocations | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `NO_ALLOCATED_TOKENS` |
116116
| Indexer ineligible | `takeRewards()` | `INDEXER_INELIGIBLE` |
117117
| Stale/zero POI | `_presentPoi()` | `STALE_POI` / `ZERO_POI` |
118118
| Allocation close | `_closeAllocation()` | `CLOSE_ALLOCATION` |
@@ -124,7 +124,7 @@ Every reward path that cannot reach an allocation has a reclaim handler:
124124
| Failure Mode | How Prevented |
125125
| ---------------------------- | ------------------------------------------------------------------------------------ |
126126
| Double-mint same rewards | Snapshot updated after every claim; same-block calls return ~0 |
127-
| Rewards stuck in accumulator | NO_ALLOCATION reclaim before allocation creation |
127+
| Rewards stuck in accumulator | NO_ALLOCATED_TOKENS reclaim before allocation creation |
128128
| Gap period loss | `_getAllocationData` calls `onSubgraphAllocationUpdate` before allocation exists |
129129
| Denial-period accumulation | `accRewardsForSubgraph` tracks; `accRewardsPerAllocatedToken` frozen; diff reclaimed |
130130
| Signal change mid-period | `onSubgraphSignalUpdate` hook called before signal changes |
@@ -148,8 +148,8 @@ RewardsManager and issuers share responsibility for correct reward accounting:
148148
**Example - Subgraph Denial** (see [RewardConditions.md](./RewardConditions.md#subgraph_denied) for full details):
149149

150150
- RM: Reclaims new rewards; freezes `accRewardsPerAllocatedToken`
151-
- AM: Defers claim; preserves pre-denial rewards in allocation snapshot
152-
- After undeny: AM can claim the preserved pre-denial rewards
151+
- AM: Defers claim; preserves uncollected rewards (no snapshot update)
152+
- After undeny: AM can claim preserved uncollected rewards
153153

154154
## Issuer Requirements
155155

docs/RewardConditions.md

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ Quick reference for all reward conditions and how they are handled across Reward
44

55
## Summary Table
66

7-
| Condition | Identifier | Handled By | Action | Rewards Outcome |
8-
| ---------------------- | ----------------------------------- | ----------------- | ------------------------- | ------------------------------------- |
9-
| `NONE` | `bytes32(0)` || Normal path | Claimed by indexer |
10-
| `NO_SIGNAL` | `keccak256("NO_SIGNAL")` | RewardsManager | Reclaim | To reclaim address |
11-
| `SUBGRAPH_DENIED` | `keccak256("SUBGRAPH_DENIED")` | Both | Reclaim (RM) / Defer (AM) | New: reclaimed; Pre-denial: preserved |
12-
| `BELOW_MINIMUM_SIGNAL` | `keccak256("BELOW_MINIMUM_SIGNAL")` | RewardsManager | Reclaim | To reclaim address |
13-
| `NO_ALLOCATION` | `keccak256("NO_ALLOCATION")` | RewardsManager | Reclaim | To reclaim address |
14-
| `INDEXER_INELIGIBLE` | `keccak256("INDEXER_INELIGIBLE")` | RewardsManager | Reclaim | To reclaim address |
15-
| `STALE_POI` | `keccak256("STALE_POI")` | AllocationManager | Reclaim | To reclaim address |
16-
| `ZERO_POI` | `keccak256("ZERO_POI")` | AllocationManager | Reclaim | To reclaim address |
17-
| `ALLOCATION_TOO_YOUNG` | `keccak256("ALLOCATION_TOO_YOUNG")` | AllocationManager | Defer | Preserved for later |
18-
| `CLOSE_ALLOCATION` | `keccak256("CLOSE_ALLOCATION")` | AllocationManager | Reclaim | To reclaim address |
7+
| Condition | Identifier | Handled By | Action | Rewards Outcome |
8+
| ---------------------- | ----------------------------------- | ----------------- | ------------------------- | -------------------------------------- |
9+
| `NONE` | `bytes32(0)` || Normal path | Claimed by indexer |
10+
| `NO_SIGNAL` | `keccak256("NO_SIGNAL")` | RewardsManager | Reclaim | To reclaim address |
11+
| `SUBGRAPH_DENIED` | `keccak256("SUBGRAPH_DENIED")` | Both | Reclaim (RM) / Defer (AM) | New: reclaimed; Uncollected: preserved |
12+
| `BELOW_MINIMUM_SIGNAL` | `keccak256("BELOW_MINIMUM_SIGNAL")` | RewardsManager | Reclaim | To reclaim address |
13+
| `NO_ALLOCATED_TOKENS` | `keccak256("NO_ALLOCATED_TOKENS")` | RewardsManager | Reclaim | To reclaim address |
14+
| `INDEXER_INELIGIBLE` | `keccak256("INDEXER_INELIGIBLE")` | RewardsManager | Reclaim | To reclaim address |
15+
| `STALE_POI` | `keccak256("STALE_POI")` | AllocationManager | Reclaim | To reclaim address |
16+
| `ZERO_POI` | `keccak256("ZERO_POI")` | AllocationManager | Reclaim | To reclaim address |
17+
| `ALLOCATION_TOO_YOUNG` | `keccak256("ALLOCATION_TOO_YOUNG")` | AllocationManager | Defer | Preserved for later |
18+
| `CLOSE_ALLOCATION` | `keccak256("CLOSE_ALLOCATION")` | AllocationManager | Reclaim | To reclaim address |
1919

2020
## Reward Distribution Levels
2121

@@ -36,7 +36,7 @@ Rewards flow through three levels, with reclaim possible at each:
3636
│ ───────────────────────────────────────────────────────────────── │
3737
│ onSubgraphSignalUpdate() / onSubgraphAllocationUpdate() │
3838
│ │
39-
│ Reclaim: SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, NO_ALLOCATION
39+
│ Reclaim: SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, NO_ALLOCATED_TOKENS
4040
│ │
4141
│ Behavior: │
4242
│ - accRewardsForSubgraph only increases when claimable │
@@ -73,20 +73,20 @@ Rewards flow through three levels, with reclaim possible at each:
7373

7474
- **Trigger**: `isDenied(subgraphDeploymentId)` returns true
7575
- **Effect**: `accRewardsPerAllocatedToken` stops increasing
76-
- **Handling**: New rewards reclaimed; pre-denial rewards preserved in allocation snapshots
76+
- **Handling**: New rewards reclaimed; accumulator frozen (uncollected rewards preserved)
7777
- **Note**: If no SUBGRAPH_DENIED reclaim address AND signal < minimum, reclaims as BELOW_MINIMUM_SIGNAL instead
7878

7979
**Reward disposition by period:**
8080

8181
| Period | Disposition |
8282
| ------------- | -------------------------------------------------------- |
83-
| Pre-denial | Claimable after undeny |
83+
| Before denial | Claimable after undeny |
8484
| During denial | Reclaimed to protocol (or dropped if no reclaim address) |
8585
| Post-undeny | Claimable normally |
8686

8787
**Effect on allocations:**
8888

89-
- _Existing allocations_: Pre-denial rewards preserved; cannot claim while denied; claimable after undeny
89+
- _Existing allocations_: Uncollected rewards preserved (accumulator frozen, snapshot unchanged); cannot claim while denied; claimable after undeny
9090
- _New allocations (created while denied)_: Start with frozen baseline; only earn rewards after undeny
9191
- _POI presentation_: Indexers should continue presenting POIs to prevent staleness (returns 0 but maintains allocation health)
9292

@@ -104,7 +104,7 @@ Rewards flow through three levels, with reclaim possible at each:
104104
- **Effect**: `accRewardsPerAllocatedToken` stops increasing
105105
- **Handling**: Rewards reclaimed to configured address
106106

107-
#### NO_ALLOCATION
107+
#### NO_ALLOCATED_TOKENS
108108

109109
- **Trigger**: Subgraph has signal but zero allocated tokens
110110
- **Effect**: Rewards cannot be distributed to allocations
@@ -146,7 +146,7 @@ Conditions checked in order (first match wins):
146146

147147
- **Trigger**: `isDenied(subgraphDeploymentId)` at POI presentation
148148
- **Effect**: Cannot claim while denied
149-
- **Handling**: **Deferred** (returns 0, no snapshot update, pre-denial rewards preserved)
149+
- **Handling**: **Deferred** (returns 0, no snapshot update, uncollected rewards preserved)
150150

151151
#### CLOSE_ALLOCATION
152152

@@ -198,6 +198,33 @@ defaultReclaimAddress = treasuryAddress; // Catch-all
198198

199199
**Important**: Changes apply retroactively to all future reclaims.
200200

201+
## Parameter Changes: minimumSubgraphSignal
202+
203+
### Retroactive Application Risk
204+
205+
When `minimumSubgraphSignal` is changed via `setMinimumSubgraphSignal()`, existing subgraphs are NOT automatically updated. When subgraphs are later updated (via signal/allocation changes), the **current** threshold is applied to ALL pending rewards since their last update, regardless of historical threshold values.
206+
207+
**Impact:**
208+
209+
| Change Direction | Effect |
210+
| ------------------- | ------------------------------------------------------------------------ |
211+
| Threshold increases | Pending rewards on previously eligible subgraphs are reclaimed |
212+
| Threshold decreases | Previously ineligible subgraphs retroactively accumulate pending rewards |
213+
214+
### Required Mitigation Process
215+
216+
To prevent retroactive application to long historical periods:
217+
218+
1. **Communicate** the planned threshold change with a specific future date
219+
2. **Wait** - notice period allows participants to adjust signal if desired
220+
3. **Identify** affected subgraphs off-chain (those crossing the threshold)
221+
4. **Call** `onSubgraphSignalUpdate()` for all affected subgraphs to accumulate pending rewards under current eligibility rules
222+
5. **Execute** threshold change via `setMinimumSubgraphSignal()` (promptly after step 4, ideally same block)
223+
224+
**Responsibility:** Governance handles steps 3-5; participants may optionally adjust signal in step 2.
225+
226+
For implementation details, see NatSpec documentation on `RewardsManager.setMinimumSubgraphSignal()`.
227+
201228
## Key Behaviors
202229

203230
### Snapshot Updates

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ describe('Rewards - Calculations', () => {
402402

403403
// Prepare expected results
404404
// Option B model: accRewardsForSubgraph only tracks distributable rewards
405-
// 2 blocks before allocation = reclaimed (NO_ALLOCATION), 5 blocks after = distributable
405+
// 2 blocks before allocation = reclaimed (NO_ALLOCATED_TOKENS), 5 blocks after = distributable
406406
const expectedSubgraphRewards = toGRT('1000') // 5 blocks × 200 GRT/block
407407
const expectedRewardsAT = toGRT('0.08') // 1000 GRT / 12500 allocated tokens
408408

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function expectApproxEq(actual: BigNumber, expected: BigNumber, message: string)
3030
`${message}: difference ${diff.toString()} exceeds tolerance ${REWARDS_TOLERANCE}`,
3131
).to.be.true
3232
}
33-
const NO_ALLOCATION = utils.id('NO_ALLOCATION')
33+
const NO_ALLOCATED_TOKENS = utils.id('NO_ALLOCATED_TOKENS')
3434
const BELOW_MINIMUM_SIGNAL = utils.id('BELOW_MINIMUM_SIGNAL')
3535

3636
describe('Rewards - Reclaim Addresses', () => {
@@ -879,10 +879,10 @@ describe('Rewards - Reclaim Addresses', () => {
879879
})
880880
})
881881

882-
describe('reclaim NO_ALLOCATION - signal but no allocations', function () {
883-
it('should reclaim when signal exists but no allocations and NO_ALLOCATION address set', async function () {
884-
// Set reclaim address for NO_ALLOCATION
885-
await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATION, reclaimWallet.address)
882+
describe('reclaim NO_ALLOCATED_TOKENS - signal but no allocations', function () {
883+
it('should reclaim when signal exists but no allocations and NO_ALLOCATED_TOKENS address set', async function () {
884+
// Set reclaim address for NO_ALLOCATED_TOKENS
885+
await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATED_TOKENS, reclaimWallet.address)
886886

887887
// Create signal but NO allocation
888888
const signalled1 = toGRT('1500')

packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -449,23 +449,23 @@ describe('Rewards: Signal and Allocation Update Accounting', () => {
449449
})
450450

451451
describe('onSubgraphSignalUpdate with no allocations', function () {
452-
it('should reclaim as NO_ALLOCATION when signal exists but no allocations', async function () {
452+
it('should reclaim as NO_ALLOCATED_TOKENS when signal exists but no allocations', async function () {
453453
// Setup: only signal, no allocation
454454
await grt.connect(governor).mint(curator.address, tokensToSignal)
455455
await grt.connect(curator).approve(curation.address, tokensToSignal)
456456
await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0)
457457

458-
// Configure reclaim address for NO_ALLOCATION
459-
const NO_ALLOCATION = hre.ethers.utils.id('NO_ALLOCATION')
460-
await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATION, governor.address)
458+
// Configure reclaim address for NO_ALLOCATED_TOKENS
459+
const NO_ALLOCATED_TOKENS = hre.ethers.utils.id('NO_ALLOCATED_TOKENS')
460+
await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATED_TOKENS, governor.address)
461461

462462
// Record initial state
463463
const initialState = await rewardsManager.subgraphs(subgraphDeploymentID)
464464

465465
// Advance blocks - rewards should accumulate
466466
await helpers.mine(100)
467467

468-
// Call onSubgraphSignalUpdate - should reclaim as NO_ALLOCATION
468+
// Call onSubgraphSignalUpdate - should reclaim as NO_ALLOCATED_TOKENS
469469
const tx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID)
470470
const receipt = await tx.wait()
471471
const afterSignalUpdate = await rewardsManager.subgraphs(subgraphDeploymentID)
@@ -476,10 +476,10 @@ describe('Rewards: Signal and Allocation Update Accounting', () => {
476476
'accRewardsForSubgraph should not change when no allocations (rewards reclaimed)',
477477
)
478478

479-
// Verify reclaim event was emitted with NO_ALLOCATION reason
479+
// Verify reclaim event was emitted with NO_ALLOCATED_TOKENS reason
480480
const reclaimEvent = receipt.events?.find((e) => e.event === 'RewardsReclaimed')
481481
expect(reclaimEvent).to.not.be.undefined
482-
expect(reclaimEvent!.args![0]).to.equal(NO_ALLOCATION, 'Should reclaim with NO_ALLOCATION reason')
482+
expect(reclaimEvent!.args![0]).to.equal(NO_ALLOCATED_TOKENS, 'Should reclaim with NO_ALLOCATED_TOKENS reason')
483483
expect(reclaimEvent!.args![1]).to.be.gt(0, 'Should have reclaimed rewards')
484484
})
485485

@@ -496,8 +496,8 @@ describe('Rewards: Signal and Allocation Update Accounting', () => {
496496
await helpers.mine(100)
497497

498498
// Configure reclaim and call signal update
499-
const NO_ALLOCATION = hre.ethers.utils.id('NO_ALLOCATION')
500-
await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATION, governor.address)
499+
const NO_ALLOCATED_TOKENS = hre.ethers.utils.id('NO_ALLOCATED_TOKENS')
500+
await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATED_TOKENS, governor.address)
501501
await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID)
502502

503503
// View should remain stable (rewards reclaimed)

packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ describe('Rewards - SubgraphService', () => {
380380
// Mine blocks
381381
await helpers.mine(5)
382382

383-
// Get rewards - Option B: with no allocations, rewards are reclaimed (NO_ALLOCATION)
383+
// Get rewards - Option B: with no allocations, rewards are reclaimed (NO_ALLOCATED_TOKENS)
384384
// so both accRewardsPerAllocatedToken and accRewardsForSubgraph remain 0
385385
const [accRewardsPerAllocatedToken, accRewardsForSubgraph] =
386386
await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ describe('Rewards', () => {
348348
await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0)
349349
await curation.connect(curator2).mint(subgraphDeploymentID2, signalled2, 0)
350350

351-
// Create allocations for both subgraphs so rewards are accumulated (not reclaimed as NO_ALLOCATION)
351+
// Create allocations for both subgraphs so rewards are accumulated (not reclaimed as NO_ALLOCATED_TOKENS)
352352
await grt.connect(governor).mint(indexer1.address, tokensToStake)
353353
await grt.connect(indexer1).approve(staking.address, tokensToStake)
354354
await staking.connect(indexer1).stake(tokensToStake)
@@ -426,7 +426,7 @@ describe('Rewards', () => {
426426
const signalled1 = toGRT('1500')
427427
await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0)
428428

429-
// Create an allocation so rewards are accumulated (not reclaimed as NO_ALLOCATION)
429+
// Create an allocation so rewards are accumulated (not reclaimed as NO_ALLOCATED_TOKENS)
430430
const tokensToStake = toGRT('100000')
431431
const tokensToAllocate = toGRT('10000')
432432
await grt.connect(governor).mint(indexer1.address, tokensToStake)

0 commit comments

Comments
 (0)