Skip to content

Commit aa08230

Browse files
committed
fix: reclaim pending rewards on stale allocation resize (TRST-R-1)
_resizeAllocation() now checks staleness and reclaims accumulated pending rewards instead of allowing them to accrue for allocations that are not performing. Also fixes MockRewardsManager.calcRewards and reclaimRewards to return realistic values for meaningful test coverage.
1 parent 5e31905 commit aa08230

4 files changed

Lines changed: 68 additions & 12 deletions

File tree

packages/subgraph-service/contracts/utilities/AllocationManager.sol

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ abstract contract AllocationManager is
261261
/**
262262
* @notice Resize an allocation
263263
* @dev Will lock or release tokens in the provision tracker depending on the new allocation size.
264-
* Rewards accrued but not issued before the resize will be accounted for as pending rewards.
264+
* Rewards accrued but not issued before the resize will be accounted for as pending rewards,
265+
* unless the allocation is stale, in which case pending rewards are reclaimed.
265266
* These will be paid out when the indexer presents a POI.
266267
*
267268
* Requirements:
@@ -304,6 +305,13 @@ abstract contract AllocationManager is
304305
accRewardsPerAllocatedTokenPending
305306
);
306307

308+
// If allocation is stale, reclaim pending rewards defensively.
309+
// Stale allocations are not performing, so rewards should not accumulate.
310+
if (allocation.isStale(maxPOIStaleness)) {
311+
_graphRewardsManager().reclaimRewards(RewardsCondition.STALE_POI, _allocationId);
312+
_allocations.clearPendingRewards(_allocationId);
313+
}
314+
307315
// Update total allocated tokens for the subgraph deployment
308316
if (_tokens > oldTokens) {
309317
_subgraphAllocatedTokens[allocation.subgraphDeploymentId] += (_tokens - oldTokens);

packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,29 @@ contract MockRewardsManager is IRewardsManager {
5555

5656
function reclaimRewards(bytes32, address _allocationId) external view returns (uint256) {
5757
address rewardsIssuer = msg.sender;
58-
(bool isActive, , , uint256 tokens, uint256 accRewardsPerAllocatedToken, ) = IRewardsIssuer(rewardsIssuer)
59-
.getAllocationData(_allocationId);
58+
(
59+
bool isActive,
60+
,
61+
,
62+
uint256 tokens,
63+
uint256 accRewardsPerAllocatedToken,
64+
uint256 accRewardsPending
65+
) = IRewardsIssuer(rewardsIssuer).getAllocationData(_allocationId);
6066

6167
if (!isActive) {
6268
return 0;
6369
}
6470

65-
// Calculate accumulated but unclaimed rewards
66-
uint256 accRewardsPerTokens = tokens.mulPPM(rewardsPerSignal);
67-
uint256 rewards = accRewardsPerTokens - accRewardsPerAllocatedToken;
71+
// Mirror real _calcAllocationRewards: pending + delta from current accumulator
72+
uint256 newRewards = 0;
73+
if (rewardsPerSubgraphAllocationUpdate > accRewardsPerAllocatedToken) {
74+
newRewards =
75+
((rewardsPerSubgraphAllocationUpdate - accRewardsPerAllocatedToken) * tokens) /
76+
FIXED_POINT_SCALING_FACTOR;
77+
}
6878

6979
// Note: We don't mint tokens for reclaimed rewards, they are just discarded
70-
return rewards;
80+
return accRewardsPending + newRewards;
7181
}
7282

7383
// -- Getters --
@@ -98,7 +108,9 @@ contract MockRewardsManager is IRewardsManager {
98108

99109
function getRewards(address, address) external view returns (uint256) {}
100110

101-
function calcRewards(uint256, uint256) external pure returns (uint256) {}
111+
function calcRewards(uint256 _tokens, uint256 _accRewardsPerAllocatedToken) external pure returns (uint256) {
112+
return (_accRewardsPerAllocatedToken * _tokens) / FIXED_POINT_SCALING_FACTOR;
113+
}
102114

103115
function getAllocatedIssuancePerBlock() external view returns (uint256) {}
104116

packages/subgraph-service/test/unit/subgraphService/SubgraphService.t.sol

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,8 @@ contract SubgraphServiceTest is SubgraphServiceSharedTest {
130130
IAllocation.State memory afterAllocation = subgraphService.getAllocation(_allocationId);
131131
uint256 accRewardsPerAllocatedTokenDelta = afterAllocation.accRewardsPerAllocatedToken -
132132
beforeAllocation.accRewardsPerAllocatedToken;
133-
uint256 afterAccRewardsPending = rewardsManager.calcRewards(
134-
beforeAllocation.tokens,
135-
accRewardsPerAllocatedTokenDelta
136-
);
133+
uint256 afterAccRewardsPending = beforeAllocation.accRewardsPending +
134+
rewardsManager.calcRewards(beforeAllocation.tokens, accRewardsPerAllocatedTokenDelta);
137135

138136
// check state
139137
if (_tokens > beforeAllocation.tokens) {

packages/subgraph-service/test/unit/subgraphService/allocation/resize.t.sol

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.27;
44
import { SubgraphServiceTest } from "../SubgraphService.t.sol";
55
import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-service/ISubgraphService.sol";
66
import { IAllocationManager } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol";
7+
import { IAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocation.sol";
78
import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol";
89

910
contract SubgraphServiceAllocationResizeTest is SubgraphServiceTest {
@@ -105,4 +106,41 @@ contract SubgraphServiceAllocationResizeTest is SubgraphServiceTest {
105106
);
106107
subgraphService.resizeAllocation(users.indexer, allocationId, resizeTokens);
107108
}
109+
110+
function test_SubgraphService_Allocation_Resize_StaleAllocation_ReclaimsPending(
111+
uint256 tokens,
112+
uint256 resizeTokens
113+
) public useIndexer useAllocation(tokens) {
114+
resizeTokens = bound(resizeTokens, 1, MAX_TOKENS);
115+
vm.assume(resizeTokens != tokens);
116+
117+
// Skip past MAX_POI_STALENESS to make allocation stale
118+
skip(MAX_POI_STALENESS + 1);
119+
120+
mint(users.indexer, resizeTokens);
121+
_addToProvision(users.indexer, resizeTokens);
122+
subgraphService.resizeAllocation(users.indexer, allocationId, resizeTokens);
123+
124+
// Pending rewards should be zero after stale resize
125+
IAllocation.State memory allocation = subgraphService.getAllocation(allocationId);
126+
assertEq(allocation.accRewardsPending, 0);
127+
assertEq(allocation.tokens, resizeTokens);
128+
}
129+
130+
function test_SubgraphService_Allocation_Resize_NotStale_PreservesPending(
131+
uint256 tokens,
132+
uint256 resizeTokens
133+
) public useIndexer useAllocation(tokens) {
134+
resizeTokens = bound(resizeTokens, 1, MAX_TOKENS);
135+
vm.assume(resizeTokens != tokens);
136+
137+
mint(users.indexer, resizeTokens);
138+
_addToProvision(users.indexer, resizeTokens);
139+
subgraphService.resizeAllocation(users.indexer, allocationId, resizeTokens);
140+
141+
// Pending rewards should be preserved (non-zero) for non-stale allocation
142+
IAllocation.State memory allocation = subgraphService.getAllocation(allocationId);
143+
uint256 expectedPending = rewardsManager.calcRewards(tokens, REWARDS_PER_SUBGRAPH_ALLOCATION_UPDATE);
144+
assertEq(allocation.accRewardsPending, expectedPending);
145+
}
108146
}

0 commit comments

Comments
 (0)