Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
538e004
feat: issuance allocation contracts
RembrandtK Oct 8, 2025
01b12fe
fix: resolve coverage build logic error after contract changes
RembrandtK Oct 8, 2025
d811ba5
feat: integrate IssuanceAllocator into RewardsManager
RembrandtK Oct 8, 2025
121337f
feat: add interface ID auto-generation system
RembrandtK Oct 8, 2025
1eef7b1
fix: subgraph-service MockRewardsManager
RembrandtK Oct 8, 2025
afbb544
feat: move interface ID generation to interfaces package
RembrandtK Oct 8, 2025
8ffbbdd
fix: resolve lint issues and centralize interface ID generation
RembrandtK Oct 8, 2025
82ec319
fix: address PR comments and improve import consistency
RembrandtK Oct 8, 2025
65f0947
chore: removing unused code
RembrandtK Oct 9, 2025
25cfb80
docs(issuance): add README and remove LICENSE reference
RembrandtK Oct 9, 2025
080b252
docs(issuance): add contract documentation references to README
RembrandtK Oct 9, 2025
e17797a
feat: not only IA can call beforeIssuanceAllocationChange()
RembrandtK Oct 9, 2025
b589fee
feat: replace Python interface ID generation with Node.js
RembrandtK Oct 13, 2025
384252f
chore: lint fixes
RembrandtK Oct 13, 2025
d90c57d
feat: split IIssuanceAllocator into role-based sub-interfaces
RembrandtK Oct 13, 2025
ebe8f73
feat: split IRewardsEligibilityOracle into role-based sub-interfaces
RembrandtK Oct 13, 2025
be61051
fix: add missing override keywords to interface implementations
RembrandtK Oct 13, 2025
ce07a80
feat: add complete interface coverage for issuance contracts
RembrandtK Oct 13, 2025
dc800f3
feat: pnpm upgrade
RembrandtK Oct 14, 2025
0e9abee
chore: add Codecov config to ignore test and mock files
RembrandtK Oct 14, 2025
f54f5de
feat: enhance solhint-disable verification script with unit tests
RembrandtK Oct 14, 2025
1b32efa
fix: remove unnecessary solhint-disable rule in MockIssuanceAllocator
RembrandtK Oct 14, 2025
cc386dc
refactor: use ERC165Upgradeable pattern in RewardsManager
RembrandtK Oct 14, 2025
894b410
refactor: implement direct IERC165 in RewardsManager
RembrandtK Oct 14, 2025
e28473c
feat: upgrade OpenZeppelin contracts from v3.4.1 to v3.4.2
RembrandtK Oct 7, 2025
e5d4d8f
chore: merge improvements from build-lint-upgrade
RembrandtK Oct 14, 2025
80debce
refactor: convert TODO check from Python to JavaScript
RembrandtK Oct 14, 2025
a2cffec
refactor: convert bytecode comparison script from Python to JavaScript
RembrandtK Oct 14, 2025
f476866
refactor: convert solhint verification scripts from CommonJS to ES mo…
RembrandtK Oct 14, 2025
3056af5
chore: removing unneeded solhint-disables
RembrandtK Oct 14, 2025
01f4768
chore: removing redundant test file
RembrandtK Oct 14, 2025
991f392
chore: comment cleanup
RembrandtK Oct 14, 2025
20f40a5
chore: standardize package.json metadata and structure across packages
RembrandtK Oct 14, 2025
a5dd22a
test: separate issuance-related tests from rewards.test.ts
RembrandtK Oct 14, 2025
201f618
test: combine duplicate issuance allocator test files
RembrandtK Oct 14, 2025
e84c35e
chore: moving IERC165 to start
RembrandtK Oct 14, 2025
855d0f9
chore: removing unneeded comment
RembrandtK Oct 14, 2025
6320402
chore: restoring toolshed author
RembrandtK Oct 14, 2025
7b0634b
fix: test to take test files from config
RembrandtK Oct 14, 2025
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
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ const eslintConfig = [
...globals.mocha,
},
},
rules: {
// Allow 'any' types in test files where they're often necessary for testing edge cases
'@typescript-eslint/no-explicit-any': 'off',
},
},

