Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

1,115 changes: 0 additions & 1,115 deletions packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts

This file was deleted.

16 changes: 1 addition & 15 deletions packages/issuance/test/unit/rewards-manager/distribution.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ contract RewardsManagerDistributionTest is RewardsManagerSharedTest {
event Transfer(address indexed from, address indexed to, uint256 value);

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

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

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

function _setupIndexerAllocationSignalingAfter() internal {
Expand All @@ -32,16 +28,6 @@ contract RewardsManagerDistributionTest is RewardsManagerSharedTest {
_signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL);
}

/// Rewards for an allocation of `tokens` out of `allocatedTokens` on a subgraph holding all the signal.
function _expectedAllocationRewards(
uint256 blocks,
uint256 tokens,
uint256 allocatedTokens
) internal pure returns (uint256) {
uint256 subgraphRewards = _expectedSubgraphRewards(blocks, SIGNAL, SIGNAL);
return (_expectedRewardsPerAllocatedToken(subgraphRewards, allocatedTokens) * tokens) / 1e18;
}

/// RewardsManager half of the legacy-Staking close cases: event amount, mint to the issuer and total supply.
function _assertRewardsDistributedOnClose() internal {
// All the subgraph's rewards since the allocation go to it
Expand Down
283 changes: 283 additions & 0 deletions packages/issuance/test/unit/rewards-manager/eligibility.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import { Vm } from "forge-std/Vm.sol";
import { IProviderEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IProviderEligibility.sol";

import { MockERC165 } from "../../../contracts/test/allocate/MockERC165.sol";
import { RewardsManagerSharedTest } from "./shared.t.sol";

contract RewardsManagerEligibilityTest is RewardsManagerSharedTest {
event ProviderEligibilityOracleSet(IProviderEligibility indexed oldOracle, IProviderEligibility indexed newOracle);
event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount);
event RewardsDenied(address indexed indexer, address indexed allocationID);
event RewardsDeniedDueToEligibility(address indexed indexer, address indexed allocationID, uint256 amount);

bytes32 internal constant SUBGRAPH_DEPLOYMENT_ID_1 = keccak256("subgraphDeploymentID1");

address internal allocationID1 = makeAddr("allocationID1");

/// Old `setupIndexerAllocation` followed by `mineEpoch`: the allocation earns `EPOCH_LENGTH` blocks.
function _allocateAndAccrue() internal returns (uint256 expectedRewards) {
_signalThenAllocate(SUBGRAPH_DEPLOYMENT_ID_1, allocationID1);
vm.roll(block.number + EPOCH_LENGTH);
return _expectedAllocationRewards(EPOCH_LENGTH, ALLOCATION, ALLOCATION);
}

function _setAvailabilityOracle() internal {
vm.prank(governor);
rm.setSubgraphAvailabilityOracle(governor);
}

function _setDenied(bool deny) internal {
vm.prank(governor);
rm.setDenied(SUBGRAPH_DEPLOYMENT_ID_1, deny);
}

function _findLog(Vm.Log[] memory logs, bytes32 topic) internal view returns (Vm.Log memory) {
for (uint256 i = 0; i < logs.length; ++i) {
if (logs[i].emitter == address(rm) && logs[i].topics[0] == topic) return logs[i];
}
revert("log not found");
}

// -- rewards eligibility oracle --

function test_RewardsEligibilityOracle_RevertWhen_Unauthorized() public {
vm.prank(indexer);
vm.expectRevert(bytes("Only Controller governor"));
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));
}

function test_RewardsEligibilityOracle_SetsOracleAsGovernor() public {
vm.expectEmit(address(rm));
emit ProviderEligibilityOracleSet(IProviderEligibility(address(0)), IProviderEligibility(address(oracle)));
vm.prank(governor);
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));

assertEq(address(rm.getProviderEligibilityOracle()), address(oracle));
}

function test_RewardsEligibilityOracle_AllowsSettingToZeroAddress() public {
vm.prank(governor);
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));

vm.expectEmit(address(rm));
emit ProviderEligibilityOracleSet(IProviderEligibility(address(oracle)), IProviderEligibility(address(0)));
vm.prank(governor);
rm.setProviderEligibilityOracle(IProviderEligibility(address(0)));

assertEq(address(rm.getProviderEligibilityOracle()), address(0));
}

function test_RewardsEligibilityOracle_RevertWhen_OracleIsEOA() public {
// An EOA has no code, so the supportsInterface call reverts
vm.prank(governor);
vm.expectRevert();
rm.setProviderEligibilityOracle(IProviderEligibility(indexer));
}

function test_RewardsEligibilityOracle_RevertWhen_InterfaceNotSupported() public {
MockERC165 mockERC165 = new MockERC165();

vm.prank(governor);
vm.expectRevert(bytes("Contract does not support IProviderEligibility interface"));
rm.setProviderEligibilityOracle(IProviderEligibility(address(mockERC165)));
}

function test_RewardsEligibilityOracle_NoEventWhenSettingSameOracle() public {
vm.prank(governor);
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));

vm.recordLogs();
vm.prank(governor);
rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle)));
_assertNotEmitted(vm.getRecordedLogs(), ProviderEligibilityOracleSet.selector);
}

// -- rewards eligibility in takeRewards --

function test_RewardsEligibilityInTakeRewards_DeniesRewardsDueToEligibilityOracle() public {
_useEligibilityOracle(false);
uint256 expectedRewards = _allocateAndAccrue();

vm.expectEmit(address(rm), 1);
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
_close(allocationID1);
}

