Skip to content

Commit 89def3d

Browse files
committed
feat: enumerable indexer tracking for REO and issuance constructor cleanup
Add EnumerableSet-based indexer tracking to RewardsEligibilityOracle with a helper contract for paginated queries. Introduce retention-based cleanup that removes indexers only after a configurable idle period. Split REO interface into focused sub-interfaces for administration, status, events, maintenance, and helper operations. Also refactor issuance constructors from address to IGraphToken type and normalise pragma to ^0.8.27 across issuance test files.
1 parent ec72360 commit 89def3d

31 files changed

Lines changed: 825 additions & 51 deletions

packages/interfaces/contracts/issuance/eligibility/IRewardsEligibilityAdministration.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,14 @@ interface IRewardsEligibilityAdministration is IRewardsEligibilityEvents {
3434
* @return True if successfully set (always the case for current code)
3535
*/
3636
function setEligibilityValidation(bool enabled) external returns (bool);
37+
38+
/**
39+
* @notice Set the indexer retention period for tracked indexer cleanup
40+
* @dev Only callable by accounts with the OPERATOR_ROLE. Indexers whose last
41+
* renewal timestamp is older than this period can be permissionlessly removed
42+
* from the tracked set via {IRewardsEligibilityMaintenance-removeStaleIndexer}.
43+
* @param indexerRetentionPeriod New retention period in seconds
44+
* @return True if the state is as requested (retention period is set to the specified value)
45+
*/
46+
function setIndexerRetentionPeriod(uint256 indexerRetentionPeriod) external returns (bool);
3747
}

