Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
id: coverage_files
run: |
# Find all coverage-final.json files
COVERAGE_FILES=$(find ./packages -name "coverage-final.json" -path "*/reports/coverage/*" | tr '\n' ',' | sed 's/,$//')
COVERAGE_FILES=$(find ./packages -name "coverage-final.json" -path "*/coverage/*" | tr '\n' ',' | sed 's/,$//')
echo "files=$COVERAGE_FILES" >> $GITHUB_OUTPUT
echo "Found coverage files: $COVERAGE_FILES"

Expand Down
23 changes: 0 additions & 23 deletions packages/contracts/.solcover.js

This file was deleted.

53 changes: 51 additions & 2 deletions packages/contracts/contracts/rewards/RewardsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@
// TODO: Re-enable and fix issues when publishing a new version
// solhint-disable gas-increment-by-one, gas-indexed-events, gas-small-strings, gas-strict-inequalities

import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";

Check warning on line 9 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @openzeppelin/contracts/math/SafeMath.sol
import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol";

Check warning on line 10 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @openzeppelin/contracts/introspection/IERC165.sol

import { GraphUpgradeable } from "../upgrades/GraphUpgradeable.sol";
import { Managed } from "../governance/Managed.sol";
import { MathUtils } from "../staking/libs/MathUtils.sol";
import { IGraphToken } from "../token/IGraphToken.sol";

import { RewardsManagerV5Storage } from "./RewardsManagerStorage.sol";
import { RewardsManagerV6Storage } from "./RewardsManagerStorage.sol";
import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol";

Check warning on line 18 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol
import { IRewardsIssuer } from "./IRewardsIssuer.sol";
import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";

Check warning on line 20 in packages/contracts/contracts/rewards/RewardsManager.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManager.sol doesn't exist in: @graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol

