Skip to content

Commit 7409b4e

Browse files
committed
test(issuance): port RewardsManager eligibility and reclaim tests to Foundry
1 parent 732f9fc commit 7409b4e

8 files changed

Lines changed: 1096 additions & 1839 deletions

File tree

‎packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts‎

Lines changed: 0 additions & 700 deletions
This file was deleted.

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

Lines changed: 0 additions & 1115 deletions
This file was deleted.

‎packages/issuance/test/unit/rewards-manager/distribution.t.sol‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ contract RewardsManagerDistributionTest is RewardsManagerSharedTest {
99
event Transfer(address indexed from, address indexed to, uint256 value);
1010

1111
uint256 internal constant ISSUANCE_RATE_PERIODS = 4;
12-
/// Blocks per `helpers.mineEpoch`: the old tests set the epoch length to 10
13-
uint256 internal constant EPOCH_LENGTH = 10;
1412

1513
bytes32 internal constant SUBGRAPH_DEPLOYMENT_ID_1 = keccak256("subgraphDeploymentID1");
1614
bytes32 internal constant SUBGRAPH_DEPLOYMENT_ID_2 = keccak256("subgraphDeploymentID2");
@@ -21,9 +19,7 @@ contract RewardsManagerDistributionTest is RewardsManagerSharedTest {
2119

2220
/// Signal, then allocate one block later, so the pre-allocation block is reclaimed (NO_ALLOCATED_TOKENS).
2321
function _setupIndexerAllocation() internal {
24-
_signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL);
25-
vm.roll(block.number + 1);
26-
_allocate(allocationID1, SUBGRAPH_DEPLOYMENT_ID_1, ALLOCATION);
22+
_signalThenAllocate(SUBGRAPH_DEPLOYMENT_ID_1, allocationID1);
2723
}
2824

2925
function _setupIndexerAllocationSignalingAfter() internal {
@@ -32,16 +28,6 @@ contract RewardsManagerDistributionTest is RewardsManagerSharedTest {
3228
_signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL);
3329
}
3430

35-
/// Rewards for an allocation of `tokens` out of `allocatedTokens` on a subgraph holding all the signal.
36-
function _expectedAllocationRewards(
37-
uint256 blocks,
38-
uint256 tokens,
39-
uint256 allocatedTokens
40-
) internal pure returns (uint256) {
41-
uint256 subgraphRewards = _expectedSubgraphRewards(blocks, SIGNAL, SIGNAL);
42-
return (_expectedRewardsPerAllocatedToken(subgraphRewards, allocatedTokens) * tokens) / 1e18;
43-
}
44-
4531
/// RewardsManager half of the legacy-Staking close cases: event amount, mint to the issuer and total supply.
4632
function _assertRewardsDistributedOnClose() internal {
4733
// All the subgraph's rewards since the allocation go to it
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
import { Vm } from "forge-std/Vm.sol";
5+
import { IProviderEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IProviderEligibility.sol";
6+
7+
import { MockERC165 } from "../../../contracts/test/allocate/MockERC165.sol";
8+
import { RewardsManagerSharedTest } from "./shared.t.sol";
9+
10+
contract RewardsManagerEligibilityTest is RewardsManagerSharedTest {
11+
event ProviderEligibilityOracleSet(IProviderEligibility indexed oldOracle, IProviderEligibility indexed newOracle);
12+
event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount);
13+
event RewardsDenied(address indexed indexer, address indexed allocationID);
14+
event RewardsDeniedDueToEligibility(address indexed indexer, address indexed allocationID, uint256 amount);
15+
16+
bytes32 internal constant SUBGRAPH_DEPLOYMENT_ID_1 = keccak256("subgraphDeploymentID1");
17+
18+
address internal allocationID1 = makeAddr("allocationID1");
19+
20+
/// Old `setupIndexerAllocation` followed by `mineEpoch`: the allocation earns `EPOCH_LENGTH` blocks.
21+
function _allocateAndAccrue() internal returns (uint256 expectedRewards) {
22+
_signalThenAllocate(SUBGRAPH_DEPLOYMENT_ID_1, allocationID1);
23+
vm.roll(block.number + EPOCH_LENGTH);
24+
return _expectedAllocationRewards(EPOCH_LENGTH, ALLOCATION, ALLOCATION);
25+
}
26+
27+
function _setAvailabilityOracle() internal {
28+
vm.prank(governor);
29+
rm.setSubgraphAvailabilityOracle(governor);
30+
}
31+
32+
function _setDenied(bool deny) internal {
33+
vm.prank(governor);
34+
rm.setDenied(SUBGRAPH_DEPLOYMENT_ID_1, deny);
35+
}
36+
37+
function _findLog(Vm.Log[] memory logs, bytes32 topic) internal view returns (Vm.Log memory) {
38+
for (uint256 i = 0; i < logs.length; ++i) {
39+
if (logs[i].emitter == address(rm) && logs[i].topics[0] == topic) return logs[i];
40+
}
41+
revert("log not found");
42+
}
43+
44+
// -- rewards eligibility oracle --
45+
46+
function test_RewardsEligibilityOracle_RevertWhen_Unauthorized() public {
47+
vm.prank(indexer);
48+
vm.expectRevert(bytes("Only Controller governor"));
49+
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));
50+
}
51+
52+
function test_RewardsEligibilityOracle_SetsOracleAsGovernor() public {
53+
vm.expectEmit(address(rm));
54+
emit ProviderEligibilityOracleSet(IProviderEligibility(address(0)), IProviderEligibility(address(oracle)));
55+
vm.prank(governor);
56+
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));
57+
58+
assertEq(address(rm.getProviderEligibilityOracle()), address(oracle));
59+
}
60+
61+
function test_RewardsEligibilityOracle_AllowsSettingToZeroAddress() public {
62+
vm.prank(governor);
63+
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));
64+
65+
vm.expectEmit(address(rm));
66+
emit ProviderEligibilityOracleSet(IProviderEligibility(address(oracle)), IProviderEligibility(address(0)));
67+
vm.prank(governor);
68+
rm.setProviderEligibilityOracle(IProviderEligibility(address(0)));
69+
70+
assertEq(address(rm.getProviderEligibilityOracle()), address(0));
71+
}
72+
73+
function test_RewardsEligibilityOracle_RevertWhen_OracleIsEOA() public {
74+
// An EOA has no code, so the supportsInterface call reverts
75+
vm.prank(governor);
76+
vm.expectRevert();
77+
rm.setProviderEligibilityOracle(IProviderEligibility(indexer));
78+
}
79+
80+
function test_RewardsEligibilityOracle_RevertWhen_InterfaceNotSupported() public {
81+
MockERC165 mockERC165 = new MockERC165();
82+
83+
vm.prank(governor);
84+
vm.expectRevert(bytes("Contract does not support IProviderEligibility interface"));
85+
rm.setProviderEligibilityOracle(IProviderEligibility(address(mockERC165)));
86+
}
87+
88+
function test_RewardsEligibilityOracle_NoEventWhenSettingSameOracle() public {
89+
vm.prank(governor);
90+
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));
91+
92+
vm.recordLogs();
93+
vm.prank(governor);
94+
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));
95+
_assertNotEmitted(vm.getRecordedLogs(), ProviderEligibilityOracleSet.selector);
96+
}
97+
98+
// -- rewards eligibility in takeRewards --
99+
100+
function test_RewardsEligibilityInTakeRewards_DeniesRewardsDueToEligibilityOracle() public {
101+
_useEligibilityOracle(false);
102+
uint256 expectedRewards = _allocateAndAccrue();
103+
104+
vm.expectEmit(address(rm), 1);
105+
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
106+
_close(allocationID1);
107+
}
108+
109+
function test_RewardsEligibilityInTakeRewards_AllowsRewardsWhenOracleApproves() public {
110+
_useEligibilityOracle(true);
111+
uint256 expectedRewards = _allocateAndAccrue();
112+
113+
vm.expectEmit(address(rm), 1);
114+
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
115+
_close(allocationID1);
116+
}
117+
118+
// -- rewards eligibility oracle and denylist interaction --
119+
120+
function test_RewardsEligibilityOracleAndDenylistInteraction_PrioritizesDenylistOverOracleWhenBothDeny() public {
121+
_setAvailabilityOracle();
122+
_setDenied(true);
123+
_useEligibilityOracle(false);
124+
125+
// Created while denied: the accumulator is frozen, so the allocation has no rewards to deny
126+
_allocateAndAccrue();
127+
128+
vm.recordLogs();
129+
_close(allocationID1);
130+
Vm.Log[] memory logs = vm.getRecordedLogs();
131+
_assertNotEmitted(logs, RewardsDenied.selector);
132+
_assertNotEmitted(logs, RewardsDeniedDueToEligibility.selector);
133+
_assertNotEmitted(logs, HorizonRewardsAssigned.selector);
134+
}
135+
136+
function test_RewardsEligibilityOracleAndDenylistInteraction_ChecksOracleWhenDenylistAllowsButIndexerIneligible()
137+
public
138+
{
139+
_useEligibilityOracle(false);
140+
uint256 expectedRewards = _allocateAndAccrue();
141+
142+
vm.expectEmit(address(rm), 1);
143+
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
144+
_close(allocationID1);
145+
}
146+
147+
function test_RewardsEligibilityOracleAndDenylistInteraction_IndexerBecomesIneligibleMidAllocation() public {
148+
_useEligibilityOracle(true);
149+
uint256 expectedRewards = _allocateAndAccrue();
150+
151+
// Eligibility is checked at close time, not at creation time
152+
oracle.setIndexerEligible(indexer, false);
153+
154+
vm.expectEmit(address(rm), 1);
155+
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
156+
_close(allocationID1);
157+
}
158+
159+
function test_RewardsEligibilityOracleAndDenylistInteraction_IndexerBecomesEligibleMidAllocation() public {
160+
_useEligibilityOracle(false);
161+
uint256 expectedRewards = _allocateAndAccrue();
162+
163+
oracle.setIndexerEligible(indexer, true);
164+
165+
vm.expectEmit(address(rm), 1);
166+
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
167+
_close(allocationID1);
168+
}
169+
170+
function test_RewardsEligibilityOracleAndDenylistInteraction_DenylistAddedMidAllocation() public {
171+
_setAvailabilityOracle();
172+
_allocateAndAccrue();
173+
174+
_setDenied(true);
175+
vm.roll(block.number + 1);
176+
177+
vm.expectEmit(address(rm));
178+
emit RewardsDenied(indexer, allocationID1);
179+
_close(allocationID1);
180+
}
181+
182+
function test_RewardsEligibilityOracleAndDenylistInteraction_DenylistRemovedMidAllocation() public {
183+
_setAvailabilityOracle();
184+
_setDenied(true);
185+
186+
// Created while denied: rewards accrued while denied are reclaimed at the subgraph level
187+
_allocateAndAccrue();
188+
189+
// Undeny snapshots the accumulator, then only post-undeny rewards accrue
190+
_setDenied(false);
191+
vm.roll(block.number + EPOCH_LENGTH);
192+
193+
vm.expectEmit(address(rm));
194+
emit HorizonRewardsAssigned(
195+
indexer,
196+
allocationID1,
197+
_expectedAllocationRewards(EPOCH_LENGTH, ALLOCATION, ALLOCATION)
198+
);
199+
_close(allocationID1);
200+
}
201+
202+
function test_RewardsEligibilityOracleAndDenylistInteraction_AllowsRewardsWhenOracleIsZeroAddress() public {
203+
assertEq(address(rm.getProviderEligibilityOracle()), address(0));
204+
205+
uint256 expectedRewards = _allocateAndAccrue();
206+
207+
vm.expectEmit(address(rm), 1);
208+
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
209+
_close(allocationID1);
210+
}
211+
212+
function test_RewardsEligibilityOracleAndDenylistInteraction_RevertWhen_IneligibleAndRevertOnIneligible() public {
213+
_useEligibilityOracle(false);
214+
vm.prank(governor);
215+
rm.setRevertOnIneligible(true);
216+
217+
_allocateAndAccrue();
218+
219+
vm.expectRevert(bytes("Indexer not eligible for rewards"));
220+
issuer.close(allocationID1);
221+
}
222+
223+
function test_RewardsEligibilityOracleAndDenylistInteraction_NoRevertForEligibleWhenRevertOnIneligible() public {
224+
_useEligibilityOracle(true);
225+
vm.prank(governor);
226+
rm.setRevertOnIneligible(true);
227+
228+
uint256 expectedRewards = _allocateAndAccrue();
229+
230+
vm.expectEmit(address(rm));
231+
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
232+
_close(allocationID1);
233+
}
234+
235+
function test_RewardsEligibilityOracleAndDenylistInteraction_ReclaimsForIneligibleWhenNotRevertOnIneligible()
236+
public
237+
{
238+
_useEligibilityOracle(false);
239+
assertFalse(rm.getRevertOnIneligible());
240+
241+
uint256 expectedRewards = _allocateAndAccrue();
242+
243+
vm.expectEmit(address(rm), 1);
244+
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
245+
uint256 rewards = _close(allocationID1);
246+
assertEq(rewards, 0);
247+
}
248+
249+
function test_RewardsEligibilityOracleAndDenylistInteraction_EventStructureDiffersBetweenDenialMechanisms() public {
250+
uint256 snapshot = vm.snapshotState();
251+
252+
// Denylist denial: event WITHOUT amount. Deny after the allocation so there are rewards to deny.
253+
_setAvailabilityOracle();
254+
_allocateAndAccrue();
255+
_setDenied(true);
256+
vm.roll(block.number + 1);
257+
258+
vm.recordLogs();
259+
_close(allocationID1);
260+
Vm.Log memory denied = _findLog(vm.getRecordedLogs(), RewardsDenied.selector);
261+
assertEq(denied.topics.length, 3);
262+
assertEq(denied.topics[1], bytes32(uint256(uint160(indexer))));
263+
assertEq(denied.topics[2], bytes32(uint256(uint160(allocationID1))));
264+
assertEq(denied.data.length, 0);
265+
266+
// Reset, as the old test's fixture teardown and setup
267+
vm.revertToState(snapshot);
268+
269+
// Eligibility denial: event WITH amount, what the indexer would have received
270+
_useEligibilityOracle(false);
271+
uint256 expectedRewards = _allocateAndAccrue();
272+
273+
vm.recordLogs();
274+
_close(allocationID1);
275+
Vm.Log memory ineligible = _findLog(vm.getRecordedLogs(), RewardsDeniedDueToEligibility.selector);
276+
assertEq(ineligible.topics.length, 3);
277+
assertEq(ineligible.topics[1], bytes32(uint256(uint160(indexer))));
278+
assertEq(ineligible.topics[2], bytes32(uint256(uint160(allocationID1))));
279+
uint256 amount = abi.decode(ineligible.data, (uint256));
280+
assertGt(amount, 0);
281+
assertEq(amount, expectedRewards);
282+
}
283+
}

0 commit comments

Comments
 (0)