// Add Hardhat globals for hardhat config files
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"lint:yaml": "npx yaml-lint .github/**/*.{yml,yaml} packages/contracts/task/config/*.yml; prettier -w --cache --log-level warn '**/*.{yml,yaml}'",
"format": "prettier -w --cache --log-level warn '**/*.{js,ts,cjs,mjs,jsx,tsx,json,md,yaml,yml}'",
"test": "pnpm build && pnpm -r run test:self",
"test:coverage": "pnpm build && pnpm -r run test:coverage:self"
"test:coverage": "pnpm build && pnpm -r run build:self:coverage && pnpm -r run test:coverage:self"
},
"devDependencies": {
"@changesets/cli": "catalog:",
Expand Down
2 changes: 0 additions & 2 deletions packages/contracts/test/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ const config: HardhatUserConfig = {
// Graph Protocol extensions
graphConfig: path.join(configDir, 'graph.hardhat.yml'),
addressBook: process.env.ADDRESS_BOOK || 'addresses.json',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
localhost: {
chainId: 1337,
Expand All @@ -75,7 +74,6 @@ const config: HardhatUserConfig = {
currency: 'USD',
outputFile: 'reports/gas-report.log',
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any

export default config
213 changes: 213 additions & 0 deletions packages/interfaces/contracts/issuance/allocate/IIssuanceAllocator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6 || ^0.8.0;
pragma abicoder v2;

/**
* @notice Target issuance per block information
* @param allocatorIssuancePerBlock Issuance per block for allocator-minting (non-self-minting)
* @param allocatorIssuanceBlockAppliedTo The block up to which allocator issuance has been applied
* @param selfIssuancePerBlock Issuance per block for self-minting
* @param selfIssuanceBlockAppliedTo The block up to which self issuance has been applied
*/
struct TargetIssuancePerBlock {
uint256 allocatorIssuancePerBlock;
uint256 allocatorIssuanceBlockAppliedTo;
uint256 selfIssuancePerBlock;
uint256 selfIssuanceBlockAppliedTo;
}

/**
* @notice Allocation information
* @param totalAllocationPPM Total allocation in PPM (allocatorMintingAllocationPPM + selfMintingAllocationPPM)
* @param allocatorMintingPPM Allocator-minting allocation in PPM (Parts Per Million)
* @param selfMintingPPM Self-minting allocation in PPM (Parts Per Million)
*/
struct Allocation {
uint256 totalAllocationPPM;
uint256 allocatorMintingPPM;
uint256 selfMintingPPM;
}

/**
* @notice Allocation target information
* @param allocatorMintingPPM The allocator-minting allocation amount in PPM (Parts Per Million)
* @param selfMintingPPM The self-minting allocation amount in PPM (Parts Per Million)
* @param lastChangeNotifiedBlock Last block when this target was notified of changes
*/
struct AllocationTarget {
uint256 allocatorMintingPPM;
uint256 selfMintingPPM;
uint256 lastChangeNotifiedBlock;
}

/**
* @title IIssuanceAllocator
* @author Edge & Node
* @notice Interface for the IssuanceAllocator contract, which is responsible for
* allocating token issuance to different components of the protocol.
*
* @dev The allocation model distinguishes between two types of targets:
* 1. Self-minting contracts: These can mint tokens themselves and are supported
* primarily for backwards compatibility with existing contracts.
* 2. Non-self-minting contracts: These cannot mint tokens themselves and rely on
* their issuanceallocator to mint tokens for them.
*/
interface IIssuanceAllocator {
/**
* @notice Distribute issuance to allocated non-self-minting targets.
* @return Block number that issuance has beee distributed to. That will normally be the current block number, unless the contract is paused.
*
* @dev When the contract is paused, no issuance is distributed and lastIssuanceBlock is not updated.
*/
function distributeIssuance() external returns (uint256);

/**
* @notice Set the issuance per block.
* @param newIssuancePerBlock New issuance per block
* @param evenIfDistributionPending If true, set even if there is pending issuance distribution
* @return True if the value is applied (including if already the case), false if not applied due to paused state
*/
function setIssuancePerBlock(uint256 newIssuancePerBlock, bool evenIfDistributionPending) external returns (bool);

/**
* @notice Set the allocation for a target with only allocator minting
* @param target Address of the target to update
* @param allocatorMintingPPM Allocator-minting allocation for the target (in PPM)
* @return True if the value is applied (including if already the case), false if not applied
* @dev This variant sets selfMintingPPM to 0 and evenIfDistributionPending to false
*/
function setTargetAllocation(address target, uint256 allocatorMintingPPM) external returns (bool);

/**
* @notice Set the allocation for a target with both allocator and self minting
* @param target Address of the target to update
* @param allocatorMintingPPM Allocator-minting allocation for the target (in PPM)
* @param selfMintingPPM Self-minting allocation for the target (in PPM)
* @return True if the value is applied (including if already the case), false if not applied
* @dev This variant sets evenIfDistributionPending to false
*/
function setTargetAllocation(
address target,
uint256 allocatorMintingPPM,
uint256 selfMintingPPM
) external returns (bool);

/**
* @notice Set the allocation for a target
* @param target Address of the target to update
* @param allocatorMintingPPM Allocator-minting allocation for the target (in PPM)
* @param selfMintingPPM Self-minting allocation for the target (in PPM)
* @param evenIfDistributionPending Whether to force the allocation change even if issuance has not been distributed up to the current block
* @return True if the value is applied (including if already the case), false if not applied
*/
function setTargetAllocation(
address target,
uint256 allocatorMintingPPM,
uint256 selfMintingPPM,
bool evenIfDistributionPending
) external returns (bool);

/**
* @notice Notify a specific target about an upcoming allocation change
* @param target Address of the target to notify
* @return True if notification was sent or already sent this block, false otherwise
*/
function notifyTarget(address target) external returns (bool);

/**
* @notice Force set the lastChangeNotifiedBlock for a target to a specific block number
* @param target Address of the target to update
* @param blockNumber Block number to set as the lastChangeNotifiedBlock
* @return The block number that was set
* @dev This can be used to enable notification to be sent again (by setting to a past block)
* @dev or to prevent notification until a future block (by setting to current or future block).
*/
function forceTargetNoChangeNotificationBlock(address target, uint256 blockNumber) external returns (uint256);

/**
* @notice Distribute any pending accumulated issuance to allocator-minting targets.
* @return Block number up to which issuance has been distributed
* @dev This function can be called even when the contract is paused.
* @dev If there is no pending issuance, this function is a no-op.
* @dev If allocatorMintingAllowance is 0 (all targets are self-minting), this function is a no-op.
*/
function distributePendingIssuance() external returns (uint256);

/**
* @notice Distribute any pending accumulated issuance to allocator-minting targets, accumulating up to a specific block.
* @param toBlockNumber The block number to accumulate pending issuance up to (must be >= lastIssuanceAccumulationBlock and <= current block)
* @return Block number up to which issuance has been distributed
* @dev This function can be called even when the contract is paused.
* @dev Accumulates pending issuance up to the specified block, then distributes all accumulated issuance.
* @dev If there is no pending issuance after accumulation, this function is a no-op for distribution.
* @dev If allocatorMintingAllowance is 0 (all targets are self-minting), this function is a no-op for distribution.
*/
function distributePendingIssuance(uint256 toBlockNumber) external returns (uint256);

/**
* @notice Get the current allocation for a target
* @param target Address of the target
* @return Allocation struct containing total, allocator-minting, and self-minting allocations
*/
function getTargetAllocation(address target) external view returns (Allocation memory);

/**
* @notice Get the current global allocation totals
* @return Allocation struct containing total, allocator-minting, and self-minting allocations across all targets
*/
function getTotalAllocation() external view returns (Allocation memory);

/**
* @notice Get all allocated target addresses
* @return Array of target addresses
*/
function getTargets() external view returns (address[] memory);

/**
* @notice Get a specific allocated target address by index
* @param index The index of the target address to retrieve
* @return The target address at the specified index
*/
function getTargetAt(uint256 index) external view returns (address);

/**
* @notice Get the number of allocated targets
* @return The total number of allocated targets
*/
function getTargetCount() external view returns (uint256);

/**
* @notice Target issuance per block information
* @param target Address of the target
* @return TargetIssuancePerBlock struct containing allocatorIssuanceBlockAppliedTo, selfIssuanceBlockAppliedTo, allocatorIssuancePerBlock, and selfIssuancePerBlock
* @dev This function does not revert when paused, instead the caller is expected to correctly read and apply the information provided.
* @dev Targets should check allocatorIssuanceBlockAppliedTo and selfIssuanceBlockAppliedTo - if either is not the current block, that type of issuance is paused for that target.
* @dev Targets should not check the allocator's pause state directly, but rely on the blockAppliedTo fields to determine if issuance is paused.
*/
function getTargetIssuancePerBlock(address target) external view returns (TargetIssuancePerBlock memory);

/**
* @notice Get the current issuance per block
* @return The current issuance per block
*/
function issuancePerBlock() external view returns (uint256);

/**
* @notice Get the last block number where issuance was distributed
* @return The last block number where issuance was distributed
*/
function lastIssuanceDistributionBlock() external view returns (uint256);

/**
* @notice Get the last block number where issuance was accumulated during pause
* @return The last block number where issuance was accumulated during pause
*/
function lastIssuanceAccumulationBlock() external view returns (uint256);

/**
* @notice Get the amount of pending accumulated allocator issuance
* @return The amount of pending accumulated allocator issuance
*/
function pendingAccumulatedAllocatorIssuance() external view returns (uint256);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity ^0.7.6 || ^0.8.0;

/**
* @title IIssuanceTarget
* @author Edge & Node
* @notice Interface for contracts that receive issuance from an issuance allocator
*/
interface IIssuanceTarget {
/**
* @notice Called by the issuance allocator before the target's issuance allocation changes
* @dev The target should ensure that all issuance related calculations are up-to-date
* with the current block so that an allocation change can be applied correctly.
* Note that the allocation could change multiple times in the same block after
* this function has been called, only the final allocation is relevant.
*/
function beforeIssuanceAllocationChange() external;

/**
* @notice Sets the issuance allocator for this target
* @dev This function facilitates upgrades by providing a standard way for targets
* to change their allocator. Implementations can define their own access control.
* @param issuanceAllocator Address of the issuance allocator
*/
function setIssuanceAllocator(address issuanceAllocator) external;
}
101 changes: 101 additions & 0 deletions packages/issuance/contracts/allocate/DirectAllocation.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: GPL-2.0-or-later

pragma solidity 0.8.27;

import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol";

Check warning on line 5 in packages/issuance/contracts/allocate/DirectAllocation.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/issuance/contracts/allocate/DirectAllocation.sol doesn't exist in: @graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol
import { BaseUpgradeable } from "../common/BaseUpgradeable.sol";

// solhint-disable-next-line no-unused-import
import { ERC165Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; // Used by @inheritdoc

Check warning on line 9 in packages/issuance/contracts/allocate/DirectAllocation.sol

View workflow job for this annotation

GitHub Actions / Lint Files

Import in packages/issuance/contracts/allocate/DirectAllocation.sol doesn't exist in: @openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol

/**
* @title DirectAllocation
* @author Edge & Node
* @notice A simple contract that receives tokens from the IssuanceAllocator and allows
* an authorized operator to withdraw them.
*
* @dev This contract is designed to be an allocator-minting target in the IssuanceAllocator.
* The IssuanceAllocator will mint tokens directly to this contract, and the authorized
* operator can send them to individual addresses as needed.
*
* This contract is pausable by the PAUSE_ROLE. When paused, tokens cannot be sent.
* @custom:security-contact Please email [email protected] if you find any bugs. We might have an active bug bounty program.
*/
contract DirectAllocation is BaseUpgradeable, IIssuanceTarget {

// -- Custom Errors --

/// @notice Thrown when token transfer fails
/// @param to The address that the transfer was attempted to
/// @param amount The amount of tokens that failed to transfer
error SendTokensFailed(address to, uint256 amount);

// -- Events --

/// @notice Emitted when tokens are sent
/// @param to The address that received the tokens
/// @param amount The amount of tokens sent
event TokensSent(address indexed to, uint256 indexed amount);
// Do not need to index amount, ignoring gas-indexed-events warning.

/// @notice Emitted before the issuance allocation changes
event BeforeIssuanceAllocationChange();

// -- Constructor --

/**
* @notice Constructor for the DirectAllocation contract
* @dev This contract is upgradeable, but we use the constructor to pass the Graph Token address
* to the base contract.
* @param graphToken Address of the Graph Token contract
* @custom:oz-upgrades-unsafe-allow constructor
*/
constructor(address graphToken) BaseUpgradeable(graphToken) {}

// -- Initialization --

/**
* @notice Initialize the DirectAllocation contract
* @param governor Address that will have the GOVERNOR_ROLE
*/
function initialize(address governor) external virtual initializer {
__BaseUpgradeable_init(governor);
}

// -- ERC165 --

/**
* @inheritdoc ERC165Upgradeable
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IIssuanceTarget).interfaceId || super.supportsInterface(interfaceId);
}

// -- External Functions --

/**
* @notice Send tokens to a specified address
* @dev This function can only be called by accounts with the OPERATOR_ROLE
* @param to Address to send tokens to
* @param amount Amount of tokens to send
*/
function sendTokens(address to, uint256 amount) external onlyRole(OPERATOR_ROLE) whenNotPaused {
require(GRAPH_TOKEN.transfer(to, amount), SendTokensFailed(to, amount));
emit TokensSent(to, amount);
}

/**
* @dev For DirectAllocation, this is a no-op since we don't need to perform any calculations
* before an allocation change. We simply receive tokens from the IssuanceAllocator.
* @inheritdoc IIssuanceTarget
*/
function beforeIssuanceAllocationChange() external virtual override {
emit BeforeIssuanceAllocationChange();
}

/**
* @dev No-op for DirectAllocation; issuanceAllocator is not stored.
* @inheritdoc IIssuanceTarget
*/
function setIssuanceAllocator(address issuanceAllocator) external virtual override onlyRole(GOVERNOR_ROLE) { }
}
Loading
Loading