/**
* @title Rewards Manager Contract
Expand All @@ -37,7 +39,7 @@
* until the actual takeRewards function is called.
* custom:security-contact Please email security+contracts@ thegraph.com (remove space) if you find any bugs. We might have an active bug bounty program.
*/
contract RewardsManager is RewardsManagerV5Storage, GraphUpgradeable, IRewardsManager {
contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IRewardsManager {
using SafeMath for uint256;

/// @dev Fixed point scaling factor used for decimals in reward calculations
Expand All @@ -61,6 +63,14 @@
*/
event RewardsDenied(address indexed indexer, address indexed allocationID);

/**
* @notice Emitted when rewards are denied to an indexer due to eligibility
* @param indexer Address of the indexer being denied rewards
* @param allocationID Address of the allocation being denied rewards
* @param amount Amount of rewards that would have been assigned
*/
event RewardsDeniedDueToEligibility(address indexed indexer, address indexed allocationID, uint256 amount);

/**
* @notice Emitted when a subgraph is denied for claiming rewards
* @param subgraphDeploymentID Subgraph deployment ID being denied
Expand All @@ -75,6 +85,16 @@
*/
event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService);

/**
* @notice Emitted when the rewards eligibility oracle contract is set
* @param oldRewardsEligibilityOracle Previous rewards eligibility oracle address
* @param newRewardsEligibilityOracle New rewards eligibility oracle address
*/
event RewardsEligibilityOracleSet(
address indexed oldRewardsEligibilityOracle,
address indexed newRewardsEligibilityOracle
);

// -- Modifiers --

/**
Expand Down Expand Up @@ -151,6 +171,28 @@
emit SubgraphServiceSet(oldSubgraphService, _subgraphService);
}

/**
* @inheritdoc IRewardsManager
* @dev Note that the rewards eligibility oracle can be set to the zero address to disable use of an oracle, in
* which case no indexers will be denied rewards due to eligibility.
*/
function setRewardsEligibilityOracle(address newRewardsEligibilityOracle) external override onlyGovernor {
if (address(rewardsEligibilityOracle) != newRewardsEligibilityOracle) {
// Check that the contract supports the IRewardsEligibilityOracle interface
// Allow zero address to disable the oracle
if (newRewardsEligibilityOracle != address(0)) {
require(
IERC165(newRewardsEligibilityOracle).supportsInterface(type(IRewardsEligibilityOracle).interfaceId),
"Contract does not support IRewardsEligibilityOracle interface"
);
}

address oldRewardsEligibilityOracle = address(rewardsEligibilityOracle);
rewardsEligibilityOracle = IRewardsEligibilityOracle(newRewardsEligibilityOracle);
emit RewardsEligibilityOracleSet(oldRewardsEligibilityOracle, newRewardsEligibilityOracle);
}
}

// -- Denylist --

/**
Expand Down Expand Up @@ -404,6 +446,13 @@
rewards = accRewardsPending.add(
_calcRewards(tokens, accRewardsPerAllocatedToken, updatedAccRewardsPerAllocatedToken)
);

// Do not reward if indexer is not eligible based on rewards eligibility
if (address(rewardsEligibilityOracle) != address(0) && !rewardsEligibilityOracle.isEligible(indexer)) {
emit RewardsDeniedDueToEligibility(indexer, _allocationID, rewards);
return 0;
}

if (rewards > 0) {
// Mint directly to rewards issuer for the reward amount
// The rewards issuer contract will do bookkeeping of the reward and
Expand Down
11 changes: 11 additions & 0 deletions packages/contracts/contracts/rewards/RewardsManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

pragma solidity ^0.7.6 || 0.8.27;

import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";

Check warning on line 10 in packages/contracts/contracts/rewards/RewardsManagerStorage.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManagerStorage.sol doesn't exist in: @graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol
import { IRewardsIssuer } from "./IRewardsIssuer.sol";
import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol";

Check warning on line 12 in packages/contracts/contracts/rewards/RewardsManagerStorage.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/rewards/RewardsManagerStorage.sol doesn't exist in: @graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol
import { Managed } from "../governance/Managed.sol";

/**
Expand Down Expand Up @@ -75,3 +76,13 @@
/// @notice Address of the subgraph service
IRewardsIssuer public subgraphService;
}

/**
* @title RewardsManagerV6Storage
* @author Edge & Node
* @notice Storage layout for RewardsManager V6
*/
contract RewardsManagerV6Storage is RewardsManagerV5Storage {
/// @notice Address of the rewards eligibility oracle contract
IRewardsEligibilityOracle public rewardsEligibilityOracle;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-or-later

// solhint-disable named-parameters-mapping

pragma solidity 0.7.6;

import { IRewardsEligibilityOracle } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol";

Check warning on line 7 in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol doesn't exist in: @graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityOracle.sol
import { ERC165 } from "@openzeppelin/contracts/introspection/ERC165.sol";

Check warning on line 8 in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/contracts/contracts/tests/MockRewardsEligibilityOracle.sol doesn't exist in: @openzeppelin/contracts/introspection/ERC165.sol

/**
* @title MockRewardsEligibilityOracle
* @author Edge & Node
* @notice A simple mock contract for the RewardsEligibilityOracle interface
* @dev A simple mock contract for the RewardsEligibilityOracle interface
*/
contract MockRewardsEligibilityOracle is IRewardsEligibilityOracle, ERC165 {
/// @dev Mapping to store eligibility status for each indexer
mapping(address => bool) private eligible;

/// @dev Mapping to track which indexers have been explicitly set
mapping(address => bool) private isSet;

/// @dev Default response for indexers not explicitly set
bool private defaultResponse;

/**
* @notice Constructor
* @param newDefaultResponse Default response for isEligible
*/
constructor(bool newDefaultResponse) {
defaultResponse = newDefaultResponse;
}

/**
* @notice Set whether a specific indexer is eligible
* @param indexer The indexer address
* @param eligibility Whether the indexer is eligible
*/
function setIndexerEligible(address indexer, bool eligibility) external {
eligible[indexer] = eligibility;
isSet[indexer] = true;
}

/**
* @notice Set the default response for indexers not explicitly set
* @param newDefaultResponse The default response
*/
function setDefaultResponse(bool newDefaultResponse) external {
defaultResponse = newDefaultResponse;
}

/**
* @inheritdoc IRewardsEligibilityOracle
*/
function isEligible(address indexer) external view override returns (bool) {
// If the indexer has been explicitly set, return that value
if (isSet[indexer]) {
return eligible[indexer];
}

// Otherwise return the default response
return defaultResponse;
}

/**
* @inheritdoc ERC165
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IRewardsEligibilityOracle).interfaceId || super.supportsInterface(interfaceId);
}
}
3 changes: 2 additions & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@
],
"scripts": {
"prepack": "pnpm build",
"clean": "rm -rf artifacts/ cache/ types/ abis/ build/ dist/",
"clean": "rm -rf artifacts/ cache/ types/ abis/ build/ dist/ coverage/",
"build": "pnpm build:self",
"build:self": "pnpm compile",
"compile": "hardhat compile",
"test": "pnpm --filter @graphprotocol/contracts-tests test",
"test:coverage": "pnpm --filter @graphprotocol/contracts-tests run test:coverage",
"deploy": "pnpm predeploy && pnpm build",
"deploy-localhost": "pnpm build",
"predeploy": "scripts/predeploy",
Expand Down
12 changes: 3 additions & 9 deletions packages/contracts/test/.solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ module.exports = {
network_id: 1337,
},
skipFiles,
istanbulFolder: './reports/coverage',
istanbulFolder: '../coverage',
// Remove 'html' reporter to avoid duplicates, keep lcov for lcov.info
istanbulReporter: ['lcov', 'text', 'json'],
configureYulOptimizer: true,
mocha: {
grep: '@skip-on-coverage',
invert: true,
},
onCompileComplete: async function (/* config */) {
// Set environment variable to indicate we're running under coverage
process.env.SOLIDITY_COVERAGE = 'true'
},
onIstanbulComplete: async function (/* config */) {
// Clean up environment variable
delete process.env.SOLIDITY_COVERAGE
},
}
2 changes: 1 addition & 1 deletion packages/contracts/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
},
"scripts": {
"postinstall": "scripts/setup-symlinks",
"clean": "rm -rf artifacts/ cache/ reports/ types/",
"clean": "rm -rf artifacts/ cache/ types/",
"build": "pnpm build:dep",
"build:dep": "pnpm --filter '@graphprotocol/contracts-tests^...' run build:self",
"test": "pnpm build && pnpm test:self",
Expand Down
1 change: 0 additions & 1 deletion packages/contracts/test/scripts/coverage
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ set -eo pipefail
echo {} > addresses-local.json

DISABLE_SECURE_ACCOUNTS=true \
SOLIDITY_COVERAGE=true \
L1_GRAPH_CONFIG=config/graph.hardhat.yml \
L2_GRAPH_CONFIG=config/graph.arbitrum-hardhat.yml \
ADDRESS_BOOK=addresses-local.json \
Expand Down
30 changes: 6 additions & 24 deletions packages/contracts/test/tests/unit/gns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { BigNumber, ContractTransaction, ethers, Event } from 'ethers'
import { defaultAbiCoder } from 'ethers/lib/utils'
import hre from 'hardhat'

import { isRunningUnderCoverage } from '../../utils/coverage'
import { NetworkFixture } from './lib/fixtures'
import {
AccountDefaultName,
Expand Down Expand Up @@ -787,16 +788,9 @@ describe.skip('L1GNS @skip-on-coverage', () => {
const tx = gns.connect(me).multicall([bogusPayload, tx2.data])

// Under coverage, the error message may be different due to instrumentation
const isRunningUnderCoverage =
hre.network.name === 'coverage' ||
process.env.SOLIDITY_COVERAGE === 'true' ||
process.env.npm_lifecycle_event === 'test:coverage'

if (isRunningUnderCoverage) {
// Under coverage, the transaction should still revert, but the message might be empty
if (isRunningUnderCoverage()) {
await expect(tx).to.be.reverted
} else {
// Normal test run should have the specific error message
await expect(tx).revertedWith("function selector was not recognized and there's no fallback function")
}
})
Expand Down Expand Up @@ -1295,14 +1289,8 @@ describe.skip('L1GNS @skip-on-coverage', () => {
await expect(tx2).revertedWith('NO_SIGNAL')
})
it('sets the curator signal to zero so they cannot withdraw', async function () {
// Check if we're running under coverage
const isRunningUnderCoverage =
hre.network.name === 'coverage' ||
process.env.SOLIDITY_COVERAGE === 'true' ||
process.env.npm_lifecycle_event === 'test:coverage'

if (isRunningUnderCoverage) {
// Under coverage, skip this test as it has issues with BigNumber values
// Under coverage, skip this test as it has issues with BigNumber values
if (isRunningUnderCoverage()) {
this.skip()
return
}
Expand All @@ -1328,14 +1316,8 @@ describe.skip('L1GNS @skip-on-coverage', () => {
await expect(tx).revertedWith('GNS: No signal to withdraw GRT')
})
it('gives each curator an amount of tokens proportional to their nSignal', async function () {
// Check if we're running under coverage
const isRunningUnderCoverage =
hre.network.name === 'coverage' ||
process.env.SOLIDITY_COVERAGE === 'true' ||
process.env.npm_lifecycle_event === 'test:coverage'

if (isRunningUnderCoverage) {
// Under coverage, skip this test as it has issues with BigNumber values
// Under coverage, skip this test as it has issues with BigNumber values
if (isRunningUnderCoverage()) {
this.skip()
return
}
Expand Down
Loading
Loading