packages/interfaces/contracts/issuance/eligibility/IRewardsEligibilityEvents.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,14 @@ interface IRewardsEligibilityEvents {
3131
/// @param oldTimeout The previous timeout period in seconds
3232
/// @param newTimeout The new timeout period in seconds
3333
event OracleUpdateTimeoutUpdated(uint256 indexed oldTimeout, uint256 indexed newTimeout);
34+
35+
/// @notice Emitted when an indexer is added to or removed from the tracked set
36+
/// @param indexer The indexer address
37+
/// @param tracked True when added (first renewal), false when removed (stale cleanup)
38+
event IndexerTrackingUpdated(address indexed indexer, bool indexed tracked);
39+
40+
/// @notice Emitted when the indexer retention period is updated
41+
/// @param oldPeriod The previous retention period in seconds
42+
/// @param newPeriod The new retention period in seconds
43+
event IndexerRetentionPeriodSet(uint256 indexed oldPeriod, uint256 indexed newPeriod);
3444
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
pragma solidity ^0.7.6 || ^0.8.0;
4+
5+
/**
6+
* @title Interface for the {RewardsEligibilityHelper} contract
7+
* @author Edge & Node
8+
* @notice Stateless, permissionless convenience contract for {RewardsEligibilityOracle}.
9+
* Provides batch removal of expired indexers from the tracked set.
10+
* Independently deployable — better versions can be deployed without protocol changes.
11+
*
12+
* @custom:security-contact Please email security+contracts@thegraph.com if you find any
13+
* bugs. We may have an active bug bounty program.
14+
*/
15+
interface IRewardsEligibilityHelper {
16+
/**
17+
* @notice Remove expired indexers from the tracked set by explicit address list
18+
* @dev Calls {IRewardsEligibilityMaintenance-removeExpiredIndexer} for each address.
19+
* @param indexers Array of indexer addresses to check and remove
20+
* @return gone Number of indexers now absent from the tracked set
21+
*/
22+
function removeExpiredIndexers(address[] calldata indexers) external returns (uint256 gone);
23+
24+
/**
25+
* @notice Remove all expired indexers from the tracked set
26+
* @dev Snapshots the full tracked set then calls
27+
* {IRewardsEligibilityMaintenance-removeExpiredIndexer} for each.
28+
* May be expensive for large sets; prefer the paginated overload for gas-bounded calls.
29+
* @return gone Number of indexers now absent from the tracked set
30+
*/
31+
function removeExpiredIndexers() external returns (uint256 gone);
32+
33+
/**
34+
* @notice Remove expired indexers from the tracked set by paginated scan
35+
* @dev Reads a slice of the tracked set via {IRewardsEligibilityStatus-getIndexers}
36+
* and calls {IRewardsEligibilityMaintenance-removeExpiredIndexer} for each.
37+
* Note: removals shift set indices between pages, so some indexers may be skipped
38+
* across consecutive paginated calls. Use the parameterless overload to process all.
39+
* @param offset Start index into the tracked indexer set
40+
* @param count Maximum number of indexers to process
41+
* @return gone Number of indexers now absent from the tracked set
42+
*/
43+
function removeExpiredIndexers(uint256 offset, uint256 count) external returns (uint256 gone);
44+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
pragma solidity ^0.7.6 || ^0.8.0;
4+
5+
import { IRewardsEligibilityEvents } from "./IRewardsEligibilityEvents.sol";
6+
7+
/**
8+
* @title IRewardsEligibilityMaintenance
9+
* @author Edge & Node
10+
* @notice Interface for permissionless maintenance of the tracked indexer set.
11+
* Allows anyone to remove indexers whose last renewal is older than the
12+
* configured indexer retention period.
13+
*/
14+
interface IRewardsEligibilityMaintenance is IRewardsEligibilityEvents {
15+
/**
16+
* @notice Remove an expired indexer from the tracked set
17+
* @dev Permissionless. An indexer is expired when
18+
* `block.timestamp >= renewalTimestamp + indexerRetentionPeriod`.
19+
* Removes the indexer from the enumerable set and deletes its renewal timestamp.
20+
* No-op (returns true) if the indexer is not in the tracked set.
21+
* @param indexer The indexer address to remove
22+
* @return gone True if the indexer is absent from the tracked set (whether removed
23+
* by this call or already not tracked); false if the indexer is still tracked (not expired)
24+
*/
25+
function removeExpiredIndexer(address indexer) external returns (bool gone);
26+
}

packages/interfaces/contracts/issuance/eligibility/IRewardsEligibilityStatus.sol

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,31 @@ interface IRewardsEligibilityStatus {
3939
* @return True if eligibility validation is enabled, false otherwise
4040
*/
4141
function getEligibilityValidation() external view returns (bool);
42+
43+
/**
44+
* @notice Get the indexer retention period for tracked indexer cleanup
45+
* @return The current indexer retention period in seconds
46+
*/
47+
function getIndexerRetentionPeriod() external view returns (uint256);
48+
49+
/**
50+
* @notice Get the number of tracked indexers
51+
* @return count The number of indexers in the tracked set
52+
*/
53+
function getIndexerCount() external view returns (uint256 count);
54+
55+
/**
56+
* @notice Get all tracked indexer addresses
57+
* @dev May be expensive for large sets — prefer the paginated overload for on-chain use.
58+
* @return result Array of tracked indexer addresses
59+
*/
60+
function getIndexers() external view returns (address[] memory result);
61+
62+
/**
63+
* @notice Get a paginated slice of tracked indexer addresses
64+
* @param offset The index to start from
65+
* @param count Maximum number to return (clamped to available)
66+
* @return result Array of tracked indexer addresses
67+
*/
68+
function getIndexers(uint256 offset, uint256 count) external view returns (address[] memory result);
4269
}

packages/issuance/contracts/allocate/DirectAllocation.sol

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pragma solidity ^0.8.27;
55
import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol";
66
import { ISendTokens } from "@graphprotocol/interfaces/contracts/issuance/allocate/ISendTokens.sol";
77
import { BaseUpgradeable } from "../common/BaseUpgradeable.sol";
8+
import { IGraphToken } from "../common/IGraphToken.sol";
89

910
// solhint-disable-next-line no-unused-import
1011
import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; // Used by @inheritdoc
@@ -38,19 +39,16 @@ contract DirectAllocation is BaseUpgradeable, IIssuanceTarget, ISendTokens {
3839
event TokensSent(address indexed to, uint256 indexed amount);
3940
// Do not need to index amount, ignoring gas-indexed-events warning.
4041

41-
/// @notice Emitted before the issuance allocation changes
42-
event BeforeIssuanceAllocationChange();
43-
4442
// -- Constructor --
4543

4644
/**
4745
* @notice Constructor for the DirectAllocation contract
4846
* @dev This contract is upgradeable, but we use the constructor to pass the Graph Token address
4947
* to the base contract.
50-
* @param graphToken Address of the Graph Token contract
48+
* @param graphToken The Graph Token contract
5149
* @custom:oz-upgrades-unsafe-allow constructor
5250
*/
53-
constructor(address graphToken) BaseUpgradeable(graphToken) {}
51+
constructor(IGraphToken graphToken) BaseUpgradeable(graphToken) {}
5452

5553
// -- Initialization --
5654

@@ -89,9 +87,7 @@ contract DirectAllocation is BaseUpgradeable, IIssuanceTarget, ISendTokens {
8987
* before an allocation change. We simply receive tokens from the IssuanceAllocator.
9088
* @inheritdoc IIssuanceTarget
9189
*/
92-
function beforeIssuanceAllocationChange() external virtual override {
93-
emit BeforeIssuanceAllocationChange();
94-
}
90+
function beforeIssuanceAllocationChange() external virtual override {}
9591

9692
/**
9793
* @dev No-op for DirectAllocation; issuanceAllocator is not stored.

packages/issuance/contracts/allocate/IssuanceAllocator.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IIssuanceAllocationStatus } from "@graphprotocol/interfaces/contracts/i
1515
import { IIssuanceAllocationData } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationData.sol";
1616
import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol";
1717
import { BaseUpgradeable } from "../common/BaseUpgradeable.sol";
18+
import { IGraphToken } from "../common/IGraphToken.sol";
1819
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
1920
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
2021

@@ -324,10 +325,10 @@ contract IssuanceAllocator is
324325
* @notice Constructor for the IssuanceAllocator contract
325326
* @dev This contract is upgradeable, but we use the constructor to pass the Graph Token address
326327
* to the base contract.
327-
* @param _graphToken Address of the Graph Token contract
328+
* @param _graphToken The Graph Token contract
328329
* @custom:oz-upgrades-unsafe-allow constructor
329330
*/
330-
constructor(address _graphToken) BaseUpgradeable(_graphToken) {}
331+
constructor(IGraphToken _graphToken) BaseUpgradeable(_graphToken) {}
331332

332333
// -- Initialization --
333334

packages/issuance/contracts/common/BaseUpgradeable.sol

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ abstract contract BaseUpgradeable is
8787
* @notice Constructor for the BaseUpgradeable contract
8888
* @dev This contract is upgradeable, but we use the constructor to set immutable variables
8989
* and disable initializers to prevent the implementation contract from being initialized.
90-
* @param graphToken Address of the Graph Token contract
90+
* @param graphToken The Graph Token contract
9191
* @custom:oz-upgrades-unsafe-allow constructor
9292
*/
93-
constructor(address graphToken) {
94-
require(graphToken != address(0), GraphTokenCannotBeZeroAddress());
95-
GRAPH_TOKEN = IGraphToken(graphToken);
93+
constructor(IGraphToken graphToken) {
94+
require(address(graphToken) != address(0), GraphTokenCannotBeZeroAddress());
95+
GRAPH_TOKEN = graphToken;
9696
_disableInitializers();
9797
}
9898

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
pragma solidity ^0.8.27;
4+
5+
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
6+
7+
/**
8+
* @title EnumerableSetUtil
9+
* @author Edge & Node
10+
* @notice Pagination helpers for OpenZeppelin EnumerableSet types.
11+
*/
12+
library EnumerableSetUtil {
13+
using EnumerableSet for EnumerableSet.AddressSet;
14+
using EnumerableSet for EnumerableSet.Bytes32Set;
15+
16+
/**
17+
* @notice Return a page of addresses from an AddressSet.
18+
* @param set The enumerable address set to paginate
19+
* @param offset Number of entries to skip
20+
* @param count Maximum number of entries to return
21+
* @return result Array of addresses (may be shorter than count)
22+
*/
23+
function getPage(
24+
EnumerableSet.AddressSet storage set,
25+
uint256 offset,
26+
uint256 count
27+
) internal view returns (address[] memory result) {
28+
uint256 total = set.length();
29+
// solhint-disable-next-line gas-strict-inequalities
30+
if (total <= offset) return new address[](0);
31+
32+
uint256 remaining = total - offset;
33+
if (remaining < count) count = remaining;
34+
35+
result = new address[](count);
36+
for (uint256 i = 0; i < count; ++i) result[i] = set.at(offset + i);
37+
}
38+
39+
/**
40+
* @notice Return a page of bytes16 ids from a Bytes32Set (truncating each entry).
41+
* @param set The enumerable bytes32 set to paginate
42+
* @param offset Number of entries to skip
43+
* @param count Maximum number of entries to return
44+
* @return result Array of bytes16 values (may be shorter than count)
45+
*/
46+
function getPageBytes16(
47+
EnumerableSet.Bytes32Set storage set,
48+
uint256 offset,
49+
uint256 count
50+
) internal view returns (bytes16[] memory result) {
51+
uint256 total = set.length();
52+
// solhint-disable-next-line gas-strict-inequalities
53+
if (total <= offset) return new bytes16[](0);
54+
55+
uint256 remaining = total - offset;
56+
if (remaining < count) count = remaining;
57+
58+
result = new bytes16[](count);
59+
for (uint256 i = 0; i < count; ++i) result[i] = bytes16(set.at(offset + i));
60+
}
61+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
pragma solidity ^0.8.27;
4+
5+
import { IRewardsEligibilityHelper } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityHelper.sol";
6+
import { IRewardsEligibilityMaintenance } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityMaintenance.sol";
7+
import { IRewardsEligibilityStatus } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityStatus.sol";
8+
9+
/**
10+
* @title RewardsEligibilityHelper
11+
* @author Edge & Node
12+
* @notice Stateless, permissionless convenience contract for {RewardsEligibilityOracle}.
13+
* Provides batch removal of expired indexers from the tracked set.
14+
* Independently deployable — better versions can be deployed without protocol changes.
15+
*
16+
* @custom:security-contact Please email security+contracts@thegraph.com if you find any
17+
* bugs. We may have an active bug bounty program.
18+
*/
19+
contract RewardsEligibilityHelper is IRewardsEligibilityHelper {
20+
/// @notice The RewardsEligibilityOracle contract address
21+
address public immutable ORACLE;
22+
23+
/// @notice Thrown when an address parameter is the zero address
24+
error ZeroAddress();
25+
26+
/**
27+
* @notice Constructor for the RewardsEligibilityHelper contract
28+
* @param oracle Address of the RewardsEligibilityOracle contract
29+
*/
30+
constructor(address oracle) {
31+
require(oracle != address(0), ZeroAddress());
32+
ORACLE = oracle;
33+
}
34+
35+
/// @inheritdoc IRewardsEligibilityHelper
36+
function removeExpiredIndexers(address[] calldata indexers) external returns (uint256 gone) {
37+
for (uint256 i = 0; i < indexers.length; ++i)
38+
if (IRewardsEligibilityMaintenance(ORACLE).removeExpiredIndexer(indexers[i])) ++gone;
39+
}
40+
41+
/// @inheritdoc IRewardsEligibilityHelper
42+
function removeExpiredIndexers() external returns (uint256 gone) {
43+
address[] memory indexers = IRewardsEligibilityStatus(ORACLE).getIndexers();
44+
for (uint256 i = 0; i < indexers.length; ++i)
45+
if (IRewardsEligibilityMaintenance(ORACLE).removeExpiredIndexer(indexers[i])) ++gone;
46+
}
47+
48+
/// @inheritdoc IRewardsEligibilityHelper
49+
function removeExpiredIndexers(uint256 offset, uint256 count) external returns (uint256 gone) {
50+
address[] memory indexers = IRewardsEligibilityStatus(ORACLE).getIndexers(offset, count);
51+
for (uint256 i = 0; i < indexers.length; ++i)
52+
if (IRewardsEligibilityMaintenance(ORACLE).removeExpiredIndexer(indexers[i])) ++gone;
53+
}
54+
}

0 commit comments

Comments
 (0)