function test_RewardsEligibilityInTakeRewards_AllowsRewardsWhenOracleApproves() public {
_useEligibilityOracle(true);
uint256 expectedRewards = _allocateAndAccrue();

vm.expectEmit(address(rm), 1);
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
_close(allocationID1);
}

// -- rewards eligibility oracle and denylist interaction --

function test_RewardsEligibilityOracleAndDenylistInteraction_PrioritizesDenylistOverOracleWhenBothDeny() public {
_setAvailabilityOracle();
_setDenied(true);
_useEligibilityOracle(false);

// Created while denied: the accumulator is frozen, so the allocation has no rewards to deny
_allocateAndAccrue();

vm.recordLogs();
_close(allocationID1);
Vm.Log[] memory logs = vm.getRecordedLogs();
_assertNotEmitted(logs, RewardsDenied.selector);
_assertNotEmitted(logs, RewardsDeniedDueToEligibility.selector);
_assertNotEmitted(logs, HorizonRewardsAssigned.selector);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_ChecksOracleWhenDenylistAllowsButIndexerIneligible()
public
{
_useEligibilityOracle(false);
uint256 expectedRewards = _allocateAndAccrue();

vm.expectEmit(address(rm), 1);
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
_close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_IndexerBecomesIneligibleMidAllocation() public {
_useEligibilityOracle(true);
uint256 expectedRewards = _allocateAndAccrue();

// Eligibility is checked at close time, not at creation time
oracle.setIndexerEligible(indexer, false);

vm.expectEmit(address(rm), 1);
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
_close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_IndexerBecomesEligibleMidAllocation() public {
_useEligibilityOracle(false);
uint256 expectedRewards = _allocateAndAccrue();

oracle.setIndexerEligible(indexer, true);

vm.expectEmit(address(rm), 1);
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
_close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_DenylistAddedMidAllocation() public {
_setAvailabilityOracle();
_allocateAndAccrue();

_setDenied(true);
vm.roll(block.number + 1);

vm.expectEmit(address(rm));
emit RewardsDenied(indexer, allocationID1);
_close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_DenylistRemovedMidAllocation() public {
_setAvailabilityOracle();
_setDenied(true);

// Created while denied: rewards accrued while denied are reclaimed at the subgraph level
_allocateAndAccrue();

// Undeny snapshots the accumulator, then only post-undeny rewards accrue
_setDenied(false);
vm.roll(block.number + EPOCH_LENGTH);

vm.expectEmit(address(rm));
emit HorizonRewardsAssigned(
indexer,
allocationID1,
_expectedAllocationRewards(EPOCH_LENGTH, ALLOCATION, ALLOCATION)
);
_close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_AllowsRewardsWhenOracleIsZeroAddress() public {
assertEq(address(rm.getProviderEligibilityOracle()), address(0));

uint256 expectedRewards = _allocateAndAccrue();

vm.expectEmit(address(rm), 1);
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
_close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_RevertWhen_IneligibleAndRevertOnIneligible() public {
_useEligibilityOracle(false);
vm.prank(governor);
rm.setRevertOnIneligible(true);

_allocateAndAccrue();

vm.expectRevert(bytes("Indexer not eligible for rewards"));
issuer.close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_NoRevertForEligibleWhenRevertOnIneligible() public {
_useEligibilityOracle(true);
vm.prank(governor);
rm.setRevertOnIneligible(true);

uint256 expectedRewards = _allocateAndAccrue();

vm.expectEmit(address(rm));
emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards);
_close(allocationID1);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_ReclaimsForIneligibleWhenNotRevertOnIneligible()
public
{
_useEligibilityOracle(false);
assertFalse(rm.getRevertOnIneligible());

uint256 expectedRewards = _allocateAndAccrue();

vm.expectEmit(address(rm), 1);
emit RewardsDeniedDueToEligibility(indexer, allocationID1, expectedRewards);
uint256 rewards = _close(allocationID1);
assertEq(rewards, 0);
}

function test_RewardsEligibilityOracleAndDenylistInteraction_EventStructureDiffersBetweenDenialMechanisms() public {
uint256 snapshot = vm.snapshotState();

// Denylist denial: event WITHOUT amount. Deny after the allocation so there are rewards to deny.
_setAvailabilityOracle();
_allocateAndAccrue();
_setDenied(true);
vm.roll(block.number + 1);

vm.recordLogs();
_close(allocationID1);
Vm.Log memory denied = _findLog(vm.getRecordedLogs(), RewardsDenied.selector);
assertEq(denied.topics.length, 3);
assertEq(denied.topics[1], bytes32(uint256(uint160(indexer))));
assertEq(denied.topics[2], bytes32(uint256(uint160(allocationID1))));
assertEq(denied.data.length, 0);

// Reset, as the old test's fixture teardown and setup
vm.revertToState(snapshot);

// Eligibility denial: event WITH amount, what the indexer would have received
_useEligibilityOracle(false);
uint256 expectedRewards = _allocateAndAccrue();

vm.recordLogs();
_close(allocationID1);
Vm.Log memory ineligible = _findLog(vm.getRecordedLogs(), RewardsDeniedDueToEligibility.selector);
assertEq(ineligible.topics.length, 3);
assertEq(ineligible.topics[1], bytes32(uint256(uint160(indexer))));
assertEq(ineligible.topics[2], bytes32(uint256(uint160(allocationID1))));
uint256 amount = abi.decode(ineligible.data, (uint256));
assertGt(amount, 0);
assertEq(amount, expectedRewards);
}
}
Loading
Loading