From 7dbe58ae552af14a70528e1535cc751825216935 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Mon, 26 Oct 2026 16:58:54 -0300 Subject: [PATCH] test(issuance): port RewardsManager issuance allocator and subgraph service tests to Foundry --- .../rewards-issuance-allocator.test.ts | 420 --------------- .../rewards/rewards-subgraph-service.test.ts | 483 ------------------ .../rewards-manager/issuanceAllocator.t.sol | 256 ++++++++++ .../rewards-manager/subgraphService.t.sol | 257 ++++++++++ .../contracts-migration/test-mapping/t5c.md | 46 ++ 5 files changed, 559 insertions(+), 903 deletions(-) delete mode 100644 packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts delete mode 100644 packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts create mode 100644 packages/issuance/test/unit/rewards-manager/issuanceAllocator.t.sol create mode 100644 packages/issuance/test/unit/rewards-manager/subgraphService.t.sol create mode 100644 scripts/contracts-migration/test-mapping/t5c.md diff --git a/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts deleted file mode 100644 index 8047b8fd6..000000000 --- a/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { Curation } from '@graphprotocol/contracts' -import { GraphToken } from '@graphprotocol/contracts' -import { RewardsManager } from '@graphprotocol/contracts' -import { GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' -import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { constants } from 'ethers' -import hre from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' - -describe('Rewards - Issuance Allocator', () => { - const graph = hre.graph() - let curator1: SignerWithAddress - let governor: SignerWithAddress - let indexer1: SignerWithAddress - - let fixture: NetworkFixture - - let contracts: GraphNetworkContracts - let grt: GraphToken - let curation: Curation - let rewardsManager: RewardsManager - - const subgraphDeploymentID1 = randomHexBytes() - - const ISSUANCE_PER_BLOCK = toGRT('200') // 200 GRT every block - - before(async function () { - const testAccounts = await graph.getTestAccounts() - curator1 = testAccounts[0] - indexer1 = testAccounts[1] - ;({ governor } = await graph.getNamedAccounts()) - - fixture = new NetworkFixture(graph.provider) - contracts = await fixture.load(governor) - grt = contracts.GraphToken as GraphToken - curation = contracts.Curation as Curation - rewardsManager = contracts.RewardsManager as RewardsManager - - // 200 GRT per block - await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) - - // Distribute test funds - for (const wallet of [curator1]) { - await grt.connect(governor).mint(wallet.address, toGRT('1000000')) - await grt.connect(wallet).approve(curation.address, toGRT('1000000')) - } - }) - - beforeEach(async function () { - await fixture.setUp() - // Reset issuance allocator to ensure we use direct issuancePerBlock - await rewardsManager.connect(governor).setIssuanceAllocator(constants.AddressZero) - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('setIssuanceAllocator', function () { - describe('ERC-165 validation', function () { - it('should successfully set an issuance allocator that supports the interface', async function () { - // Deploy a mock issuance allocator that supports ERC-165 and IIssuanceAllocationDistribution - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockAllocator.deployed() - - // Should succeed because MockIssuanceAllocator supports IIssuanceAllocationDistribution - await expect(rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address)) - .to.emit(rewardsManager, 'IssuanceAllocatorSet') - .withArgs(constants.AddressZero, mockAllocator.address) - - // Verify the allocator was set - expect(await rewardsManager.getIssuanceAllocator()).to.equal(mockAllocator.address) - }) - - it('should revert when setting to EOA address (no contract code)', async function () { - const eoaAddress = indexer1.address - - // Should revert because EOAs don't have contract code to call supportsInterface on - await expect(rewardsManager.connect(governor).setIssuanceAllocator(eoaAddress)).to.be.reverted - }) - - it('should revert when setting to contract that does not support IIssuanceAllocationDistribution', async function () { - // Deploy a contract that supports ERC-165 but not IIssuanceAllocationDistribution - const MockERC165Factory = await hre.ethers.getContractFactory('contracts/tests/MockERC165.sol:MockERC165') - const mockERC165 = await MockERC165Factory.deploy() - await mockERC165.deployed() - - // Should revert because the contract doesn't support IIssuanceAllocationDistribution - await expect(rewardsManager.connect(governor).setIssuanceAllocator(mockERC165.address)).to.be.revertedWith( - 'Contract does not support IIssuanceAllocationDistribution interface', - ) - }) - - it('should validate interface before updating rewards calculation', async function () { - // This test ensures that ERC165 validation happens before updateAccRewardsPerSignal - // Deploy a contract that supports ERC-165 but not IIssuanceAllocationDistribution - const MockERC165Factory = await hre.ethers.getContractFactory('contracts/tests/MockERC165.sol:MockERC165') - const mockERC165 = await MockERC165Factory.deploy() - await mockERC165.deployed() - - // Should revert with interface error, not with any rewards calculation error - await expect(rewardsManager.connect(governor).setIssuanceAllocator(mockERC165.address)).to.be.revertedWith( - 'Contract does not support IIssuanceAllocationDistribution interface', - ) - }) - }) - - describe('access control', function () { - it('should revert when called by non-governor', async function () { - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockAllocator.deployed() - - // Should revert because indexer1 is not the governor - await expect(rewardsManager.connect(indexer1).setIssuanceAllocator(mockAllocator.address)).to.be.revertedWith( - 'Only Controller governor', - ) - }) - }) - - describe('state management', function () { - it('should allow setting issuance allocator to zero address (disable)', async function () { - // First set a valid allocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockAllocator.deployed() - - await rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address) - expect(await rewardsManager.getIssuanceAllocator()).to.equal(mockAllocator.address) - - // Now disable by setting to zero address - await expect(rewardsManager.connect(governor).setIssuanceAllocator(constants.AddressZero)) - .to.emit(rewardsManager, 'IssuanceAllocatorSet') - .withArgs(mockAllocator.address, constants.AddressZero) - - expect(await rewardsManager.getIssuanceAllocator()).to.equal(constants.AddressZero) - - // Should now use local issuancePerBlock again — both getters agree - expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) - expect(await rewardsManager.getRawIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) - }) - - it('should emit IssuanceAllocatorSet event when setting allocator', async function () { - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockIssuanceAllocator.deployed() - - const tx = rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - await expect(tx) - .emit(rewardsManager, 'IssuanceAllocatorSet') - .withArgs(constants.AddressZero, mockIssuanceAllocator.address) - }) - - it('should not emit event when setting to same allocator address', async function () { - // Deploy a mock issuance allocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockAllocator.deployed() - - // Set the allocator first time - await rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address) - - // Setting to same address should not emit event - const tx = await rewardsManager.connect(governor).setIssuanceAllocator(mockAllocator.address) - const receipt = await tx.wait() - - // Filter for IssuanceAllocatorSet events - const events = receipt.events?.filter((e) => e.event === 'IssuanceAllocatorSet') || [] - expect(events.length).to.equal(0) - }) - - it('should update rewards before changing issuance allocator', async function () { - // This test verifies that updateAccRewardsPerSignal is called when setting allocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockIssuanceAllocator.deployed() - - // Setting the allocator should trigger updateAccRewardsPerSignal - // We can't easily test this directly, but we can verify the allocator was set - await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - expect(await rewardsManager.getIssuanceAllocator()).eq(mockIssuanceAllocator.address) - }) - }) - }) - - describe('getAllocatedIssuancePerBlock', function () { - it('should return issuancePerBlock when no issuanceAllocator is set', async function () { - const expectedIssuance = toGRT('100.025') - await rewardsManager.connect(governor).setIssuancePerBlock(expectedIssuance) - - // Ensure no issuanceAllocator is set - expect(await rewardsManager.getIssuanceAllocator()).eq(constants.AddressZero) - - // Both getters should agree when no allocator is set - expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(expectedIssuance) - expect(await rewardsManager.getRawIssuancePerBlock()).eq(expectedIssuance) - }) - - it('should return value from issuanceAllocator when set', async function () { - // Create a mock IssuanceAllocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockIssuanceAllocator.deployed() - - // Set the mock allocator on RewardsManager - await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - - // Verify the allocator was set - expect(await rewardsManager.getIssuanceAllocator()).eq(mockIssuanceAllocator.address) - - // Set RewardsManager as a self-minting target with 25 GRT per block - const expectedIssuance = toGRT('25') - await mockIssuanceAllocator['setTargetAllocation(address,uint256,uint256,bool)']( - rewardsManager.address, - 0, // allocator issuance - expectedIssuance, // self issuance - true, - ) - - // Allocated getter returns the allocator value, raw getter still returns storage value - expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(expectedIssuance) - expect(await rewardsManager.getRawIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) - }) - - it('should return 0 when issuanceAllocator is set but target not registered as self-minter', async function () { - // Create a mock IssuanceAllocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockIssuanceAllocator.deployed() - - // Set the mock allocator on RewardsManager - await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - - // Set RewardsManager as an allocator-minting target (only allocator issuance) - await mockIssuanceAllocator['setTargetAllocation(address,uint256,uint256,bool)']( - rewardsManager.address, - toGRT('25'), // allocator issuance - 0, // self issuance - false, - ) - - // Allocated returns 0 (not a self-minting target), raw is unchanged - expect(await rewardsManager.getAllocatedIssuancePerBlock()).eq(0) - expect(await rewardsManager.getRawIssuancePerBlock()).eq(ISSUANCE_PER_BLOCK) - }) - }) - - describe('setIssuancePerBlock', function () { - it('should allow setIssuancePerBlock when issuanceAllocator is set', async function () { - // Create and set a mock IssuanceAllocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockIssuanceAllocator.deployed() - await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - - // Should allow setting issuancePerBlock even when allocator is set - const newIssuancePerBlock = toGRT('100') - await rewardsManager.connect(governor).setIssuancePerBlock(newIssuancePerBlock) - - // Both raw getter and storage variable reflect the new value - expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) - expect(await rewardsManager.getRawIssuancePerBlock()).eq(newIssuancePerBlock) - - // But the effective (allocated) issuance still comes from the allocator - expect(await rewardsManager.getAllocatedIssuancePerBlock()).not.eq(newIssuancePerBlock) - }) - }) - - describe('beforeIssuanceAllocationChange', function () { - it('should handle beforeIssuanceAllocationChange correctly', async function () { - // Create and set a mock IssuanceAllocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - const mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockIssuanceAllocator.deployed() - await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - - // Anyone should be able to call this function - await rewardsManager.connect(governor).beforeIssuanceAllocationChange() - - // Should also succeed when called by the allocator - await mockIssuanceAllocator.callBeforeIssuanceAllocationChange(rewardsManager.address) - }) - }) - - describe('issuance allocator integration', function () { - let mockIssuanceAllocator: any - - beforeEach(async function () { - // Create and setup mock allocator - const MockIssuanceAllocatorFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockIssuanceAllocator.sol:MockIssuanceAllocator', - ) - mockIssuanceAllocator = await MockIssuanceAllocatorFactory.deploy() - await mockIssuanceAllocator.deployed() - }) - - it('should accumulate rewards using allocator rate over time', async function () { - // Setup: Create signal - const totalSignal = toGRT('1000') - await curation.connect(curator1).mint(subgraphDeploymentID1, totalSignal, 0) - - // Set allocator with specific rate (50 GRT per block, different from local 200 GRT) - const allocatorRate = toGRT('50') - await mockIssuanceAllocator.setTargetAllocation(rewardsManager.address, 0, allocatorRate, false) - await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - - // Snapshot state after setting allocator - const rewardsAfterSet = await rewardsManager.getAccRewardsPerSignal() - - // Mine blocks to accrue rewards at allocator rate - const blocksToMine = 10 - await helpers.mine(blocksToMine) - - // Get accumulated rewards - const rewardsAfterMining = await rewardsManager.getAccRewardsPerSignal() - const actualAccrued = rewardsAfterMining.sub(rewardsAfterSet) - - // Calculate expected rewards: (rate × blocks) / totalSignal - // Expected = (50 GRT × 10 blocks) / 1000 GRT signal = 0.5 GRT per signal - const expectedAccrued = allocatorRate.mul(blocksToMine).mul(toGRT('1')).div(totalSignal) - - // Verify rewards accumulated at allocator rate (not local rate of 200 GRT/block) - expect(actualAccrued).to.eq(expectedAccrued) - - // Verify NOT using local rate (would be 4x higher: 200 vs 50) - const wrongExpected = ISSUANCE_PER_BLOCK.mul(blocksToMine).mul(toGRT('1')).div(totalSignal) - expect(actualAccrued).to.not.eq(wrongExpected) - }) - - it('should maintain reward consistency when switching between rates', async function () { - // Setup: Create signal - const totalSignal = toGRT('2000') - await curation.connect(curator1).mint(subgraphDeploymentID1, totalSignal, 0) - - // Snapshot initial state - const block0 = await helpers.latestBlock() - const rewards0 = await rewardsManager.getAccRewardsPerSignal() - - // Phase 1: Accrue at local rate (200 GRT/block) - await helpers.mine(5) - const block1 = await helpers.latestBlock() - const rewards1 = await rewardsManager.getAccRewardsPerSignal() - - // Calculate phase 1 accrual - const blocksPhase1 = block1 - block0 - const phase1Accrued = rewards1.sub(rewards0) - const expectedPhase1 = ISSUANCE_PER_BLOCK.mul(blocksPhase1).mul(toGRT('1')).div(totalSignal) - expect(phase1Accrued).to.eq(expectedPhase1) - - // Phase 2: Switch to allocator with different rate (100 GRT/block) - const allocatorRate = toGRT('100') - await mockIssuanceAllocator.setTargetAllocation(rewardsManager.address, 0, allocatorRate, false) - await rewardsManager.connect(governor).setIssuanceAllocator(mockIssuanceAllocator.address) - - const block2 = await helpers.latestBlock() - const rewards2 = await rewardsManager.getAccRewardsPerSignal() - - await helpers.mine(8) - const block3 = await helpers.latestBlock() - const rewards3 = await rewardsManager.getAccRewardsPerSignal() - - // Calculate phase 2 accrual (includes the setIssuanceAllocator block at local rate) - const blocksPhase2 = block3 - block2 - const phase2Accrued = rewards3.sub(rewards2) - const expectedPhase2 = allocatorRate.mul(blocksPhase2).mul(toGRT('1')).div(totalSignal) - expect(phase2Accrued).to.eq(expectedPhase2) - - // Phase 3: Switch back to local rate (200 GRT/block) - await rewardsManager.connect(governor).setIssuanceAllocator(constants.AddressZero) - - const block4 = await helpers.latestBlock() - const rewards4 = await rewardsManager.getAccRewardsPerSignal() - - await helpers.mine(4) - const block5 = await helpers.latestBlock() - const rewards5 = await rewardsManager.getAccRewardsPerSignal() - - // Calculate phase 3 accrual - const blocksPhase3 = block5 - block4 - const phase3Accrued = rewards5.sub(rewards4) - const expectedPhase3 = ISSUANCE_PER_BLOCK.mul(blocksPhase3).mul(toGRT('1')).div(totalSignal) - expect(phase3Accrued).to.eq(expectedPhase3) - - // Verify total consistency: all rewards from start to end must equal sum of all phases - // including the transition blocks (setIssuanceAllocator calls mine blocks too) - const transitionPhase1to2 = rewards2.sub(rewards1) // Block mined by setIssuanceAllocator - const transitionPhase2to3 = rewards4.sub(rewards3) // Block mined by removing allocator - const totalExpected = phase1Accrued - .add(transitionPhase1to2) - .add(phase2Accrued) - .add(transitionPhase2to3) - .add(phase3Accrued) - const totalActual = rewards5.sub(rewards0) - expect(totalActual).to.eq(totalExpected) - }) - }) -}) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts deleted file mode 100644 index 58338cac8..000000000 --- a/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { Curation } from '@graphprotocol/contracts' -import { GraphToken } from '@graphprotocol/contracts' -import { RewardsManager } from '@graphprotocol/contracts' -import { GraphNetworkContracts, helpers, randomAddress, randomHexBytes, toGRT } from '@graphprotocol/sdk' -import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' -import { expect } from 'chai' -import { constants } from 'ethers' -import hre from 'hardhat' -import { network } from 'hardhat' - -import { NetworkFixture } from '../lib/fixtures' - -// TODO: Behavior change - HorizonRewardsAssigned is no longer emitted when rewards == 0 -// Set to true if the old behavior is restored (emitting event for zero rewards) -const EMIT_EVENT_FOR_ZERO_REWARDS = false - -describe('Rewards - SubgraphService', () => { - const graph = hre.graph() - let curator1: SignerWithAddress - let governor: SignerWithAddress - let indexer1: SignerWithAddress - - let fixture: NetworkFixture - - let contracts: GraphNetworkContracts - let grt: GraphToken - let curation: Curation - let rewardsManager: RewardsManager - - const subgraphDeploymentID1 = randomHexBytes() - const allocationID1 = randomAddress() - - const ISSUANCE_PER_BLOCK = toGRT('200') // 200 GRT every block - - before(async function () { - const testAccounts = await graph.getTestAccounts() - curator1 = testAccounts[0] - indexer1 = testAccounts[1] - ;({ governor } = await graph.getNamedAccounts()) - - fixture = new NetworkFixture(graph.provider) - contracts = await fixture.load(governor) - grt = contracts.GraphToken as GraphToken - curation = contracts.Curation as Curation - rewardsManager = contracts.RewardsManager - - // 200 GRT per block - await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) - - // Distribute test funds - for (const wallet of [indexer1, curator1]) { - await grt.connect(governor).mint(wallet.address, toGRT('1000000')) - await grt.connect(wallet).approve(curation.address, toGRT('1000000')) - } - }) - - beforeEach(async function () { - await fixture.setUp() - }) - - afterEach(async function () { - await fixture.tearDown() - }) - - describe('subgraph service configuration', function () { - it('should reject setSubgraphService if unauthorized', async function () { - const newService = randomAddress() - const tx = rewardsManager.connect(indexer1).setSubgraphService(newService) - await expect(tx).revertedWith('Only Controller governor') - }) - - it('should set subgraph service if governor', async function () { - const newService = randomAddress() - const tx = rewardsManager.connect(governor).setSubgraphService(newService) - - await expect(tx).emit(rewardsManager, 'SubgraphServiceSet').withArgs(constants.AddressZero, newService) - - expect(await rewardsManager.subgraphService()).eq(newService) - }) - - it('should allow setting to zero address', async function () { - const service = randomAddress() - await rewardsManager.connect(governor).setSubgraphService(service) - - const tx = rewardsManager.connect(governor).setSubgraphService(constants.AddressZero) - await expect(tx).emit(rewardsManager, 'SubgraphServiceSet').withArgs(service, constants.AddressZero) - - expect(await rewardsManager.subgraphService()).eq(constants.AddressZero) - }) - - it('should emit event when setting different address', async function () { - const service1 = randomAddress() - const service2 = randomAddress() - - await rewardsManager.connect(governor).setSubgraphService(service1) - - // Setting a different address should emit event - const tx = await rewardsManager.connect(governor).setSubgraphService(service2) - await expect(tx).emit(rewardsManager, 'SubgraphServiceSet').withArgs(service1, service2) - }) - }) - - describe('subgraph service as rewards issuer', function () { - let mockSubgraphService: any - - beforeEach(async function () { - // Deploy mock SubgraphService - const MockSubgraphServiceFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockSubgraphService.sol:MockSubgraphService', - ) - mockSubgraphService = await MockSubgraphServiceFactory.deploy() - await mockSubgraphService.deployed() - - // Set it on RewardsManager - await rewardsManager.connect(governor).setSubgraphService(mockSubgraphService.address) - }) - - describe('getRewards from subgraph service', function () { - it('should calculate rewards for subgraph service allocations', async function () { - // Setup: Create signal for rewards calculation - const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - - // Setup allocation data in mock - const tokensAllocated = toGRT('12500') - await mockSubgraphService.setAllocation( - allocationID1, - true, // isActive - indexer1.address, - subgraphDeploymentID1, - tokensAllocated, - 0, // accRewardsPerAllocatedToken - 0, // accRewardsPending - ) - - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) - - // Mine some blocks to accrue rewards - await helpers.mine(10) - - // Get rewards - should return calculated amount - const rewards = await rewardsManager.getRewards(mockSubgraphService.address, allocationID1) - expect(rewards).to.be.gt(0) - }) - - it('should return zero for inactive allocation', async function () { - // Setup allocation as inactive - await mockSubgraphService.setAllocation( - allocationID1, - false, // isActive = false - indexer1.address, - subgraphDeploymentID1, - toGRT('12500'), - 0, - 0, - ) - - const rewards = await rewardsManager.getRewards(mockSubgraphService.address, allocationID1) - expect(rewards).to.equal(0) - }) - - it('should reject getRewards from non-rewards-issuer contract', async function () { - const randomContract = randomAddress() - const tx = rewardsManager.getRewards(randomContract, allocationID1) - await expect(tx).revertedWith('Not a rewards issuer') - }) - }) - - describe('takeRewards from subgraph service', function () { - it('should take rewards through subgraph service', async function () { - // Setup: Create signal for rewards calculation - const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - - // Setup allocation data in mock - const tokensAllocated = toGRT('12500') - await mockSubgraphService.setAllocation( - allocationID1, - true, // isActive - indexer1.address, - subgraphDeploymentID1, - tokensAllocated, - 0, // accRewardsPerAllocatedToken - 0, // accRewardsPending - ) - - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) - - // Mine some blocks to accrue rewards - await helpers.mine(10) - - // Before state - const beforeSubgraphServiceBalance = await grt.balanceOf(mockSubgraphService.address) - const beforeTotalSupply = await grt.totalSupply() - - // Impersonate the mock subgraph service contract - await network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [mockSubgraphService.address], - }) - await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) - - const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) - - // Take rewards (called by subgraph service) - const tx = await rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) - const receipt = await tx.wait() - - // Stop impersonating - await network.provider.request({ - method: 'hardhat_stopImpersonatingAccount', - params: [mockSubgraphService.address], - }) - - // Parse the event - const event = receipt.logs - .map((log: any) => { - try { - return rewardsManager.interface.parseLog(log) - } catch { - return null - } - }) - .find((e: any) => e?.name === 'HorizonRewardsAssigned') - - expect(event).to.not.be.undefined - expect(event?.args.indexer).to.equal(indexer1.address) - expect(event?.args.allocationID).to.equal(allocationID1) - expect(event?.args.amount).to.be.gt(0) - - // After state - verify tokens minted to subgraph service - const afterSubgraphServiceBalance = await grt.balanceOf(mockSubgraphService.address) - const afterTotalSupply = await grt.totalSupply() - - expect(afterSubgraphServiceBalance).to.be.gt(beforeSubgraphServiceBalance) - expect(afterTotalSupply).to.be.gt(beforeTotalSupply) - }) - - it('should return zero rewards for inactive allocation', async function () { - // Setup allocation as inactive - await mockSubgraphService.setAllocation( - allocationID1, - false, // isActive = false - indexer1.address, - subgraphDeploymentID1, - toGRT('12500'), - 0, - 0, - ) - - // Impersonate the mock subgraph service contract - await network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [mockSubgraphService.address], - }) - await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) - - const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) - - // Take rewards should return 0 - const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) - if (EMIT_EVENT_FOR_ZERO_REWARDS) { - await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) - } else { - await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') - } - - // Stop impersonating - await network.provider.request({ - method: 'hardhat_stopImpersonatingAccount', - params: [mockSubgraphService.address], - }) - }) - - it('should reject takeRewards from non-rewards-issuer contract', async function () { - const tx = rewardsManager.connect(indexer1).takeRewards(allocationID1) - await expect(tx).revertedWith('Caller must be a rewards issuer') - }) - - it('should handle zero rewards scenario', async function () { - // Setup with zero issuance - await rewardsManager.connect(governor).setIssuancePerBlock(0) - - // Setup allocation - await mockSubgraphService.setAllocation( - allocationID1, - true, - indexer1.address, - subgraphDeploymentID1, - toGRT('12500'), - 0, - 0, - ) - - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, toGRT('12500')) - - // Mine blocks - await helpers.mine(10) - - // Impersonate the mock subgraph service contract - await network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [mockSubgraphService.address], - }) - await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) - - const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) - - // Take rewards should succeed with 0 amount - const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) - if (EMIT_EVENT_FOR_ZERO_REWARDS) { - await expect(tx).emit(rewardsManager, 'HorizonRewardsAssigned').withArgs(indexer1.address, allocationID1, 0) - } else { - await expect(tx).to.not.emit(rewardsManager, 'HorizonRewardsAssigned') - } - - // Stop impersonating - await network.provider.request({ - method: 'hardhat_stopImpersonatingAccount', - params: [mockSubgraphService.address], - }) - }) - }) - - describe('mixed allocations from staking and subgraph service', function () { - it('should account for both staking and subgraph service allocations in getAccRewardsPerAllocatedToken', async function () { - // This test verifies that getSubgraphAllocatedTokens is called for both issuers - // and rewards are distributed proportionally - - // Setup: Create signal - const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - - // Setup subgraph service allocation - const tokensFromSubgraphService = toGRT('5000') - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensFromSubgraphService) - - // Note: We can't easily create a real staking allocation in this test - // but the contract code at lines 381-388 loops through both issuers - // and sums their allocated tokens. This test verifies the subgraph service path. - - // Mine some blocks - await helpers.mine(5) - - // Get accumulated rewards per allocated token - const [accRewardsPerAllocatedToken, accRewardsForSubgraph] = - await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1) - - // Should have calculated rewards based on subgraph service allocations - expect(accRewardsPerAllocatedToken).to.be.gt(0) - expect(accRewardsForSubgraph).to.be.gt(0) - }) - - it('should handle case where only subgraph service has allocations', async function () { - // Setup: Create signal - const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - - // Only subgraph service has allocations - const tokensFromSubgraphService = toGRT('10000') - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensFromSubgraphService) - - // Mine blocks - await helpers.mine(5) - - // Get rewards - const [accRewardsPerAllocatedToken] = await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1) - - expect(accRewardsPerAllocatedToken).to.be.gt(0) - }) - - it('should return zero when neither issuer has allocations', async function () { - // Setup: Create signal but no allocations - const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - - // No allocations from either issuer - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, 0) - - // Mine blocks - await helpers.mine(5) - - // Get rewards - Option B: with no allocations, rewards are reclaimed (NO_ALLOCATED_TOKENS) - // so both accRewardsPerAllocatedToken and accRewardsForSubgraph remain 0 - const [accRewardsPerAllocatedToken, accRewardsForSubgraph] = - await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1) - - expect(accRewardsPerAllocatedToken).to.equal(0) - expect(accRewardsForSubgraph).to.equal(0) // Option B: rewards reclaimed when no allocations - }) - }) - - describe('subgraph service with denylist and eligibility', function () { - it('should deny rewards from subgraph service when subgraph is on denylist', async function () { - // Setup denylist - await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) - await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) - - // Setup allocation with some pending rewards so rewards > 0 - await mockSubgraphService.setAllocation( - allocationID1, - true, - indexer1.address, - subgraphDeploymentID1, - toGRT('12500'), - 0, - toGRT('100'), // accRewardsPending > 0 so rewards will be calculated - ) - - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, toGRT('12500')) - - // Impersonate the mock subgraph service contract - await network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [mockSubgraphService.address], - }) - await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) - - const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) - - // Take rewards should be denied - const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) - await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) - - // Stop impersonating - await network.provider.request({ - method: 'hardhat_stopImpersonatingAccount', - params: [mockSubgraphService.address], - }) - }) - - it('should deny rewards from subgraph service when indexer is ineligible', async function () { - // Setup REO that denies indexer1 - const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( - 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', - ) - const mockREO = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny by default - await mockREO.deployed() - await rewardsManager.connect(governor).setProviderEligibilityOracle(mockREO.address) - - // Setup: Create signal - const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - - // Setup allocation - const tokensAllocated = toGRT('12500') - await mockSubgraphService.setAllocation( - allocationID1, - true, - indexer1.address, - subgraphDeploymentID1, - tokensAllocated, - 0, - 0, - ) - - await mockSubgraphService.setSubgraphAllocatedTokens(subgraphDeploymentID1, tokensAllocated) - - // Mine blocks to accrue rewards - await helpers.mine(5) - - // Impersonate the mock subgraph service contract - await network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [mockSubgraphService.address], - }) - await network.provider.send('hardhat_setBalance', [mockSubgraphService.address, '0x1000000000000000000']) - - const mockSubgraphServiceSigner = await hre.ethers.getSigner(mockSubgraphService.address) - - // Take rewards should be denied due to eligibility - const tx = rewardsManager.connect(mockSubgraphServiceSigner).takeRewards(allocationID1) - await expect(tx).emit(rewardsManager, 'RewardsDeniedDueToEligibility') - - // Stop impersonating - await network.provider.request({ - method: 'hardhat_stopImpersonatingAccount', - params: [mockSubgraphService.address], - }) - }) - }) - }) -}) diff --git a/packages/issuance/test/unit/rewards-manager/issuanceAllocator.t.sol b/packages/issuance/test/unit/rewards-manager/issuanceAllocator.t.sol new file mode 100644 index 000000000..f65d67de6 --- /dev/null +++ b/packages/issuance/test/unit/rewards-manager/issuanceAllocator.t.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationDistribution.sol"; + +import { MockERC165 } from "../../../contracts/test/allocate/MockERC165.sol"; +import { RewardsManagerSharedTest } from "./shared.t.sol"; + +contract RewardsManagerIssuanceAllocatorTest is RewardsManagerSharedTest { + event IssuanceAllocatorSet( + IIssuanceAllocationDistribution indexed oldIssuanceAllocator, + IIssuanceAllocationDistribution indexed newIssuanceAllocator + ); + + bytes32 internal constant SUBGRAPH_DEPLOYMENT_ID_1 = keccak256("subgraphDeploymentID1"); + + function _setAllocator(address newAllocator) internal { + vm.prank(governor); + rm.setIssuanceAllocator(IIssuanceAllocationDistribution(newAllocator)); + } + + // -- setIssuanceAllocator: ERC-165 validation -- + + function test_ERC165Validation_SetsAllocatorThatSupportsTheInterface() public { + vm.expectEmit(address(rm)); + emit IssuanceAllocatorSet( + IIssuanceAllocationDistribution(address(0)), + IIssuanceAllocationDistribution(address(allocator)) + ); + _setAllocator(address(allocator)); + + assertEq(address(rm.getIssuanceAllocator()), address(allocator)); + } + + function test_ERC165Validation_RevertWhen_AllocatorIsEOA() public { + vm.prank(governor); + vm.expectRevert(); + rm.setIssuanceAllocator(IIssuanceAllocationDistribution(indexer)); + } + + function test_ERC165Validation_RevertWhen_InterfaceNotSupported() public { + MockERC165 mockERC165 = new MockERC165(); + + vm.prank(governor); + vm.expectRevert(bytes("Contract does not support IIssuanceAllocationDistribution interface")); + rm.setIssuanceAllocator(IIssuanceAllocationDistribution(address(mockERC165))); + } + + function test_ERC165Validation_RevertWhen_InterfaceNotSupportedBeforeRewardsUpdate() public { + MockERC165 mockERC165 = new MockERC165(); + + vm.prank(governor); + vm.expectRevert(bytes("Contract does not support IIssuanceAllocationDistribution interface")); + rm.setIssuanceAllocator(IIssuanceAllocationDistribution(address(mockERC165))); + } + + // -- setIssuanceAllocator: access control -- + + function test_AccessControl_RevertWhen_CalledByNonGovernor() public { + vm.prank(indexer); + vm.expectRevert(bytes("Only Controller governor")); + rm.setIssuanceAllocator(IIssuanceAllocationDistribution(address(allocator))); + } + + // -- setIssuanceAllocator: state management -- + + function test_StateManagement_AllowsDisablingWithZeroAddress() public { + _setAllocator(address(allocator)); + assertEq(address(rm.getIssuanceAllocator()), address(allocator)); + + vm.expectEmit(address(rm)); + emit IssuanceAllocatorSet( + IIssuanceAllocationDistribution(address(allocator)), + IIssuanceAllocationDistribution(address(0)) + ); + _setAllocator(address(0)); + + assertEq(address(rm.getIssuanceAllocator()), address(0)); + + // Local issuancePerBlock is used again, both getters agree + assertEq(rm.getAllocatedIssuancePerBlock(), ISSUANCE_PER_BLOCK); + assertEq(rm.getRawIssuancePerBlock(), ISSUANCE_PER_BLOCK); + } + + function test_StateManagement_EmitsIssuanceAllocatorSet() public { + vm.expectEmit(address(rm)); + emit IssuanceAllocatorSet( + IIssuanceAllocationDistribution(address(0)), + IIssuanceAllocationDistribution(address(allocator)) + ); + _setAllocator(address(allocator)); + } + + function test_StateManagement_DoesNotEmitWhenSettingSameAllocator() public { + _setAllocator(address(allocator)); + + vm.recordLogs(); + _setAllocator(address(allocator)); + _assertNotEmitted(vm.getRecordedLogs(), IssuanceAllocatorSet.selector); + } + + function test_StateManagement_UpdatesRewardsBeforeChangingAllocator() public { + // Old test only checked the allocator was set; the pre-change update is also visible in the block marker + vm.roll(block.number + 1); + _setAllocator(address(allocator)); + + assertEq(address(rm.getIssuanceAllocator()), address(allocator)); + assertEq(rm.accRewardsPerSignalLastBlockUpdated(), block.number); + } + + // -- getAllocatedIssuancePerBlock -- + + function test_GetAllocatedIssuancePerBlock_ReturnsIssuancePerBlockWhenNoAllocatorSet() public { + uint256 expectedIssuance = 100.025 ether; + vm.prank(governor); + rm.setIssuancePerBlock(expectedIssuance); + + assertEq(address(rm.getIssuanceAllocator()), address(0)); + + assertEq(rm.getAllocatedIssuancePerBlock(), expectedIssuance); + assertEq(rm.getRawIssuancePerBlock(), expectedIssuance); + } + + function test_GetAllocatedIssuancePerBlock_ReturnsValueFromAllocatorWhenSet() public { + _setAllocator(address(allocator)); + assertEq(address(rm.getIssuanceAllocator()), address(allocator)); + + // RewardsManager is a self-minting target with 25 GRT per block + uint256 expectedIssuance = 25 ether; + allocator.setTargetAllocation(address(rm), 0, expectedIssuance, true); + + // Allocated getter returns the allocator value, raw getter still returns storage value + assertEq(rm.getAllocatedIssuancePerBlock(), expectedIssuance); + assertEq(rm.getRawIssuancePerBlock(), ISSUANCE_PER_BLOCK); + } + + function test_GetAllocatedIssuancePerBlock_ZeroWhenTargetNotRegisteredAsSelfMinter() public { + _setAllocator(address(allocator)); + + // RewardsManager is an allocator-minting target (only allocator issuance) + allocator.setTargetAllocation(address(rm), 25 ether, 0, false); + + assertEq(rm.getAllocatedIssuancePerBlock(), 0); + assertEq(rm.getRawIssuancePerBlock(), ISSUANCE_PER_BLOCK); + } + + // -- setIssuancePerBlock -- + + function test_SetIssuancePerBlock_AllowedWhenAllocatorIsSet() public { + _setAllocator(address(allocator)); + + uint256 newIssuancePerBlock = 100 ether; + vm.prank(governor); + rm.setIssuancePerBlock(newIssuancePerBlock); + + assertEq(rm.issuancePerBlock(), newIssuancePerBlock); + assertEq(rm.getRawIssuancePerBlock(), newIssuancePerBlock); + + // The effective issuance still comes from the allocator (no target registered, so zero) + assertNotEq(rm.getAllocatedIssuancePerBlock(), newIssuancePerBlock); + assertEq(rm.getAllocatedIssuancePerBlock(), 0); + } + + // -- beforeIssuanceAllocationChange -- + + function test_BeforeIssuanceAllocationChange_HandlesCallsFromAnyoneAndTheAllocator() public { + _setAllocator(address(allocator)); + + // Anyone can call this function + vm.roll(block.number + 1); + vm.prank(governor); + rm.beforeIssuanceAllocationChange(); + assertEq(rm.accRewardsPerSignalLastBlockUpdated(), block.number); + + // Also succeeds when called by the allocator + vm.roll(block.number + 1); + allocator.callBeforeIssuanceAllocationChange(address(rm)); + assertEq(rm.accRewardsPerSignalLastBlockUpdated(), block.number); + } + + // -- issuance allocator integration -- + + function test_IssuanceAllocatorIntegration_AccumulatesRewardsUsingAllocatorRateOverTime() public { + uint256 totalSignal = 1000 ether; + _signal(SUBGRAPH_DEPLOYMENT_ID_1, totalSignal); + + // Allocator rate of 50 GRT per block, different from the local 200 + uint256 allocatorRate = 50 ether; + allocator.setTargetAllocation(address(rm), 0, allocatorRate, false); + _setAllocator(address(allocator)); + + uint256 rewardsAfterSet = rm.getAccRewardsPerSignal(); + + uint256 blocksToMine = 10; + vm.roll(block.number + blocksToMine); + + uint256 actualAccrued = rm.getAccRewardsPerSignal() - rewardsAfterSet; + + // (rate x blocks) / totalSignal = 0.5 GRT per signal + uint256 expectedAccrued = (allocatorRate * blocksToMine * 1e18) / totalSignal; + assertEq(actualAccrued, expectedAccrued); + + // Not the local rate, which would be 4x higher + assertNotEq(actualAccrued, _expectedRewardsPerSignal(blocksToMine, totalSignal)); + } + + function test_IssuanceAllocatorIntegration_MaintainsRewardConsistencyWhenSwitchingBetweenRates() public { + uint256 totalSignal = 2000 ether; + _signal(SUBGRAPH_DEPLOYMENT_ID_1, totalSignal); + + uint256 rewards0 = rm.getAccRewardsPerSignal(); + // Sum of every phase and transition accrual, checked against the overall difference at the end + uint256 totalExpected; + uint256 previous = rewards0; + uint256 allocatorRate = 100 ether; + + // Phase 1: accrue at the local rate (200 GRT per block) + vm.roll(block.number + 5); + uint256 current = rm.getAccRewardsPerSignal(); + assertEq(current - previous, _expectedRewardsPerSignal(5, totalSignal)); + totalExpected += current - previous; + previous = current; + + // Phase 2: switch to the allocator at 100 GRT per block. The switch happens one block later (the block + // Hardhat mined for the tx), which still accrues at the local rate + allocator.setTargetAllocation(address(rm), 0, allocatorRate, false); + vm.roll(block.number + 1); + _setAllocator(address(allocator)); + current = rm.getAccRewardsPerSignal(); + assertEq(current - previous, _expectedRewardsPerSignal(1, totalSignal)); + totalExpected += current - previous; + previous = current; + + vm.roll(block.number + 8); + current = rm.getAccRewardsPerSignal(); + assertEq(current - previous, (allocatorRate * 8 * 1e18) / totalSignal); + totalExpected += current - previous; + previous = current; + + // Phase 3: switch back to the local rate. The block of the switch still accrues at the allocator rate + vm.roll(block.number + 1); + _setAllocator(address(0)); + current = rm.getAccRewardsPerSignal(); + assertEq(current - previous, (allocatorRate * 1 * 1e18) / totalSignal); + totalExpected += current - previous; + previous = current; + + vm.roll(block.number + 4); + current = rm.getAccRewardsPerSignal(); + assertEq(current - previous, _expectedRewardsPerSignal(4, totalSignal)); + totalExpected += current - previous; + + // All rewards from start to end equal the sum of the phases and the transition blocks + assertEq(current - rewards0, totalExpected); + } +} diff --git a/packages/issuance/test/unit/rewards-manager/subgraphService.t.sol b/packages/issuance/test/unit/rewards-manager/subgraphService.t.sol new file mode 100644 index 000000000..a7e2eca7a --- /dev/null +++ b/packages/issuance/test/unit/rewards-manager/subgraphService.t.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { IProviderEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IProviderEligibility.sol"; + +import { RewardsManagerSharedTest } from "./shared.t.sol"; + +contract RewardsManagerSubgraphServiceTest is RewardsManagerSharedTest { + event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService); + 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); + + uint256 internal constant ISSUANCE_RATE_PERIODS = 5; + + bytes32 internal constant SUBGRAPH_DEPLOYMENT_ID_1 = keccak256("subgraphDeploymentID1"); + + address internal allocationID1 = makeAddr("allocationID1"); + + /// Rewards of an allocation holding all of the allocated tokens, on a subgraph holding all the signal. + function _expectedAllocationRewards(uint256 blocks, uint256 tokens) internal pure returns (uint256) { + uint256 subgraphRewards = _expectedSubgraphRewards(blocks, SIGNAL, SIGNAL); + return (_expectedRewardsPerAllocatedToken(subgraphRewards, tokens) * tokens) / 1e18; + } + + function _setActiveAllocation(uint256 tokens, uint256 accRewardsPending) internal { + issuer.setAllocation(allocationID1, true, indexer, SUBGRAPH_DEPLOYMENT_ID_1, tokens, 0, accRewardsPending); + issuer.setSubgraphAllocatedTokens(SUBGRAPH_DEPLOYMENT_ID_1, tokens); + } + + // -- subgraph service configuration -- + + function test_SubgraphServiceConfiguration_RevertWhen_SetByNonGovernor() public { + vm.prank(indexer); + vm.expectRevert(bytes("Only Controller governor")); + rm.setSubgraphService(makeAddr("newService")); + } + + function test_SubgraphServiceConfiguration_SetsSubgraphServiceAsGovernor() public { + address newService = makeAddr("newService"); + + vm.expectEmit(address(rm)); + emit SubgraphServiceSet(address(issuer), newService); + vm.prank(governor); + rm.setSubgraphService(newService); + + assertEq(address(rm.subgraphService()), newService); + } + + function test_SubgraphServiceConfiguration_AllowsSettingToZeroAddress() public { + address service = makeAddr("service"); + vm.prank(governor); + rm.setSubgraphService(service); + + vm.expectEmit(address(rm)); + emit SubgraphServiceSet(service, address(0)); + vm.prank(governor); + rm.setSubgraphService(address(0)); + + assertEq(address(rm.subgraphService()), address(0)); + } + + function test_SubgraphServiceConfiguration_EmitsEventWhenSettingDifferentAddress() public { + address service1 = makeAddr("service1"); + address service2 = makeAddr("service2"); + + vm.prank(governor); + rm.setSubgraphService(service1); + + vm.expectEmit(address(rm)); + emit SubgraphServiceSet(service1, service2); + vm.prank(governor); + rm.setSubgraphService(service2); + } + + // -- getRewards from subgraph service -- + + function test_GetRewards_CalculatesRewardsForSubgraphServiceAllocations() public { + _signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL); + _setActiveAllocation(ALLOCATION, 0); + + vm.roll(block.number + 10); + + uint256 rewards = rm.getRewards(address(issuer), allocationID1); + assertGt(rewards, 0); + assertEq(rewards, _expectedAllocationRewards(10, ALLOCATION)); + } + + function test_GetRewards_ZeroForInactiveAllocation() public { + issuer.setAllocation(allocationID1, false, indexer, SUBGRAPH_DEPLOYMENT_ID_1, ALLOCATION, 0, 0); + + assertEq(rm.getRewards(address(issuer), allocationID1), 0); + } + + function test_GetRewards_RevertWhen_NotARewardsIssuer() public { + address randomContract = makeAddr("randomContract"); + + vm.expectRevert(bytes("Not a rewards issuer")); + rm.getRewards(randomContract, allocationID1); + } + + // -- takeRewards from subgraph service -- + + function test_TakeRewards_TakesRewardsThroughSubgraphService() public { + _signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL); + _setActiveAllocation(ALLOCATION, 0); + + vm.roll(block.number + 10); + + uint256 expectedRewards = _expectedAllocationRewards(10, ALLOCATION); + uint256 beforeIssuerBalance = token.balanceOf(address(issuer)); + uint256 beforeTotalSupply = token.totalSupply(); + + vm.expectEmit(address(rm)); + emit HorizonRewardsAssigned(indexer, allocationID1, expectedRewards); + vm.prank(address(issuer)); + uint256 rewards = rm.takeRewards(allocationID1); + + assertEq(rewards, expectedRewards); + assertEq(token.balanceOf(address(issuer)), beforeIssuerBalance + expectedRewards); + assertEq(token.totalSupply(), beforeTotalSupply + expectedRewards); + } + + function test_TakeRewards_ZeroRewardsForInactiveAllocation() public { + issuer.setAllocation(allocationID1, false, indexer, SUBGRAPH_DEPLOYMENT_ID_1, ALLOCATION, 0, 0); + + vm.recordLogs(); + vm.prank(address(issuer)); + rm.takeRewards(allocationID1); + _assertNotEmitted(vm.getRecordedLogs(), HorizonRewardsAssigned.selector); + } + + function test_TakeRewards_RevertWhen_CallerIsNotARewardsIssuer() public { + vm.prank(indexer); + vm.expectRevert(bytes("Caller must be a rewards issuer")); + rm.takeRewards(allocationID1); + } + + function test_TakeRewards_HandlesZeroRewardsScenario() public { + vm.prank(governor); + rm.setIssuancePerBlock(0); + + _setActiveAllocation(ALLOCATION, 0); + + vm.roll(block.number + 10); + + vm.recordLogs(); + vm.prank(address(issuer)); + uint256 rewards = rm.takeRewards(allocationID1); + _assertNotEmitted(vm.getRecordedLogs(), HorizonRewardsAssigned.selector); + + assertEq(rewards, 0); + } + + // -- mixed allocations from staking and subgraph service -- + // Titles are stale: there is a single rewards issuer now, so the assertions run against `issuer`. + + function test_MixedAllocationsFromStakingAndSubgraphService_AccountsForSubgraphServiceAllocationsInAccRewardsPerAllocatedToken() + public + { + _signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL); + + uint256 tokensFromSubgraphService = 5000 ether; + issuer.setSubgraphAllocatedTokens(SUBGRAPH_DEPLOYMENT_ID_1, tokensFromSubgraphService); + + vm.roll(block.number + ISSUANCE_RATE_PERIODS); + + (uint256 accRewardsPerAllocatedToken, uint256 accRewardsForSubgraph) = rm.getAccRewardsPerAllocatedToken( + SUBGRAPH_DEPLOYMENT_ID_1 + ); + + assertGt(accRewardsPerAllocatedToken, 0); + assertGt(accRewardsForSubgraph, 0); + uint256 expectedForSubgraph = _expectedSubgraphRewards(ISSUANCE_RATE_PERIODS, SIGNAL, SIGNAL); + assertEq(accRewardsForSubgraph, expectedForSubgraph); + assertEq( + accRewardsPerAllocatedToken, + _expectedRewardsPerAllocatedToken(expectedForSubgraph, tokensFromSubgraphService) + ); + } + + function test_MixedAllocationsFromStakingAndSubgraphService_HandlesCaseWhereOnlySubgraphServiceHasAllocations() + public + { + _signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL); + + uint256 tokensFromSubgraphService = 10_000 ether; + issuer.setSubgraphAllocatedTokens(SUBGRAPH_DEPLOYMENT_ID_1, tokensFromSubgraphService); + + vm.roll(block.number + ISSUANCE_RATE_PERIODS); + + (uint256 accRewardsPerAllocatedToken, ) = rm.getAccRewardsPerAllocatedToken(SUBGRAPH_DEPLOYMENT_ID_1); + + assertGt(accRewardsPerAllocatedToken, 0); + assertEq( + accRewardsPerAllocatedToken, + _expectedRewardsPerAllocatedToken( + _expectedSubgraphRewards(ISSUANCE_RATE_PERIODS, SIGNAL, SIGNAL), + tokensFromSubgraphService + ) + ); + } + + function test_MixedAllocationsFromStakingAndSubgraphService_ZeroWhenNeitherIssuerHasAllocations() public { + _signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL); + + issuer.setSubgraphAllocatedTokens(SUBGRAPH_DEPLOYMENT_ID_1, 0); + + vm.roll(block.number + ISSUANCE_RATE_PERIODS); + + // With no allocations, rewards are reclaimed (NO_ALLOCATED_TOKENS), so both stay at zero + (uint256 accRewardsPerAllocatedToken, uint256 accRewardsForSubgraph) = rm.getAccRewardsPerAllocatedToken( + SUBGRAPH_DEPLOYMENT_ID_1 + ); + + assertEq(accRewardsPerAllocatedToken, 0); + assertEq(accRewardsForSubgraph, 0); + } + + // -- subgraph service with denylist and eligibility -- + + function test_SubgraphServiceWithDenylistAndEligibility_DeniesRewardsWhenSubgraphIsOnDenylist() public { + vm.startPrank(governor); + rm.setSubgraphAvailabilityOracle(governor); + rm.setDenied(SUBGRAPH_DEPLOYMENT_ID_1, true); + vm.stopPrank(); + + // Pending rewards > 0 so there are rewards to deny + _setActiveAllocation(ALLOCATION, 100 ether); + + vm.expectEmit(address(rm)); + emit RewardsDenied(indexer, allocationID1); + vm.prank(address(issuer)); + rm.takeRewards(allocationID1); + } + + function test_SubgraphServiceWithDenylistAndEligibility_DeniesRewardsWhenIndexerIsIneligible() public { + // Oracle denies by default + oracle.setDefaultResponse(false); + vm.prank(governor); + rm.setProviderEligibilityOracle(IProviderEligibility(address(oracle))); + + _signal(SUBGRAPH_DEPLOYMENT_ID_1, SIGNAL); + _setActiveAllocation(ALLOCATION, 0); + + vm.roll(block.number + ISSUANCE_RATE_PERIODS); + + vm.expectEmit(address(rm)); + emit RewardsDeniedDueToEligibility( + indexer, + allocationID1, + _expectedAllocationRewards(ISSUANCE_RATE_PERIODS, ALLOCATION) + ); + vm.prank(address(issuer)); + rm.takeRewards(allocationID1); + } +} diff --git a/scripts/contracts-migration/test-mapping/t5c.md b/scripts/contracts-migration/test-mapping/t5c.md new file mode 100644 index 000000000..b049044b6 --- /dev/null +++ b/scripts/contracts-migration/test-mapping/t5c.md @@ -0,0 +1,46 @@ +# T5c test mapping: RewardsManager issuance allocator and subgraph service + +New tests are in `packages/issuance`, on T5a's `RewardsManagerSharedTest` (the RewardsManager Hardhat artifact behind a GraphProxy). `RMIssuanceAllocatorMock` and `RMEligibilityOracleMock` replace `MockIssuanceAllocator` / `MockRewardsEligibilityOracle`, and `RMIssuerMock` (`issuer`) plays the old `MockSubgraphService`. `curation.mint` becomes `_signal`, `helpers.mine(n)` becomes `vm.roll`, and the `hardhat_impersonateAccount` blocks become `vm.prank(address(issuer))`. The old `beforeEach` that reset the allocator to the zero address is dropped, since the fixture starts with none. Expected values are derived from the explicit rolls with the shared formula helpers and asserted exactly. No ported case uses zero-token allocations (the carried `RMIssuerMock` hook caveat does not apply; allocations are set with `setAllocation`, as the old tests did). + +| Old file | Old test (describe › it) | New test (file::function) | Note | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › ERC-165 validation › should successfully set an issuance allocator that supports the interface | test/unit/rewards-manager/issuanceAllocator.t.sol::test_ERC165Validation_SetsAllocatorThatSupportsTheInterface | `RMIssuanceAllocatorMock` instead of `MockIssuanceAllocator` | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › ERC-165 validation › should revert when setting to EOA address (no contract code) | test/unit/rewards-manager/issuanceAllocator.t.sol::test_ERC165Validation_RevertWhen_AllocatorIsEOA | Bare `vm.expectRevert()`, as the old `.reverted` | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › ERC-165 validation › should revert when setting to contract that does not support IIssuanceAllocationDistribution | test/unit/rewards-manager/issuanceAllocator.t.sol::test_ERC165Validation_RevertWhen_InterfaceNotSupported | Issuance's `MockERC165` | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › ERC-165 validation › should validate interface before updating rewards calculation | test/unit/rewards-manager/issuanceAllocator.t.sol::test_ERC165Validation_RevertWhen_InterfaceNotSupportedBeforeRewardsUpdate | Same assertion as the old test (revert string). The title is stale: `setIssuanceAllocator` calls `updateAccRewardsPerSignal` before the check, and the whole call reverts | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › access control › should revert when called by non-governor | test/unit/rewards-manager/issuanceAllocator.t.sol::test_AccessControl_RevertWhen_CalledByNonGovernor | | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › state management › should allow setting issuance allocator to zero address (disable) | test/unit/rewards-manager/issuanceAllocator.t.sol::test_StateManagement_AllowsDisablingWithZeroAddress | | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › state management › should emit IssuanceAllocatorSet event when setting allocator | test/unit/rewards-manager/issuanceAllocator.t.sol::test_StateManagement_EmitsIssuanceAllocatorSet | | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › state management › should not emit event when setting to same allocator address | test/unit/rewards-manager/issuanceAllocator.t.sol::test_StateManagement_DoesNotEmitWhenSettingSameAllocator | `vm.recordLogs` + `_assertNotEmitted` | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuanceAllocator › state management › should update rewards before changing issuance allocator | test/unit/rewards-manager/issuanceAllocator.t.sol::test_StateManagement_UpdatesRewardsBeforeChangingAllocator | The old test only asserted the allocator was set (the comment said the update could not be tested). Kept that assertion and added `accRewardsPerSignalLastBlockUpdated == block.number` after a one-block roll, which shows the update ran | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › getAllocatedIssuancePerBlock › should return issuancePerBlock when no issuanceAllocator is set | test/unit/rewards-manager/issuanceAllocator.t.sol::test_GetAllocatedIssuancePerBlock_ReturnsIssuancePerBlockWhenNoAllocatorSet | | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › getAllocatedIssuancePerBlock › should return value from issuanceAllocator when set | test/unit/rewards-manager/issuanceAllocator.t.sol::test_GetAllocatedIssuancePerBlock_ReturnsValueFromAllocatorWhenSet | `callBefore` is `true`, as the old 4-argument call | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › getAllocatedIssuancePerBlock › should return 0 when issuanceAllocator is set but target not registered as self-minter | test/unit/rewards-manager/issuanceAllocator.t.sol::test_GetAllocatedIssuancePerBlock_ZeroWhenTargetNotRegisteredAsSelfMinter | | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › setIssuancePerBlock › should allow setIssuancePerBlock when issuanceAllocator is set | test/unit/rewards-manager/issuanceAllocator.t.sol::test_SetIssuancePerBlock_AllowedWhenAllocatorIsSet | Old `not.eq(new)` kept, plus the real value (0: no target registered in the mock) | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › beforeIssuanceAllocationChange › should handle beforeIssuanceAllocationChange correctly | test/unit/rewards-manager/issuanceAllocator.t.sol::test_BeforeIssuanceAllocationChange_HandlesCallsFromAnyoneAndTheAllocator | One-block roll before each call (Hardhat mined a block per tx); asserts `accRewardsPerSignalLastBlockUpdated` after each, which the old test did not | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › issuance allocator integration › should accumulate rewards using allocator rate over time | test/unit/rewards-manager/issuanceAllocator.t.sol::test_IssuanceAllocatorIntegration_AccumulatesRewardsUsingAllocatorRateOverTime | 10-block roll after the allocator is set; exact, plus the `not.eq` local-rate check | +| packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts | Rewards - Issuance Allocator › issuance allocator integration › should maintain reward consistency when switching between rates | test/unit/rewards-manager/issuanceAllocator.t.sol::test_IssuanceAllocatorIntegration_MaintainsRewardConsistencyWhenSwitchingBetweenRates | Block-number differences replaced by explicit rolls (5, then 1 + switch, 8, 1 + switch, 4). The one-block rolls before each switch reproduce the transition block Hardhat mined for the switch tx, so the transition terms stay non-zero. Each phase is asserted against the formula, then the total against the sum | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service configuration › should reject setSubgraphService if unauthorized | test/unit/rewards-manager/subgraphService.t.sol::test_SubgraphServiceConfiguration_RevertWhen_SetByNonGovernor | | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service configuration › should set subgraph service if governor | test/unit/rewards-manager/subgraphService.t.sol::test_SubgraphServiceConfiguration_SetsSubgraphServiceAsGovernor | Old address in the event is `issuer` (set in `setUp`), not the zero address | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service configuration › should allow setting to zero address | test/unit/rewards-manager/subgraphService.t.sol::test_SubgraphServiceConfiguration_AllowsSettingToZeroAddress | | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service configuration › should emit event when setting different address | test/unit/rewards-manager/subgraphService.t.sol::test_SubgraphServiceConfiguration_EmitsEventWhenSettingDifferentAddress | | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › getRewards from subgraph service › should calculate rewards for subgraph service allocations | test/unit/rewards-manager/subgraphService.t.sol::test_GetRewards_CalculatesRewardsForSubgraphServiceAllocations | `issuer` plays the mock subgraph service; old `gt(0)` kept, plus the exact value derived from the 10-block roll | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › getRewards from subgraph service › should return zero for inactive allocation | test/unit/rewards-manager/subgraphService.t.sol::test_GetRewards_ZeroForInactiveAllocation | | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › getRewards from subgraph service › should reject getRewards from non-rewards-issuer contract | test/unit/rewards-manager/subgraphService.t.sol::test_GetRewards_RevertWhen_NotARewardsIssuer | | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › takeRewards from subgraph service › should take rewards through subgraph service | test/unit/rewards-manager/subgraphService.t.sol::test_TakeRewards_TakesRewardsThroughSubgraphService | `vm.prank(issuer)` replaces the impersonation. Old `gt(0)` event amount, balance and supply checks are exact (derived from the 10-block roll) | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › takeRewards from subgraph service › should return zero rewards for inactive allocation | test/unit/rewards-manager/subgraphService.t.sol::test_TakeRewards_ZeroRewardsForInactiveAllocation | `EMIT_EVENT_FOR_ZERO_REWARDS` is false, so asserts no `HorizonRewardsAssigned` | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › takeRewards from subgraph service › should reject takeRewards from non-rewards-issuer contract | test/unit/rewards-manager/subgraphService.t.sol::test_TakeRewards_RevertWhen_CallerIsNotARewardsIssuer | | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › takeRewards from subgraph service › should handle zero rewards scenario | test/unit/rewards-manager/subgraphService.t.sol::test_TakeRewards_HandlesZeroRewardsScenario | Also asserts the returned amount is 0 | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › mixed allocations from staking and subgraph service › should account for both staking and subgraph service allocations in getAccRewardsPerAllocatedToken | test/unit/rewards-manager/subgraphService.t.sol::test_MixedAllocationsFromStakingAndSubgraphService_AccountsForSubgraphServiceAllocationsInAccRewardsPerAllocatedToken | Title is stale (one rewards issuer now); asserted against the single `issuer`. Old `gt(0)` kept, plus exact values | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › mixed allocations from staking and subgraph service › should handle case where only subgraph service has allocations | test/unit/rewards-manager/subgraphService.t.sol::test_MixedAllocationsFromStakingAndSubgraphService_HandlesCaseWhereOnlySubgraphServiceHasAllocations | As above; exact value added | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › mixed allocations from staking and subgraph service › should return zero when neither issuer has allocations | test/unit/rewards-manager/subgraphService.t.sol::test_MixedAllocationsFromStakingAndSubgraphService_ZeroWhenNeitherIssuerHasAllocations | As above | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › subgraph service with denylist and eligibility › should deny rewards from subgraph service when subgraph is on denylist | test/unit/rewards-manager/subgraphService.t.sol::test_SubgraphServiceWithDenylistAndEligibility_DeniesRewardsWhenSubgraphIsOnDenylist | | +| packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts | Rewards - SubgraphService › subgraph service as rewards issuer › subgraph service with denylist and eligibility › should deny rewards from subgraph service when indexer is ineligible | test/unit/rewards-manager/subgraphService.t.sol::test_SubgraphServiceWithDenylistAndEligibility_DeniesRewardsWhenIndexerIsIneligible | `RMEligibilityOracleMock` with default response `false`. The old `emit` had no args; the event now carries `amount`, asserted as the derived rewards | + +## Coverage + +`contracts/rewards/RewardsManager.sol`, external/public names in `functionsHit` that no issuance test calls yet (closed in T5f; T5d's reclaim port is expected to reach them): + +- `getDefaultReclaimAddress`: reclaim configuration, ported by T5d (`rewards-reclaim.test.ts`). +- `setDefaultReclaimAddress`: as above. +- `setReclaimAddress`: as above.