From fdd4c352181a82ce67c97c96a794043ed93134df Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:49:02 +0000 Subject: [PATCH 01/43] chore: undo baseline test and package exclusions Tests: - Remove .skip from rewards test suites in contracts-test - Re-enable forge test and lint in subgraph-service package.json Workspace: - Remove issuance, deployment exclusions from pnpm-workspace.yaml - Restore toolshed issuance dependency to workspace:^ - Delete issuance types placeholders (no longer needed) - Update pnpm-lock.yaml Revert "fix(build): add placeholder issuance types for baseline build" This reverts commit 42881331edfe15f9f134e79bb8692d023b92ca5f. --- .../tests/unit/rewards/rewards-config.test.ts | 2 +- .../rewards-eligibility-oracle.test.ts | 2 +- .../unit/rewards/rewards-interface.test.ts | 2 +- .../rewards-issuance-allocator.test.ts | 2 +- .../unit/rewards/rewards-reclaim.test.ts | 2 +- .../tests/unit/rewards/rewards.test.ts | 2 +- packages/horizon/package.json | 4 +- packages/issuance/types/index.d.ts | 6 - packages/issuance/types/index.js | 1 - packages/issuance/types/package.json | 1 - packages/subgraph-service/package.json | 12 +- packages/toolshed/package.json | 2 +- pnpm-lock.yaml | 1328 ++++++++++++++++- pnpm-workspace.yaml | 3 - 14 files changed, 1342 insertions(+), 27 deletions(-) delete mode 100644 packages/issuance/types/index.d.ts delete mode 100644 packages/issuance/types/index.js delete mode 100644 packages/issuance/types/package.json diff --git a/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts index 10b4537c6..b9cbf4dfe 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts @@ -11,7 +11,7 @@ import { NetworkFixture } from '../lib/fixtures' const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block -describe.skip('Rewards - Configuration', () => { +describe('Rewards - Configuration', () => { const graph = hre.graph() let governor: SignerWithAddress let indexer1: SignerWithAddress diff --git a/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts index 22e731ff7..6c9fa37aa 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts @@ -13,7 +13,7 @@ import { NetworkFixture } from '../lib/fixtures' const { HashZero } = constants -describe.skip('Rewards - Eligibility Oracle', () => { +describe('Rewards - Eligibility Oracle', () => { const graph = hre.graph() let curator1: SignerWithAddress let governor: SignerWithAddress diff --git a/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts index b5bf55d22..a085c0b2c 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts @@ -8,7 +8,7 @@ import hre from 'hardhat' import { NetworkFixture } from '../lib/fixtures' -describe.skip('RewardsManager interfaces', () => { +describe('RewardsManager interfaces', () => { const graph = hre.graph() let governor: SignerWithAddress 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 index 6528af6f2..8047b8fd6 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-issuance-allocator.test.ts @@ -9,7 +9,7 @@ import hre from 'hardhat' import { NetworkFixture } from '../lib/fixtures' -describe.skip('Rewards - Issuance Allocator', () => { +describe('Rewards - Issuance Allocator', () => { const graph = hre.graph() let curator1: SignerWithAddress let governor: SignerWithAddress diff --git a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts index 6b42ba84d..5cfe7f2a1 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts @@ -18,7 +18,7 @@ const INDEXER_INELIGIBLE = utils.id('INDEXER_INELIGIBLE') const SUBGRAPH_DENIED = utils.id('SUBGRAPH_DENIED') const CLOSE_ALLOCATION = utils.id('CLOSE_ALLOCATION') -describe.skip('Rewards - Reclaim Addresses', () => { +describe('Rewards - Reclaim Addresses', () => { const graph = hre.graph() let curator1: SignerWithAddress let governor: SignerWithAddress diff --git a/packages/contracts-test/tests/unit/rewards/rewards.test.ts b/packages/contracts-test/tests/unit/rewards/rewards.test.ts index 15f37edd5..97d11ae01 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards.test.ts @@ -1115,7 +1115,7 @@ describe('Rewards', () => { expect(afterTokenSupply).gt(beforeTokenSupply) }) - it.skip('should reclaim denied-period rewards via onSubgraphAllocationUpdate', async function () { + it('should reclaim denied-period rewards via onSubgraphAllocationUpdate', async function () { // Setup reclaim address const reclaimWallet = assetHolder await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) diff --git a/packages/horizon/package.json b/packages/horizon/package.json index 7cb38e98f..f030d63b0 100644 --- a/packages/horizon/package.json +++ b/packages/horizon/package.json @@ -20,10 +20,10 @@ "README.md" ], "scripts": { - "lint": "pnpm lint:ts; pnpm lint:sol; pnpm lint:md; pnpm lint:json", + "lint": "pnpm lint:ts; pnpm lint:sol; pnpm lint:forge; pnpm lint:md; pnpm lint:json", "lint:ts": "eslint --fix --cache '**/*.{js,ts,cjs,mjs,jsx,tsx}'; prettier -w --cache --log-level warn '**/*.{js,ts,cjs,mjs,jsx,tsx}'", "lint:sol": "solhint --fix --noPrompt --noPoster 'contracts/**/*.sol'; prettier -w --cache --log-level warn '**/*.sol'", - "disabled:lint:forge": "forge lint", + "lint:forge": "forge lint", "lint:md": "markdownlint --fix --ignore-path ../../.gitignore '**/*.md'; prettier -w --cache --log-level warn '**/*.md'", "lint:json": "prettier -w --cache --log-level warn '**/*.json'", "clean": "rm -rf build dist cache cache_forge typechain-types", diff --git a/packages/issuance/types/index.d.ts b/packages/issuance/types/index.d.ts deleted file mode 100644 index 36488599d..000000000 --- a/packages/issuance/types/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Placeholder types for baseline build - replaced by typechain output in full build -import type { BaseContract } from 'ethers' - -export interface DirectAllocation extends BaseContract {} -export interface IssuanceAllocator extends BaseContract {} -export interface RewardsEligibilityOracle extends BaseContract {} diff --git a/packages/issuance/types/index.js b/packages/issuance/types/index.js deleted file mode 100644 index 10051c768..000000000 --- a/packages/issuance/types/index.js +++ /dev/null @@ -1 +0,0 @@ -// Placeholder diff --git a/packages/issuance/types/package.json b/packages/issuance/types/package.json deleted file mode 100644 index 729ac4d93..000000000 --- a/packages/issuance/types/package.json +++ /dev/null @@ -1 +0,0 @@ -{"type":"commonjs"} diff --git a/packages/subgraph-service/package.json b/packages/subgraph-service/package.json index 49d303e2c..a00a28e57 100644 --- a/packages/subgraph-service/package.json +++ b/packages/subgraph-service/package.json @@ -18,22 +18,22 @@ "README.md" ], "scripts": { - "lint": "pnpm lint:ts; pnpm lint:sol; pnpm lint:md; pnpm lint:json", + "lint": "pnpm lint:ts; pnpm lint:sol; pnpm lint:forge; pnpm lint:md; pnpm lint:json", "lint:ts": "eslint --fix --cache '**/*.{js,ts,cjs,mjs,jsx,tsx}'; prettier -w --cache --log-level warn '**/*.{js,ts,cjs,mjs,jsx,tsx}'", "lint:sol": "solhint --fix --noPrompt --noPoster 'contracts/**/*.sol'; prettier -w --cache --log-level warn '**/*.sol'", - "disabled:lint:forge": "forge lint", + "lint:forge": "forge lint", "lint:md": "markdownlint --fix --ignore-path ../../.gitignore '**/*.md'; prettier -w --cache --log-level warn '**/*.md'", "lint:json": "prettier -w --cache --log-level warn '**/*.json'", "clean": "rm -rf build dist cache cache_forge typechain-types", "build": "pnpm build:dep && pnpm build:self", "build:dep": "pnpm --filter '@graphprotocol/subgraph-service^...' run build:self", "build:self": "hardhat compile --quiet", - "disabled:test": "pnpm build && pnpm test:self", - "disabled:test:self": "forge test", + "test": "pnpm build && pnpm test:self", + "test:self": "forge test", "test:deployment": "SECURE_ACCOUNTS_DISABLE_PROVIDER=true hardhat test test/deployment/*.ts", "test:integration": "./scripts/integration", - "disabled:test:coverage": "pnpm build && pnpm test:coverage:self", - "disabled:test:coverage:self": "forge coverage", + "test:coverage": "pnpm build && pnpm test:coverage:self", + "test:coverage:self": "forge coverage", "prepublishOnly": "pnpm run build" }, "devDependencies": { diff --git a/packages/toolshed/package.json b/packages/toolshed/package.json index 6e4ebc996..d0ad9a152 100644 --- a/packages/toolshed/package.json +++ b/packages/toolshed/package.json @@ -55,7 +55,7 @@ "dependencies": { "@graphprotocol/address-book": "workspace:^", "@graphprotocol/interfaces": "workspace:^", - "@graphprotocol/issuance": "link:../issuance", + "@graphprotocol/issuance": "workspace:^", "@nomicfoundation/hardhat-ethers": "catalog:", "debug": "^4.4.0", "ethers": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8012855f..9d87a91b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,12 +21,18 @@ catalogs: '@nomicfoundation/hardhat-ethers': specifier: ^3.1.0 version: 3.1.0 + '@nomicfoundation/hardhat-keystore': + specifier: ^3.0.3 + version: 3.0.3 '@typescript-eslint/eslint-plugin': specifier: ^8.53.0 version: 8.53.1 '@typescript-eslint/parser': specifier: ^8.53.0 version: 8.53.1 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 eslint: specifier: ^9.39.2 version: 9.39.2 @@ -99,6 +105,9 @@ catalogs: typescript-eslint: specifier: ^8.53.0 version: 8.53.1 + viem: + specifier: ^2.44.4 + version: 2.44.4 yaml-lint: specifier: ^1.7.0 version: 1.7.0 @@ -736,6 +745,112 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/deployment: + dependencies: + '@graphprotocol/contracts': + specifier: workspace:* + version: link:../contracts + '@graphprotocol/horizon': + specifier: workspace:* + version: link:../horizon + '@graphprotocol/issuance': + specifier: workspace:* + version: link:../issuance + '@graphprotocol/subgraph-service': + specifier: workspace:* + version: link:../subgraph-service + '@graphprotocol/toolshed': + specifier: workspace:* + version: link:../toolshed + '@rocketh/core': + specifier: ^0.17.8 + version: 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + ethers: + specifier: ^6.15.0 + version: 6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + hardhat: + specifier: ^3.1.5 + version: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + viem: + specifier: 'catalog:' + version: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + devDependencies: + '@nomicfoundation/hardhat-ethers': + specifier: ^4.0.0 + version: 4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + '@nomicfoundation/hardhat-keystore': + specifier: 'catalog:' + version: 3.0.3(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-network-helpers': + specifier: ^3.0.0 + version: 3.0.3(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-verify': + specifier: ^3.0.0 + version: 3.0.8(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@openzeppelin/contracts': + specifier: 5.4.0 + version: 5.4.0 + '@rocketh/deploy': + specifier: ^0.17.8 + version: 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/diamond': + specifier: ^0.17.11 + version: 0.17.11(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/doc': + specifier: ^0.17.16 + version: 0.17.16(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/export': + specifier: ^0.17.16 + version: 0.17.16(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/node': + specifier: ^0.17.16 + version: 0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/proxy': + specifier: ^0.17.12 + version: 0.17.12(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/read-execute': + specifier: ^0.17.8 + version: 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/verifier': + specifier: ^0.17.16 + version: 0.17.16(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@types/chai': + specifier: ^4.3.0 + version: 4.3.20 + '@types/mocha': + specifier: ^10.0.0 + version: 10.0.10 + '@types/node': + specifier: ^20.17.50 + version: 20.19.14 + chai: + specifier: ^4.3.0 + version: 4.5.0 + eslint: + specifier: 'catalog:' + version: 9.39.2(jiti@2.5.1) + hardhat-deploy: + specifier: 2.0.0-next.61 + version: 2.0.0-next.61(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + lint-staged: + specifier: 'catalog:' + version: 16.2.7 + mocha: + specifier: ^10.7.0 + version: 10.8.2 + rocketh: + specifier: ^0.17.13 + version: 0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + ts-node: + specifier: ^10.9.0 + version: 10.9.2(@types/node@20.19.14)(typescript@5.9.3) + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: ^5.5.0 + version: 5.9.3 + packages/hardhat-graph-protocol: dependencies: '@graphprotocol/toolshed': @@ -971,6 +1086,231 @@ importers: specifier: ^2.31.7 version: 2.37.6(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + packages/issuance: + dependencies: + '@noble/hashes': + specifier: ^1.8.0 + version: 1.8.0 + devDependencies: + '@graphprotocol/interfaces': + specifier: workspace:^ + version: link:../interfaces + '@nomicfoundation/hardhat-ethers': + specifier: ^4.0.0 + version: 4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + '@nomicfoundation/hardhat-ethers-chai-matchers': + specifier: ^3.0.0 + version: 3.0.2(@nomicfoundation/hardhat-ethers@4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10))(chai@5.3.3)(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-keystore': + specifier: 'catalog:' + version: 3.0.3(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-mocha': + specifier: ^3.0.0 + version: 3.0.9(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(mocha@10.8.2) + '@nomicfoundation/hardhat-network-helpers': + specifier: ^3.0.0 + version: 3.0.3(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-verify': + specifier: ^3.0.0 + version: 3.0.8(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@openzeppelin/contracts': + specifier: ^5.4.0 + version: 5.4.0 + '@openzeppelin/contracts-upgradeable': + specifier: ^5.4.0 + version: 5.4.0(@openzeppelin/contracts@5.4.0) + '@typechain/ethers-v6': + specifier: ^0.5.0 + version: 0.5.1(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(typechain@8.3.2(patch_hash=b34ed6afcf99760666fdc85ecb2094fdd20ce509f947eb09cef21665a2a6a1d6)(typescript@5.9.3))(typescript@5.9.3) + '@types/node': + specifier: ^20.17.50 + version: 20.19.14 + dotenv: + specifier: 'catalog:' + version: 16.6.1 + eslint: + specifier: 'catalog:' + version: 9.39.2(jiti@2.5.1) + ethers: + specifier: 'catalog:' + version: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + glob: + specifier: 'catalog:' + version: 11.0.3 + globals: + specifier: 'catalog:' + version: 16.4.0 + hardhat: + specifier: ^3.1.5 + version: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + lint-staged: + specifier: 'catalog:' + version: 16.2.7 + markdownlint-cli: + specifier: 'catalog:' + version: 0.47.0 + prettier: + specifier: 'catalog:' + version: 3.8.1 + prettier-plugin-solidity: + specifier: 'catalog:' + version: 2.1.0(prettier@3.8.1) + solhint: + specifier: 'catalog:' + version: 6.0.3(typescript@5.9.3) + typechain: + specifier: ^8.3.2 + version: 8.3.2(patch_hash=b34ed6afcf99760666fdc85ecb2094fdd20ce509f947eb09cef21665a2a6a1d6)(typescript@5.9.3) + typescript: + specifier: 'catalog:' + version: 5.9.3 + typescript-eslint: + specifier: 'catalog:' + version: 8.53.1(eslint@9.39.2(jiti@2.5.1))(typescript@5.9.3) + yaml-lint: + specifier: 'catalog:' + version: 1.7.0 + + packages/issuance/testing: + dependencies: + '@graphprotocol/contracts': + specifier: workspace:^ + version: link:../../contracts + '@graphprotocol/interfaces': + specifier: workspace:^ + version: link:../../interfaces + '@graphprotocol/issuance': + specifier: workspace:^ + version: link:.. + devDependencies: + '@nomicfoundation/hardhat-ethers': + specifier: ^4.0.0 + version: 4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + '@nomicfoundation/hardhat-ethers-chai-matchers': + specifier: ^3.0.0 + version: 3.0.2(@nomicfoundation/hardhat-ethers@4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10))(chai@5.3.3)(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-mocha': + specifier: ^3.0.0 + version: 3.0.9(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(mocha@10.8.2) + '@nomicfoundation/hardhat-network-helpers': + specifier: ^3.0.0 + version: 3.0.3(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@openzeppelin/contracts': + specifier: ^5.4.0 + version: 5.4.0 + '@openzeppelin/contracts-upgradeable': + specifier: ^5.4.0 + version: 5.4.0(@openzeppelin/contracts@5.4.0) + '@openzeppelin/foundry-upgrades': + specifier: 0.4.0 + version: 0.4.0(@openzeppelin/defender-deploy-client-cli@0.0.1-alpha.10(encoding@0.1.13))(@openzeppelin/upgrades-core@1.44.1) + '@types/chai': + specifier: ^4.3.20 + version: 4.3.20 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 + '@types/node': + specifier: ^20.17.50 + version: 20.19.14 + chai: + specifier: ^5.1.2 + version: 5.3.3 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + eslint: + specifier: 'catalog:' + version: 9.39.2(jiti@2.5.1) + eslint-plugin-no-only-tests: + specifier: 'catalog:' + version: 3.3.0 + ethers: + specifier: 'catalog:' + version: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + forge-std: + specifier: 'catalog:' + version: https://github.com/foundry-rs/forge-std/tarball/v1.14.0 + glob: + specifier: 'catalog:' + version: 11.0.3 + hardhat: + specifier: ^3.1.5 + version: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + prettier: + specifier: 'catalog:' + version: 3.8.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.14)(typescript@5.9.3) + typescript: + specifier: 'catalog:' + version: 5.9.3 + + packages/issuance/testing-coverage: + dependencies: + '@graphprotocol/interfaces': + specifier: workspace:^ + version: link:../../interfaces + devDependencies: + '@nomicfoundation/hardhat-chai-matchers': + specifier: ^2.0.0 + version: 2.1.0(@nomicfoundation/hardhat-ethers@3.1.0(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)))(chai@4.5.0)(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-ethers': + specifier: ^3.0.0 + version: 3.1.0(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@nomicfoundation/hardhat-network-helpers': + specifier: ^1.0.0 + version: 1.1.0(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@openzeppelin/contracts': + specifier: ^5.4.0 + version: 5.4.0 + '@openzeppelin/contracts-upgradeable': + specifier: ^5.4.0 + version: 5.4.0(@openzeppelin/contracts@5.4.0) + '@types/chai': + specifier: ^4.3.20 + version: 4.3.20 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 + '@types/node': + specifier: ^20.17.50 + version: 20.19.14 + chai: + specifier: ^4.5.0 + version: 4.5.0 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + eslint: + specifier: 'catalog:' + version: 9.39.2(jiti@2.5.1) + ethers: + specifier: ^6.16.0 + version: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + hardhat: + specifier: ^2.28.3 + version: 2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) + mocha: + specifier: ^10.8.2 + version: 10.8.2 + prettier: + specifier: 'catalog:' + version: 3.8.1 + solidity-coverage: + specifier: ^0.8.17 + version: 0.8.17(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.14)(typescript@5.9.3) + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/subgraph-service: devDependencies: '@graphprotocol/contracts': @@ -1257,7 +1597,7 @@ importers: specifier: workspace:^ version: link:../interfaces '@graphprotocol/issuance': - specifier: link:../issuance + specifier: workspace:^ version: link:../issuance '@nomicfoundation/hardhat-ethers': specifier: 'catalog:' @@ -1979,156 +2319,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.9': resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.9': resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.9': resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.9': resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.9': resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.9': resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.9': resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.9': resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.9': resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.9': resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.9': resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.9': resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.9': resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.9': resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.9': resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.9': resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.9': resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.9': resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.9': resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.9': resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.9': resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.9': resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3163,6 +3659,10 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@noble/ciphers@1.2.1': + resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} + engines: {node: ^14.21.3 || >=16} + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -3192,6 +3692,10 @@ packages: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} + '@noble/hashes@1.7.1': + resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.7.2': resolution: {integrity: sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==} engines: {node: ^14.21.3 || >=16} @@ -3219,34 +3723,66 @@ packages: resolution: {integrity: sha512-w0tksbdtSxz9nuzHKsfx4c2mwaD0+l5qKL2R290QdnN9gi9AV62p9DHkOgfBdyg6/a6ZlnQqnISi7C9avk/6VA==} engines: {node: '>= 18'} + '@nomicfoundation/edr-darwin-arm64@0.12.0-next.22': + resolution: {integrity: sha512-TpEBSKyMZJEPvYwBPYclC2b+qobKjn1YhVa7aJ1R7RMPy5dJ/PqsrUK5UuUFFybBqoIorru5NTcsyCMWP5T/Fg==} + engines: {node: '>= 20'} + '@nomicfoundation/edr-darwin-x64@0.11.3': resolution: {integrity: sha512-QR4jAFrPbOcrO7O2z2ESg+eUeIZPe2bPIlQYgiJ04ltbSGW27FblOzdd5+S3RoOD/dsZGKAvvy6dadBEl0NgoA==} engines: {node: '>= 18'} + '@nomicfoundation/edr-darwin-x64@0.12.0-next.22': + resolution: {integrity: sha512-aK/+m8xUkR4u+czTVGU06nSFVH43AY6XCBoR2YjO8SglAAjCSTWK3WAfVb6FcsriMmKv4PrvoyHLMbMP+fXcGA==} + engines: {node: '>= 20'} + '@nomicfoundation/edr-linux-arm64-gnu@0.11.3': resolution: {integrity: sha512-Ktjv89RZZiUmOFPspuSBVJ61mBZQ2+HuLmV67InNlh9TSUec/iDjGIwAn59dx0bF/LOSrM7qg5od3KKac4LJDQ==} engines: {node: '>= 18'} + '@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.22': + resolution: {integrity: sha512-W5vXMleG14hVzRYGPEwlHLJ6iiQE8Qh63Uj538nAz4YUI6wWSgUOZE7K2Gt1EdujZGnrt7kfDslgJ96n4nKQZw==} + engines: {node: '>= 20'} + '@nomicfoundation/edr-linux-arm64-musl@0.11.3': resolution: {integrity: sha512-B3sLJx1rL2E9pfdD4mApiwOZSrX0a/KQSBWdlq1uAhFKqkl00yZaY4LejgZndsJAa4iKGQJlGnw4HCGeVt0+jA==} engines: {node: '>= 18'} + '@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.22': + resolution: {integrity: sha512-VDp7EB3iY8MH/fFVcgEzLDGYmtS6j2honNc0RNUCFECKPrdsngGrTG8p+YFxyVjq2m5GEsdyKo4e+BKhaUNPdg==} + engines: {node: '>= 20'} + '@nomicfoundation/edr-linux-x64-gnu@0.11.3': resolution: {integrity: sha512-D/4cFKDXH6UYyKPu6J3Y8TzW11UzeQI0+wS9QcJzjlrrfKj0ENW7g9VihD1O2FvXkdkTjcCZYb6ai8MMTCsaVw==} engines: {node: '>= 18'} + '@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.22': + resolution: {integrity: sha512-XL6oA3ymRSQYyvg6hF1KIax6V/9vlWr5gJ8GPHVVODk1a/YfuEEY1osN5Zmo6aztUkSGKwSuac/3Ax7rfDDiSg==} + engines: {node: '>= 20'} + '@nomicfoundation/edr-linux-x64-musl@0.11.3': resolution: {integrity: sha512-ergXuIb4nIvmf+TqyiDX5tsE49311DrBky6+jNLgsGDTBaN1GS3OFwFS8I6Ri/GGn6xOaT8sKu3q7/m+WdlFzg==} engines: {node: '>= 18'} + '@nomicfoundation/edr-linux-x64-musl@0.12.0-next.22': + resolution: {integrity: sha512-hmkRIXxWa9P0PwfXOAO6WUw11GyV5gpxcMunqWBTkwZ4QW/hi/CkXmlLo6VHd6ceCwpUNLhCGndBtrOPrNRi4A==} + engines: {node: '>= 20'} + '@nomicfoundation/edr-win32-x64-msvc@0.11.3': resolution: {integrity: sha512-snvEf+WB3OV0wj2A7kQ+ZQqBquMcrozSLXcdnMdEl7Tmn+KDCbmFKBt3Tk0X3qOU4RKQpLPnTxdM07TJNVtung==} engines: {node: '>= 18'} + '@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.22': + resolution: {integrity: sha512-X7f+7KUMm00trsXAHCHJa+x1fc3QAbk2sBctyOgpET+GLrfCXbxqrccKi7op8f0zTweAVGg1Hsc8SjjC7kwFLw==} + engines: {node: '>= 20'} + '@nomicfoundation/edr@0.11.3': resolution: {integrity: sha512-kqILRkAd455Sd6v8mfP3C1/0tCOynJWY+Ir+k/9Boocu2kObCrsFgG+ZWB7fSBVdd9cPVSNrnhWS+V+PEo637g==} engines: {node: '>= 18'} + '@nomicfoundation/edr@0.12.0-next.22': + resolution: {integrity: sha512-JigYWf2stjpDxSndBsxRoobQHK8kz4SAVaHtTIKQLIHbsBwymE8i120Ejne6Jk+Ndc5CsNINXB8/bK6vLPe9jA==} + engines: {node: '>= 20'} + '@nomicfoundation/ethereumjs-rlp@5.0.4': resolution: {integrity: sha512-8H1S3s8F6QueOc/X92SdrA4RDenpiAEqMg5vJH99kcQaCy/a3Q6fgseo75mgWlbanGJXSlAPtnCeG9jvfTYXlw==} engines: {node: '>=18'} @@ -3272,12 +3808,25 @@ packages: '@nomicfoundation/hardhat-errors@3.0.6': resolution: {integrity: sha512-3x+OVdZv7Rgy3z6os9pB6kiHLxs6q0PCXHRu+WLZflr44PG9zW+7V9o+ehrUqmmivlHcIFr3Qh4M2wZVuoCYww==} + '@nomicfoundation/hardhat-ethers-chai-matchers@3.0.2': + resolution: {integrity: sha512-nkg+z+fq5PXcRxS/zadyosAA+oPp3sdWrKpuOcASDf0RjqsN2LsNymML0VNNkZF8TF+hYa36fbV+QOas2Fm2BQ==} + peerDependencies: + '@nomicfoundation/hardhat-ethers': ^4.0.0 + chai: ^5.1.2 + ethers: ^6.14.0 + hardhat: ^3.0.0 + '@nomicfoundation/hardhat-ethers@3.1.0': resolution: {integrity: sha512-jx6fw3Ms7QBwFGT2MU6ICG292z0P81u6g54JjSV105+FbTZOF4FJqPksLfDybxkkOeq28eDxbqq7vpxRYyIlxA==} peerDependencies: ethers: ^6.14.0 hardhat: ^2.26.0 + '@nomicfoundation/hardhat-ethers@4.0.4': + resolution: {integrity: sha512-UTw3iM7AMZ1kZlzgJbtAEfWWDYjcnT0EZkRUZd1wIVtMOXIE4nc6Ya4veodAt/KpBhG+6W06g50W+Z/0wTm62g==} + peerDependencies: + hardhat: ^3.0.7 + '@nomicfoundation/hardhat-foundry@1.2.0': resolution: {integrity: sha512-2AJQLcWnUk/iQqHDVnyOadASKFQKF1PhNtt1cONEQqzUPK+fqME1IbP+EKu+RkZTRcyc4xqUMaB0sutglKRITg==} peerDependencies: @@ -3298,6 +3847,17 @@ packages: '@nomicfoundation/hardhat-verify': ^2.1.0 hardhat: ^2.26.0 + '@nomicfoundation/hardhat-keystore@3.0.3': + resolution: {integrity: sha512-rkwfdy/GsX/2SV49RGBvMsCuR+SYGJQGD3wcrS5m2Cyap5eQFEgKZbqpua6YQRA2raxRmVVH6antIIftgBFXAQ==} + peerDependencies: + hardhat: ^3.0.0 + + '@nomicfoundation/hardhat-mocha@3.0.9': + resolution: {integrity: sha512-9hsl1TcRMudN/gUPsRjx0iGLEkl8IU9BBQ5wT5bf8N4RTSHbVwqVL+mADzpt+Dmd5nkdItynhrAJnXjwTvy5DQ==} + peerDependencies: + hardhat: ^3.0.12 + mocha: ^11.0.0 + '@nomicfoundation/hardhat-network-helpers@1.1.0': resolution: {integrity: sha512-ZS+NulZuR99NUHt2VwcgZvgeD6Y63qrbORNRuKO+lTowJxNVsrJ0zbRx1j5De6G3dOno5pVGvuYSq2QVG0qCYg==} peerDependencies: @@ -3332,11 +3892,24 @@ packages: '@nomicfoundation/hardhat-utils@3.0.6': resolution: {integrity: sha512-AD/LPNdjXNFRrZcaAAewgJpdnHpPppZxo5p+x6wGMm5Hz4B3+oLf/LUzVn8qb4DDy9RE2c24l2F8vmL/w6ZuXg==} + '@nomicfoundation/hardhat-vendored@3.0.0': + resolution: {integrity: sha512-bzIOdG4iAuYSs9JSnaVOtH7qUKJ6W5+OtOiL8MlyFuLKYN2hjIisGO4pY5zR4N7xi/3RjfcnjVNz8tU0DPg2Cw==} + '@nomicfoundation/hardhat-verify@2.1.1': resolution: {integrity: sha512-K1plXIS42xSHDJZRkrE2TZikqxp9T4y6jUMUNI/imLgN5uCcEQokmfU0DlyP9zzHncYK92HlT5IWP35UVCLrPw==} peerDependencies: hardhat: ^2.26.0 + '@nomicfoundation/hardhat-verify@3.0.8': + resolution: {integrity: sha512-AkwFvx/r0AFDk0H53mReYpkw2pvi5Jq34zAyk2+cTM7o/OnOvq0xcAaidw4BQvBf9+FMeFAKjJe+zNYgrsLatg==} + peerDependencies: + hardhat: ^3.0.0 + + '@nomicfoundation/hardhat-zod-utils@3.0.1': + resolution: {integrity: sha512-I6/pyYiS9p2lLkzQuedr1ScMocH+ew8l233xTi+LP92gjEiviJDxselpkzgU01MUM0t6BPpfP8yMO958LDEJVg==} + peerDependencies: + zod: ^3.23.8 + '@nomicfoundation/ignition-core@0.15.13': resolution: {integrity: sha512-Z4T1WIbw0EqdsN9RxtnHeQXBi7P/piAmCu8bZmReIdDo/2h06qgKWxjDoNfc9VBFZJ0+Dx79tkgQR3ewxMDcpA==} @@ -3584,6 +4157,46 @@ packages: '@resolver-engine/imports@0.3.3': resolution: {integrity: sha512-anHpS4wN4sRMwsAbMXhMfOD/y4a4Oo0Cw/5+rue7hSwGWsDOQaAU1ClK1OxjUC35/peazxEl8JaSRRS+Xb8t3Q==} + '@rocketh/core@0.17.8': + resolution: {integrity: sha512-xzAX1pZ3g8WQlx3GJezowU9rm4h7TucwWmeU8Jf+jpfaHV5EKDAt5rJMWPDjSXkUWIxiMZbsPDx78S75Q6Cixw==} + + '@rocketh/deploy@0.17.8': + resolution: {integrity: sha512-XuHTI4NKCCyYCJO/UwT0+THTtB7UDU5KMxp+rHHI0PrL9g2WSSo7Hgz1yzYgcIpoEwBx2B3ljXsp9JBEPFtujg==} + + '@rocketh/diamond@0.17.11': + resolution: {integrity: sha512-TC5ohM1BWoHYo8joJu82O5jq+GiRYyPVq57O1bHCYZyu0+hKVfGgsBBgXI1ZuIOPIlOPtRZ0LaL1QFstuiCzAg==} + + '@rocketh/doc@0.17.16': + resolution: {integrity: sha512-qqFZYceKw8XhCfrXTFU7Twq47YQf2plrn8XOMRsVezFLjzUDBCe0Xr0VHTTWZ0L1GHbBPdkw2z7wAyf8u3F2Sw==} + hasBin: true + peerDependencies: + '@rocketh/node': 0.17.16 + + '@rocketh/export@0.17.16': + resolution: {integrity: sha512-T+zGj14Uel+/J5NUl9DjBOpUE0kdeD8KU2uswmwiC3sDZnjcn3xPCbP+yQWDnMxfKgSIC3cUi+jW+S+NO2k9Lg==} + hasBin: true + peerDependencies: + '@rocketh/node': 0.17.16 + rocketh: 0.17.13 + + '@rocketh/node@0.17.16': + resolution: {integrity: sha512-ZgvQ9zfpbB4aItxeLdJsHIviRtdFKxbH9sPGDNHMEXiVILFhqKX3YutqeglkqdKoyI7oFpDlDWdAlVD6uuqJUQ==} + hasBin: true + peerDependencies: + rocketh: 0.17.13 + + '@rocketh/proxy@0.17.12': + resolution: {integrity: sha512-TVNofIkc1hT1Udg72DleWGQ2LvH7PlhFenbzRLLk7w4wGH96keJs1HDeWP/bj8+sdqWasPWrFFu8VZbPfUxqUQ==} + + '@rocketh/read-execute@0.17.8': + resolution: {integrity: sha512-4dk48km24PEeC54nFt3BxR0DniBxV8Y4rjNV89vl/ChKkWmMl42FHUvgQHEf079nsk62GCSW5a0DAVD7xBTdzA==} + + '@rocketh/verifier@0.17.16': + resolution: {integrity: sha512-NkjM7GNJDzBL+4hIdgUJMHsSdEunaZ7YgeQQV9Ef3swn0fKJv16WRiSFcsCgXalagenR62Oe1XKTsjnLAQ8ihA==} + hasBin: true + peerDependencies: + '@rocketh/node': 0.17.16 + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -3615,6 +4228,10 @@ packages: resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} engines: {node: '>=6'} + '@sentry/core@9.47.1': + resolution: {integrity: sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==} + engines: {node: '>=18'} + '@sentry/hub@5.30.0': resolution: {integrity: sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==} engines: {node: '>=6'} @@ -3978,6 +4595,9 @@ packages: '@types/chai-as-promised@7.1.8': resolution: {integrity: sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==} + '@types/chai-as-promised@8.0.2': + resolution: {integrity: sha512-meQ1wDr1K5KRCSvG2lX7n7/5wf70BeptTKst0axGvnN6zqaVpRqegoIbugiAPSqOW9K9aL8gDVrm7a2LXOtn2Q==} + '@types/chai@4.3.20': resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} @@ -3996,6 +4616,9 @@ packages: '@types/form-data@0.0.33': resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==} + '@types/fs-extra@11.0.4': + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} @@ -4027,6 +4650,9 @@ packages: resolution: {integrity: sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==} deprecated: This is a stub types definition. json5 provides its own type definitions, so you do not need this installed. + '@types/jsonfile@6.1.4': + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} @@ -4070,6 +4696,9 @@ packages: '@types/prettier@2.7.3': resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + '@types/prompts@2.4.9': + resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -5180,6 +5809,10 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + cbor2@1.12.0: + resolution: {integrity: sha512-3Cco8XQhi27DogSp9Ri6LYNZLi/TBY/JVnDe+mj06NkBjW/ZYOtekaEU4wZ4xcRMNrFkDv8KNtOAqHyDfz3lYg==} + engines: {node: '>=18.7'} + cbor@10.0.11: resolution: {integrity: sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA==} engines: {node: '>=20'} @@ -5197,6 +5830,11 @@ packages: peerDependencies: chai: '>= 2.1.2 < 6' + chai-as-promised@8.0.2: + resolution: {integrity: sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==} + peerDependencies: + chai: '>= 2.1.2 < 7' + chai@4.5.0: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} @@ -5974,6 +6612,12 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + eip-1193-jsonrpc-provider@0.4.3: + resolution: {integrity: sha512-xcrz22ArOqvbXt4LHOeV5JooL8jTt/sv8WIH7MLQTn8z7fQwRDDzUECgIwZaX1Irpn/HIZGiu6YZwIoRVfPEow==} + + eip-1193@0.6.5: + resolution: {integrity: sha512-KXCSdjFLIT5/06rMD2pMqoAZhZcTg4EofiCI70ovIOy8L/6twGJFE+RtW89S/hMFKDoNEGJ/WK8jQv7CpuGDgg==} + electron-to-chromium@1.5.218: resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==} @@ -6105,6 +6749,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -6410,6 +7059,10 @@ packages: ethers@5.8.0: resolution: {integrity: sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==} + ethers@6.15.0: + resolution: {integrity: sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==} + engines: {node: '>=14.0.0'} + ethers@6.16.0: resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==} engines: {node: '>=14.0.0'} @@ -6784,6 +7437,10 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} + fs-extra@11.3.3: + resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} + engines: {node: '>=14.14'} + fs-extra@4.0.3: resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} @@ -6918,6 +7575,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -7127,6 +7787,12 @@ packages: '@ethersproject/hardware-wallets': ^5.0.14 hardhat: ^2.0.0 + hardhat-deploy@2.0.0-next.61: + resolution: {integrity: sha512-q7KCpyyn+SSPd7064cHJVJqxLx9F3g/Ug/U1jnKBIipcRe3LxOJyggZ9zl1d5GLqX4V1zbVxwnzw4iI8w4kFyw==} + peerDependencies: + '@rocketh/node': ^0.17.15 + hardhat: ^3.1.3 + hardhat-gas-reporter@1.0.10: resolution: {integrity: sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA==} peerDependencies: @@ -7166,6 +7832,22 @@ packages: typescript: optional: true + hardhat@2.28.3: + resolution: {integrity: sha512-f1WxpCJCXzxDc12MgIIxxkvB2QK40g/atsW4Az5WQFhUXpZx4VFoSfvwYBIRsRbq6xIrgxef+tXuWda5wTLlgA==} + hasBin: true + peerDependencies: + ts-node: '*' + typescript: '*' + peerDependenciesMeta: + ts-node: + optional: true + typescript: + optional: true + + hardhat@3.1.5: + resolution: {integrity: sha512-0Z0KI/m6wJYCMZgDK3QuVqR59lSa3aMu6QHKqnbIYXKu/phQ+YFKJZAY4zkUKX21ZjcrrRg25qLUzZw1bO6g/A==} + hasBin: true + has-ansi@2.0.0: resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} engines: {node: '>=0.10.0'} @@ -8104,6 +8786,10 @@ packages: resolution: {integrity: sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==} engines: {node: '>=0.10.0'} + ldenv@0.3.16: + resolution: {integrity: sha512-ShaNPPzgUi+iGj9bsQ0TPRm6MuOcPpc1NklL0/IzJsvB0OdHwWoPhmeTVR5z0oC3zzLebrojozo/nt8d2XTZbQ==} + hasBin: true + level-codec@7.0.1: resolution: {integrity: sha512-Ua/R9B9r3RasXdRmOtd+t9TCOEIIlts+TN/7XTT2unhDaL6sJn83S3rUyljbr6lVtw49N3/yA0HHjpV6Kzb2aQ==} deprecated: Superseded by level-transcoder (https://github.com/Level/community#faq) @@ -8926,6 +9612,15 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + named-logs-console@0.5.1: + resolution: {integrity: sha512-GL2mfmVO7vcOTIl9QRczOBq6aaXsVfehXTU150cIFscRbVMbNYivhDzRgLadPC5pK+URCHmnEn5jWo+LZ7GkHQ==} + + named-logs@0.3.2: + resolution: {integrity: sha512-rpgShWrH6NakMKUDK32Pn/FZyPl7QoQRleMekHKkbrExXDymb2wNm3/BUbdTG5f3v7Qa17imVkSWHOfNFhDIPw==} + + named-logs@0.4.1: + resolution: {integrity: sha512-CHLNCYsSBTC+xVbdA2nTWWfW+c3hyQKCOfl7MzgKtO6/VoP4nXQ1o1Ji2ExY/P0v7QljOGH338fOF6rYJCoK0Q==} + nan@2.23.0: resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} @@ -8969,6 +9664,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + neoqs@6.13.0: + resolution: {integrity: sha512-IysBpjrEG9qiUb/IT6XrXSz2ASzBxLebp4s8/GBm7STYC315vMNqH0aWdRR+f7KvXK4aRlLcf5r2Z6dOTxQSrQ==} + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -9338,6 +10036,10 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + p-map@7.0.4: + resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + engines: {node: '>=18'} + p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -9735,6 +10437,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + promise-throttle@1.1.2: + resolution: {integrity: sha512-dij7vjyXNewuuN/gyr+TX2KRjw48mbV5FEtgyXaIoJjGYAKT0au23/voNvy9eS4UNJjx2KUdEcO5Yyfc1h7vWQ==} + promise-to-callback@1.0.0: resolution: {integrity: sha512-uhMIZmKM5ZteDMfLgJnoSq9GCwsNKrYau73Awf1jIy6/eUcuuZ3P+CD9zUv0kJsIUbU+x6uLNIhXhLHDs1pNPA==} engines: {node: '>=0.10.0'} @@ -10104,10 +10809,17 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-url@0.2.1: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} deprecated: https://github.com/lydell/resolve-url#deprecated + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + resolve@1.1.7: resolution: {integrity: sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==} @@ -10190,6 +10902,9 @@ packages: resolution: {integrity: sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==} hasBin: true + rocketh@0.17.13: + resolution: {integrity: sha512-W7pdiDh6a46DcG2e6CW0/VZgl8P7HEVpeoJohLuu/fAxKCIvZFHoTQgFIswY0iBIpZpzZhX0q3iDhc6ErKwVIw==} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -10525,6 +11240,10 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -11215,6 +11934,11 @@ packages: tsort@0.0.1: resolution: {integrity: sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -13164,81 +13888,159 @@ snapshots: '@esbuild/aix-ppc64@0.25.9': optional: true + '@esbuild/aix-ppc64@0.27.2': + optional: true + '@esbuild/android-arm64@0.25.9': optional: true + '@esbuild/android-arm64@0.27.2': + optional: true + '@esbuild/android-arm@0.25.9': optional: true + '@esbuild/android-arm@0.27.2': + optional: true + '@esbuild/android-x64@0.25.9': optional: true + '@esbuild/android-x64@0.27.2': + optional: true + '@esbuild/darwin-arm64@0.25.9': optional: true + '@esbuild/darwin-arm64@0.27.2': + optional: true + '@esbuild/darwin-x64@0.25.9': optional: true + '@esbuild/darwin-x64@0.27.2': + optional: true + '@esbuild/freebsd-arm64@0.25.9': optional: true + '@esbuild/freebsd-arm64@0.27.2': + optional: true + '@esbuild/freebsd-x64@0.25.9': optional: true + '@esbuild/freebsd-x64@0.27.2': + optional: true + '@esbuild/linux-arm64@0.25.9': optional: true + '@esbuild/linux-arm64@0.27.2': + optional: true + '@esbuild/linux-arm@0.25.9': optional: true + '@esbuild/linux-arm@0.27.2': + optional: true + '@esbuild/linux-ia32@0.25.9': optional: true + '@esbuild/linux-ia32@0.27.2': + optional: true + '@esbuild/linux-loong64@0.25.9': optional: true + '@esbuild/linux-loong64@0.27.2': + optional: true + '@esbuild/linux-mips64el@0.25.9': optional: true + '@esbuild/linux-mips64el@0.27.2': + optional: true + '@esbuild/linux-ppc64@0.25.9': optional: true + '@esbuild/linux-ppc64@0.27.2': + optional: true + '@esbuild/linux-riscv64@0.25.9': optional: true + '@esbuild/linux-riscv64@0.27.2': + optional: true + '@esbuild/linux-s390x@0.25.9': optional: true + '@esbuild/linux-s390x@0.27.2': + optional: true + '@esbuild/linux-x64@0.25.9': optional: true + '@esbuild/linux-x64@0.27.2': + optional: true + '@esbuild/netbsd-arm64@0.25.9': optional: true + '@esbuild/netbsd-arm64@0.27.2': + optional: true + '@esbuild/netbsd-x64@0.25.9': optional: true + '@esbuild/netbsd-x64@0.27.2': + optional: true + '@esbuild/openbsd-arm64@0.25.9': optional: true + '@esbuild/openbsd-arm64@0.27.2': + optional: true + '@esbuild/openbsd-x64@0.25.9': optional: true + '@esbuild/openbsd-x64@0.27.2': + optional: true + '@esbuild/openharmony-arm64@0.25.9': optional: true + '@esbuild/openharmony-arm64@0.27.2': + optional: true + '@esbuild/sunos-x64@0.25.9': optional: true + '@esbuild/sunos-x64@0.27.2': + optional: true + '@esbuild/win32-arm64@0.25.9': optional: true + '@esbuild/win32-arm64@0.27.2': + optional: true + '@esbuild/win32-ia32@0.25.9': optional: true + '@esbuild/win32-ia32@0.27.2': + optional: true + '@esbuild/win32-x64@0.25.9': optional: true + '@esbuild/win32-x64@0.27.2': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.5.1))': dependencies: eslint: 9.39.2(jiti@2.5.1) @@ -15524,6 +16326,8 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@noble/ciphers@1.2.1': {} + '@noble/ciphers@1.3.0': {} '@noble/curves@1.2.0': @@ -15548,6 +16352,8 @@ snapshots: '@noble/hashes@1.4.0': {} + '@noble/hashes@1.7.1': {} + '@noble/hashes@1.7.2': {} '@noble/hashes@1.8.0': {} @@ -15568,18 +16374,32 @@ snapshots: '@nomicfoundation/edr-darwin-arm64@0.11.3': {} + '@nomicfoundation/edr-darwin-arm64@0.12.0-next.22': {} + '@nomicfoundation/edr-darwin-x64@0.11.3': {} + '@nomicfoundation/edr-darwin-x64@0.12.0-next.22': {} + '@nomicfoundation/edr-linux-arm64-gnu@0.11.3': {} + '@nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.22': {} + '@nomicfoundation/edr-linux-arm64-musl@0.11.3': {} + '@nomicfoundation/edr-linux-arm64-musl@0.12.0-next.22': {} + '@nomicfoundation/edr-linux-x64-gnu@0.11.3': {} + '@nomicfoundation/edr-linux-x64-gnu@0.12.0-next.22': {} + '@nomicfoundation/edr-linux-x64-musl@0.11.3': {} + '@nomicfoundation/edr-linux-x64-musl@0.12.0-next.22': {} + '@nomicfoundation/edr-win32-x64-msvc@0.11.3': {} + '@nomicfoundation/edr-win32-x64-msvc@0.12.0-next.22': {} + '@nomicfoundation/edr@0.11.3': dependencies: '@nomicfoundation/edr-darwin-arm64': 0.11.3 @@ -15590,6 +16410,16 @@ snapshots: '@nomicfoundation/edr-linux-x64-musl': 0.11.3 '@nomicfoundation/edr-win32-x64-msvc': 0.11.3 + '@nomicfoundation/edr@0.12.0-next.22': + dependencies: + '@nomicfoundation/edr-darwin-arm64': 0.12.0-next.22 + '@nomicfoundation/edr-darwin-x64': 0.12.0-next.22 + '@nomicfoundation/edr-linux-arm64-gnu': 0.12.0-next.22 + '@nomicfoundation/edr-linux-arm64-musl': 0.12.0-next.22 + '@nomicfoundation/edr-linux-x64-gnu': 0.12.0-next.22 + '@nomicfoundation/edr-linux-x64-musl': 0.12.0-next.22 + '@nomicfoundation/edr-win32-x64-msvc': 0.12.0-next.22 + '@nomicfoundation/ethereumjs-rlp@5.0.4': {} '@nomicfoundation/ethereumjs-util@9.0.4': @@ -15619,12 +16449,37 @@ snapshots: hardhat: 2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) ordinal: 1.0.3 + '@nomicfoundation/hardhat-chai-matchers@2.1.0(@nomicfoundation/hardhat-ethers@3.1.0(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)))(chai@4.5.0)(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + '@nomicfoundation/hardhat-ethers': 3.1.0(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@types/chai-as-promised': 7.1.8 + chai: 4.5.0 + chai-as-promised: 7.1.2(chai@4.5.0) + deep-eql: 4.1.4 + ethers: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + hardhat: 2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) + ordinal: 1.0.3 + '@nomicfoundation/hardhat-errors@3.0.6': dependencies: '@nomicfoundation/hardhat-utils': 3.0.6 transitivePeerDependencies: - supports-color + '@nomicfoundation/hardhat-ethers-chai-matchers@3.0.2(@nomicfoundation/hardhat-ethers@4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10))(chai@5.3.3)(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-ethers': 4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) + '@nomicfoundation/hardhat-utils': 3.0.6 + '@types/chai-as-promised': 8.0.2 + chai: 5.3.3 + chai-as-promised: 8.0.2(chai@5.3.3) + deep-eql: 5.0.2 + ethers: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + hardhat: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + '@nomicfoundation/hardhat-ethers@3.1.0(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: debug: 4.4.3(supports-color@9.4.0) @@ -15643,6 +16498,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@nomicfoundation/hardhat-ethers@3.1.0(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + debug: 4.4.3(supports-color@9.4.0) + ethers: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + hardhat: 2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) + lodash.isequal: 4.5.0 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-ethers@4.0.4(bufferutil@4.0.9)(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + debug: 4.4.3(supports-color@9.4.0) + ethereum-cryptography: 2.2.1 + ethers: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + hardhat: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@nomicfoundation/hardhat-foundry@1.2.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: hardhat: 2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -15672,11 +16549,43 @@ snapshots: - supports-color - utf-8-validate + '@nomicfoundation/hardhat-keystore@3.0.3(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/hashes': 1.7.1 + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + chalk: 5.6.2 + debug: 4.4.3(supports-color@9.4.0) + hardhat: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-mocha@3.0.9(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))(mocha@10.8.2)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + chalk: 5.6.2 + hardhat: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + mocha: 10.8.2 + tsx: 4.21.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + '@nomicfoundation/hardhat-network-helpers@1.1.0(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: ethereumjs-util: 7.1.5 hardhat: 2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) + '@nomicfoundation/hardhat-network-helpers@1.1.0(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + ethereumjs-util: 7.1.5 + hardhat: 2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) + '@nomicfoundation/hardhat-network-helpers@3.0.3(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: '@nomicfoundation/hardhat-errors': 3.0.6 @@ -15685,6 +16594,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@nomicfoundation/hardhat-network-helpers@3.0.3(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + hardhat: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + '@nomicfoundation/hardhat-toolbox@4.0.0(841324e874603666491d4961f5a3314c)': dependencies: '@nomicfoundation/hardhat-chai-matchers': 2.1.0(@nomicfoundation/hardhat-ethers@3.1.0(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)))(chai@5.3.3)(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)) @@ -15738,6 +16655,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@nomicfoundation/hardhat-vendored@3.0.0': {} + '@nomicfoundation/hardhat-verify@2.1.1(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))': dependencies: '@ethersproject/abi': 5.8.0 @@ -15768,6 +16687,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@nomicfoundation/hardhat-verify@3.0.8(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@ethersproject/abi': 5.8.0 + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + cbor2: 1.12.0 + chalk: 5.6.2 + debug: 4.4.3(supports-color@9.4.0) + hardhat: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + semver: 7.7.2 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-zod-utils@3.0.1(zod@3.25.76)': + dependencies: + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + '@nomicfoundation/ignition-core@0.15.13(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@ethersproject/address': 5.6.1 @@ -16149,6 +17091,144 @@ snapshots: transitivePeerDependencies: - supports-color + '@rocketh/core@0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + eip-1193: 0.6.5 + named-logs: 0.4.1 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/deploy@0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + eip-1193: 0.6.5 + named-logs: 0.4.1 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/diamond@0.17.11(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/deploy': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/read-execute': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + eip-1193: 0.6.5 + named-logs: 0.4.1 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/doc@0.17.16(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/node': 0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@types/fs-extra': 11.0.4 + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + commander: 14.0.2 + fs-extra: 11.3.3 + handlebars: 4.7.8 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/export@0.17.16(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/node': 0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@types/fs-extra': 11.0.4 + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + chalk: 5.6.2 + commander: 14.0.2 + eip-1193: 0.6.5 + fs-extra: 11.3.3 + rocketh: 0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@types/prompts': 2.4.9 + change-case: 5.4.4 + commander: 14.0.2 + eip-1193: 0.6.5 + ldenv: 0.3.16 + named-logs: 0.4.1 + named-logs-console: 0.5.1 + prompts: 2.4.2 + rocketh: 0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + tsx: 4.21.0 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/proxy@0.17.12(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/deploy': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/read-execute': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + eip-1193: 0.6.5 + named-logs: 0.4.1 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/read-execute@0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + eip-1193: 0.6.5 + named-logs: 0.4.1 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + + '@rocketh/verifier@0.17.16(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@rocketh/node': 0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@types/fs-extra': 11.0.4 + '@types/qs': 6.14.0 + chalk: 5.6.2 + commander: 14.0.2 + fs-extra: 11.3.3 + ldenv: 0.3.16 + neoqs: 6.13.0 + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + '@rtsao/scc@1.1.0': {} '@scure/base@1.1.9': {} @@ -16196,6 +17276,8 @@ snapshots: '@sentry/utils': 5.30.0 tslib: 1.14.1 + '@sentry/core@9.47.1': {} + '@sentry/hub@5.30.0': dependencies: '@sentry/types': 5.30.0 @@ -16756,6 +17838,10 @@ snapshots: dependencies: '@types/chai': 4.3.20 + '@types/chai-as-promised@8.0.2': + dependencies: + '@types/chai': 4.3.20 + '@types/chai@4.3.20': {} '@types/concat-stream@1.6.1': @@ -16776,6 +17862,11 @@ snapshots: dependencies: '@types/node': 20.19.14 + '@types/fs-extra@11.0.4': + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 20.19.14 + '@types/glob@7.2.0': dependencies: '@types/minimatch': 6.0.0 @@ -16810,6 +17901,10 @@ snapshots: dependencies: json5: 2.2.3 + '@types/jsonfile@6.1.4': + dependencies: + '@types/node': 20.19.14 + '@types/katex@0.16.7': {} '@types/keyv@3.1.4': @@ -16856,6 +17951,11 @@ snapshots: '@types/prettier@2.7.3': {} + '@types/prompts@2.4.9': + dependencies: + '@types/node': 20.19.14 + kleur: 3.0.3 + '@types/qs@6.14.0': {} '@types/resolve@0.0.8': @@ -18501,6 +19601,8 @@ snapshots: caseless@0.12.0: {} + cbor2@1.12.0: {} + cbor@10.0.11: dependencies: nofilter: 3.1.0 @@ -18523,6 +19625,11 @@ snapshots: chai: 5.3.3 check-error: 1.0.3 + chai-as-promised@8.0.2(chai@5.3.3): + dependencies: + chai: 5.3.3 + check-error: 2.1.3 + chai@4.5.0: dependencies: assertion-error: 1.1.0 @@ -19403,6 +20510,13 @@ snapshots: ee-first@1.1.1: {} + eip-1193-jsonrpc-provider@0.4.3: + dependencies: + named-logs: 0.3.2 + promise-throttle: 1.1.2 + + eip-1193@0.6.5: {} + electron-to-chromium@1.5.218: {} elliptic@6.5.4: @@ -19622,6 +20736,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -20286,6 +21429,19 @@ snapshots: - bufferutil - utf-8-validate + ethers@6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 20.19.14 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@adraffy/ens-normalize': 1.10.1 @@ -20863,6 +22019,12 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@11.3.3: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@4.0.3: dependencies: graceful-fs: 4.2.11 @@ -21036,6 +22198,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + get-value@2.0.6: {} getpass@0.1.7: @@ -21393,6 +22559,19 @@ snapshots: - supports-color - utf-8-validate + hardhat-deploy@2.0.0-next.61(@rocketh/node@0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + '@rocketh/node': 0.17.16(bufferutil@4.0.9)(rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@types/debug': 4.1.12 + debug: 4.4.3(supports-color@9.4.0) + hardhat: 3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) + named-logs-console: 0.5.1 + slash: 5.1.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + hardhat-gas-reporter@1.0.10(bufferutil@4.0.9)(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10): dependencies: array-uniq: 1.0.3 @@ -21550,6 +22729,82 @@ snapshots: - supports-color - utf-8-validate + hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10): + dependencies: + '@ethereumjs/util': 9.1.0 + '@ethersproject/abi': 5.8.0 + '@nomicfoundation/edr': 0.12.0-next.22 + '@nomicfoundation/solidity-analyzer': 0.1.2 + '@sentry/node': 5.30.0 + adm-zip: 0.4.16 + aggregate-error: 3.1.0 + ansi-escapes: 4.3.2 + boxen: 5.1.2 + chokidar: 4.0.3 + ci-info: 2.0.0 + debug: 4.4.3(supports-color@9.4.0) + enquirer: 2.4.1 + env-paths: 2.2.1 + ethereum-cryptography: 1.2.0 + find-up: 5.0.0 + fp-ts: 1.19.3 + fs-extra: 7.0.1 + immutable: 4.3.7 + io-ts: 1.10.4 + json-stream-stringify: 3.1.6 + keccak: 3.0.4 + lodash: 4.17.21 + micro-eth-signer: 0.14.0 + mnemonist: 0.38.5 + mocha: 10.8.2 + p-map: 4.0.0 + picocolors: 1.1.1 + raw-body: 2.5.2 + resolve: 1.17.0 + semver: 6.3.1 + solc: 0.8.26(debug@4.4.3) + source-map-support: 0.5.21 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.15 + tsort: 0.0.1 + undici: 5.29.0 + uuid: 8.3.2 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + ts-node: 10.9.2(@types/node@20.19.14)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + hardhat@3.1.5(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@nomicfoundation/edr': 0.12.0-next.22 + '@nomicfoundation/hardhat-errors': 3.0.6 + '@nomicfoundation/hardhat-utils': 3.0.6 + '@nomicfoundation/hardhat-vendored': 3.0.0 + '@nomicfoundation/hardhat-zod-utils': 3.0.1(zod@3.25.76) + '@nomicfoundation/solidity-analyzer': 0.1.2 + '@sentry/core': 9.47.1 + adm-zip: 0.4.16 + chalk: 5.6.2 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@9.4.0) + enquirer: 2.4.1 + ethereum-cryptography: 2.2.1 + micro-eth-signer: 0.14.0 + p-map: 7.0.4 + resolve.exports: 2.0.3 + semver: 7.7.3 + tsx: 4.21.0 + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + has-ansi@2.0.0: dependencies: ansi-regex: 2.1.1 @@ -22464,6 +23719,11 @@ snapshots: dependencies: invert-kv: 1.0.0 + ldenv@0.3.16: + dependencies: + dotenv: 16.6.1 + dotenv-expand: 10.0.0 + level-codec@7.0.1: {} level-codec@9.0.2: @@ -23675,6 +24935,14 @@ snapshots: mute-stream@0.0.8: {} + named-logs-console@0.5.1: + dependencies: + named-logs: 0.4.1 + + named-logs@0.3.2: {} + + named-logs@0.4.1: {} + nan@2.23.0: optional: true @@ -23729,6 +24997,8 @@ snapshots: neo-async@2.6.2: {} + neoqs@6.13.0: {} + next-tick@1.1.0: {} ngeohash@0.6.3: {} @@ -24132,6 +25402,8 @@ snapshots: dependencies: aggregate-error: 3.1.0 + p-map@7.0.4: {} + p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -24546,6 +25818,8 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + promise-throttle@1.1.2: {} + promise-to-callback@1.0.0: dependencies: is-fn: 1.0.0 @@ -25013,8 +26287,12 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve-url@0.2.1: {} + resolve.exports@2.0.3: {} + resolve@1.1.7: {} resolve@1.17.0: @@ -25095,6 +26373,22 @@ snapshots: dependencies: bn.js: 5.2.2 + rocketh@0.17.13(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@rocketh/core': 0.17.8(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + change-case: 5.4.4 + eip-1193: 0.6.5 + eip-1193-jsonrpc-provider: 0.4.3 + ldenv: 0.3.16 + named-logs: 0.4.1 + viem: 2.44.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - typescript + - utf-8-validate + - zod + run-async@2.4.1: {} run-con@1.3.2: @@ -25494,6 +26788,8 @@ snapshots: slash@3.0.0: {} + slash@5.1.0: {} + slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -25726,6 +27022,29 @@ snapshots: shelljs: 0.8.5 web3-utils: 1.10.4 + solidity-coverage@0.8.17(hardhat@2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)): + dependencies: + '@ethersproject/abi': 5.8.0 + '@solidity-parser/parser': 0.20.2 + chalk: 2.4.2 + death: 1.1.0 + difflib: 0.2.4 + fs-extra: 8.1.0 + ghost-testrpc: 0.0.2 + global-modules: 2.0.0 + globby: 10.0.2 + hardhat: 2.28.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10) + jsonschema: 1.5.0 + lodash: 4.17.21 + mocha: 10.8.2 + node-emoji: 1.11.0 + pify: 4.0.1 + recursive-readdir: 2.2.3 + sc-istanbul: 0.4.6 + semver: 7.7.3 + shelljs: 0.8.5 + web3-utils: 1.10.4 + solidity-docgen@0.6.0-beta.36(hardhat@2.26.3(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.14)(typescript@5.9.3))(typescript@5.9.3)(utf-8-validate@5.0.10)): dependencies: handlebars: 4.7.8 @@ -26366,6 +27685,13 @@ snapshots: tsort@0.0.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 16c123378..50df878bf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,9 +1,6 @@ packages: - packages/* - packages/*/* - - '!packages/issuance' - - '!packages/issuance/*' - - '!packages/deployment' catalog: '@changesets/cli': ^2.29.7 From 32a5d1eb30d4baf92aa5f40adac71497c2d9d876 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 02:35:25 +0000 Subject: [PATCH 02/43] fix: upgrade CI Node.js to 22 for Hardhat 3.x compatibility Hardhat 3.x requires Node.js 22+ due to use of Iterator.prototype.flatMap --- .github/actions/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 23c24f1be..301183cba 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -17,7 +17,7 @@ runs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: 'pnpm' - name: Set up pnpm via Corepack shell: bash From fe6b1c4d8b0d1cd05b8a72db2bea8332a151b109 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:45:22 +0000 Subject: [PATCH 03/43] chore: lint fixes --- .../contracts/rewards/RewardsManager.sol | 24 +++++++------ .../contracts/data-service/DataService.sol | 2 ++ .../data-service/DataServiceStorage.sol | 1 + .../extensions/DataServiceFees.sol | 1 + .../extensions/DataServiceFeesStorage.sol | 1 + .../extensions/DataServicePausable.sol | 1 + .../DataServicePausableUpgradeable.sol | 4 +++ .../extensions/DataServiceRescuable.sol | 4 +++ .../utilities/ProvisionManager.sol | 5 +++ .../utilities/ProvisionManagerStorage.sol | 1 + .../contracts/libraries/LibFixedMath.sol | 1 + .../horizon/contracts/libraries/PPMMath.sol | 1 + .../contracts/payments/PaymentsEscrow.sol | 1 + .../collectors/GraphTallyCollector.sol | 1 + .../contracts/staking/HorizonStaking.sol | 3 ++ .../staking/HorizonStakingExtension.sol | 1 + .../staking/HorizonStakingStorage.sol | 3 ++ .../staking/libraries/ExponentialRebates.sol | 3 ++ .../contracts/staking/utilities/Managed.sol | 5 +++ .../contracts/utilities/Authorizable.sol | 1 + .../contracts/rewards/IRewardsManager.sol | 4 +-- .../data-service/IDataServicePausable.sol | 4 +-- .../subgraph-service/IDisputeManager.sol | 35 ++++++++++--------- .../subgraph-service/ISubgraphService.sol | 17 +++++---- .../contracts/common/BaseUpgradeable.sol | 5 +-- .../contracts/DisputeManager.sol | 17 ++++----- .../contracts/SubgraphService.sol | 17 +++++---- .../contracts/libraries/Allocation.sol | 3 ++ .../contracts/libraries/Attestation.sol | 5 +-- .../contracts/utilities/AllocationManager.sol | 17 ++++----- .../utilities/AttestationManager.sol | 4 +-- .../utilities/AttestationManagerStorage.sol | 1 + .../contracts/utilities/Directory.sol | 1 + 33 files changed, 119 insertions(+), 75 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 9a85b0274..66c569d39 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -3,9 +3,6 @@ pragma solidity 0.7.6; pragma abicoder v2; -// 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"; import { IERC165 } from "@openzeppelin/contracts/introspection/IERC165.sol"; @@ -141,6 +138,7 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I * @dev Modifier to restrict access to the subgraph availability oracle only */ modifier onlySubgraphAvailabilityOracle() { + // solhint-disable-next-line gas-small-strings require(msg.sender == address(subgraphAvailabilityOracle), "Caller must be the subgraph availability oracle"); _; } @@ -242,6 +240,7 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I // Check that the contract supports the IIssuanceAllocationDistribution interface // Allow zero address to disable the allocator if (newIssuanceAllocator != address(0)) { + // solhint-disable-next-line gas-small-strings require( IERC165(newIssuanceAllocator).supportsInterface(type(IIssuanceAllocationDistribution).interfaceId), "Contract does not support IIssuanceAllocationDistribution interface" @@ -279,6 +278,7 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I // Check that the contract supports the IRewardsEligibility interface // Allow zero address to disable the oracle if (newRewardsEligibilityOracle != address(0)) { + // solhint-disable-next-line gas-small-strings require( IERC165(newRewardsEligibilityOracle).supportsInterface(type(IRewardsEligibility).interfaceId), "Contract does not support IRewardsEligibility interface" @@ -301,6 +301,7 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I * regardless of which address was configured when the rewards were originally accrued. */ function setReclaimAddress(bytes32 reason, address newAddress) external override onlyGovernor { + // solhint-disable-next-line gas-small-strings require(reason != bytes32(0), "Cannot set reclaim address for (bytes32(0))"); address oldAddress = reclaimAddresses[reason]; @@ -317,19 +318,19 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I * @inheritdoc IRewardsManager * @dev Can only be called by the subgraph availability oracle */ - function setDenied(bytes32 _subgraphDeploymentID, bool _deny) external override onlySubgraphAvailabilityOracle { - _setDenied(_subgraphDeploymentID, _deny); + function setDenied(bytes32 subgraphDeploymentId, bool deny) external override onlySubgraphAvailabilityOracle { + _setDenied(subgraphDeploymentId, deny); } /** * @notice Internal: Denies to claim rewards for a subgraph. - * @param _subgraphDeploymentID Subgraph deployment ID - * @param _deny Whether to set the subgraph as denied for claiming rewards or not + * @param subgraphDeploymentId Subgraph deployment ID + * @param deny Whether to set the subgraph as denied for claiming rewards or not */ - function _setDenied(bytes32 _subgraphDeploymentID, bool _deny) private { - uint256 sinceBlock = _deny ? block.number : 0; - denylist[_subgraphDeploymentID] = sinceBlock; - emit RewardsDenylistUpdated(_subgraphDeploymentID, sinceBlock); + function _setDenied(bytes32 subgraphDeploymentId, bool deny) private { + uint256 sinceBlock = deny ? block.number : 0; + denylist[subgraphDeploymentId] = sinceBlock; + emit RewardsDenylistUpdated(subgraphDeploymentId, sinceBlock); } /// @inheritdoc IRewardsManager @@ -401,6 +402,7 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I uint256 subgraphSignalledTokens = curation().getCurationPoolTokens(_subgraphDeploymentID); // Only accrue rewards if over a threshold + // solhint-disable-next-line gas-strict-inequalities uint256 newRewards = (subgraphSignalledTokens >= minimumSubgraphSignal) // Accrue new rewards since last snapshot ? getAccRewardsPerSignal().sub(subgraph.accRewardsPerSignalSnapshot).mul(subgraphSignalledTokens).div( FIXED_POINT_SCALING_FACTOR diff --git a/packages/horizon/contracts/data-service/DataService.sol b/packages/horizon/contracts/data-service/DataService.sol index 21205dbfe..b09217b5f 100644 --- a/packages/horizon/contracts/data-service/DataService.sol +++ b/packages/horizon/contracts/data-service/DataService.sol @@ -59,6 +59,7 @@ abstract contract DataService is GraphDirectory, ProvisionManager, DataServiceV1 return _getDelegationRatio(); } + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract and any parent contracts. */ @@ -67,6 +68,7 @@ abstract contract DataService is GraphDirectory, ProvisionManager, DataServiceV1 __DataService_init_unchained(); } + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract. */ diff --git a/packages/horizon/contracts/data-service/DataServiceStorage.sol b/packages/horizon/contracts/data-service/DataServiceStorage.sol index 0b6d27e4b..15ed1ff01 100644 --- a/packages/horizon/contracts/data-service/DataServiceStorage.sol +++ b/packages/horizon/contracts/data-service/DataServiceStorage.sol @@ -9,6 +9,7 @@ pragma solidity 0.8.27; * bugs. We may have an active bug bounty program. */ abstract contract DataServiceV1Storage { + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract uint256[50] private __gap; diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index 91f5c5f4e..9ccebc040 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -143,6 +143,7 @@ abstract contract DataServiceFees is DataService, DataServiceFeesV1Storage, IDat return claims[_claimId].nextClaim; } + // forge-lint: disable-next-item(asm-keccak256) /** * @notice Builds a stake claim ID * @param _serviceProvider The address of the service provider diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol index bafb8fe52..eb80e3c04 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol @@ -22,6 +22,7 @@ abstract contract DataServiceFeesV1Storage { /// @notice Service providers registered in the data service mapping(address serviceProvider => ILinkedList.List list) public claimsLists; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract uint256[50] private __gap; diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol index b1bd4203a..b9e4d0d05 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol @@ -24,6 +24,7 @@ abstract contract DataServicePausable is Pausable, DataService, IDataServicePaus /// @notice List of pause guardians and their allowed status mapping(address pauseGuardian => bool allowed) public pauseGuardians; + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Checks if the caller is a pause guardian. */ diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol index ad792f914..f48cbff9f 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol @@ -20,9 +20,11 @@ abstract contract DataServicePausableUpgradeable is PausableUpgradeable, DataSer /// @notice List of pause guardians and their allowed status mapping(address pauseGuardian => bool allowed) public pauseGuardians; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades uint256[50] private __gap; + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Checks if the caller is a pause guardian. */ @@ -41,6 +43,7 @@ abstract contract DataServicePausableUpgradeable is PausableUpgradeable, DataSer _unpause(); } + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract and parent contracts */ @@ -49,6 +52,7 @@ abstract contract DataServicePausableUpgradeable is PausableUpgradeable, DataSer __DataServicePausable_init_unchained(); } + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract */ diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol b/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol index a6af01533..9d609a087 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceRescuable.sol @@ -27,10 +27,12 @@ abstract contract DataServiceRescuable is DataService, IDataServiceRescuable { /// @notice List of rescuers and their allowed status mapping(address rescuer => bool allowed) public rescuers; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract uint256[50] private __gap; + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Checks if the caller is a rescuer. */ @@ -39,11 +41,13 @@ abstract contract DataServiceRescuable is DataService, IDataServiceRescuable { _; } + // forge-lint: disable-next-item(mixed-case-function) /// @inheritdoc IDataServiceRescuable function rescueGRT(address to, uint256 tokens) external virtual onlyRescuer { _rescueTokens(to, address(_graphToken()), tokens); } + // forge-lint: disable-next-item(mixed-case-function) /// @inheritdoc IDataServiceRescuable function rescueETH(address payable to, uint256 tokens) external virtual onlyRescuer { _rescueTokens(to, Denominations.NATIVE_TOKEN, tokens); diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index 9d0db415b..ab0626cf2 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -111,6 +111,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa */ error ProvisionManagerProvisionNotFound(address serviceProvider); + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Checks if the caller is authorized to manage the provision of a service provider. * @param serviceProvider The address of the service provider. @@ -123,6 +124,8 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa _; } + // Warning: Virtual modifiers are deprecated and scheduled for removal. + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Checks if a provision of a service provider is valid according * to the parameter ranges established. @@ -135,6 +138,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa _; } + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract and any parent contracts. */ @@ -142,6 +146,7 @@ abstract contract ProvisionManager is Initializable, GraphDirectory, ProvisionMa __ProvisionManager_init_unchained(); } + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract. * @dev All parameters set to their entire range as valid. diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol index d2d3495ba..1004a324b 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol @@ -31,6 +31,7 @@ abstract contract ProvisionManagerV1Storage { /// @dev Max calculated as service provider's stake * delegationRatio uint32 internal _delegationRatio; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades /// Note that this contract is not upgradeable but might be inherited by an upgradeable contract uint256[50] private __gap; diff --git a/packages/horizon/contracts/libraries/LibFixedMath.sol b/packages/horizon/contracts/libraries/LibFixedMath.sol index 2468721b2..608ae34d3 100644 --- a/packages/horizon/contracts/libraries/LibFixedMath.sol +++ b/packages/horizon/contracts/libraries/LibFixedMath.sol @@ -22,6 +22,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines, gas-strict-inequalities +// forge-lint: disable-start(unsafe-typecast) /** * @title LibFixedMath diff --git a/packages/horizon/contracts/libraries/PPMMath.sol b/packages/horizon/contracts/libraries/PPMMath.sol index 998a912e8..97ac73db0 100644 --- a/packages/horizon/contracts/libraries/PPMMath.sol +++ b/packages/horizon/contracts/libraries/PPMMath.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities +// forge-lint: disable-start(mixed-case-function) /** * @title PPMMath library diff --git a/packages/horizon/contracts/payments/PaymentsEscrow.sol b/packages/horizon/contracts/payments/PaymentsEscrow.sol index 50a5386c9..2a4a5845a 100644 --- a/packages/horizon/contracts/payments/PaymentsEscrow.sol +++ b/packages/horizon/contracts/payments/PaymentsEscrow.sol @@ -38,6 +38,7 @@ contract PaymentsEscrow is Initializable, MulticallUpgradeable, GraphDirectory, mapping(address payer => mapping(address collector => mapping(address receiver => IPaymentsEscrow.EscrowAccount escrowAccount))) public escrowAccounts; + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Modifier to prevent function execution when contract is paused * @dev Reverts if the controller indicates the contract is paused diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol index eb33d931c..3d0c1bdcc 100644 --- a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol +++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.27; // solhint-disable gas-small-strings // solhint-disable gas-strict-inequalities // solhint-disable function-max-lines +// forge-lint: disable-start(mixed-case-function, mixed-case-variable) import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphTallyCollector } from "@graphprotocol/interfaces/contracts/horizon/IGraphTallyCollector.sol"; diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 73f48c354..3553f04c4 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -48,6 +48,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { /// @dev Minimum amount of delegation. uint256 private constant MIN_DELEGATION = 1e18; + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Checks that the caller is authorized to operate over a provision. * @param serviceProvider The address of the service provider. @@ -61,6 +62,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { _; } + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @notice Checks that the caller is authorized to operate over a provision or it is the verifier. * @param serviceProvider The address of the service provider. @@ -1045,6 +1047,7 @@ contract HorizonStaking is HorizonStakingBase, IHorizonStakingMain { ); require(thawRequestList.count < MAX_THAW_REQUESTS, HorizonStakingTooManyThawRequests()); + // forge-lint: disable-next-item(asm-keccak256) bytes32 thawRequestId = keccak256(abi.encodePacked(_serviceProvider, _verifier, _owner, thawRequestList.nonce)); ThawRequest storage thawRequest = _getThawRequest(_requestType, thawRequestId); thawRequest.shares = _shares; diff --git a/packages/horizon/contracts/staking/HorizonStakingExtension.sol b/packages/horizon/contracts/staking/HorizonStakingExtension.sol index b1adcde0d..2a57df32e 100644 --- a/packages/horizon/contracts/staking/HorizonStakingExtension.sol +++ b/packages/horizon/contracts/staking/HorizonStakingExtension.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines, gas-strict-inequalities +// forge-lint: disable-start(mixed-case-variable, mixed-case-function, unwrapped-modifier-logic) import { ICuration } from "@graphprotocol/interfaces/contracts/contracts/curation/ICuration.sol"; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; diff --git a/packages/horizon/contracts/staking/HorizonStakingStorage.sol b/packages/horizon/contracts/staking/HorizonStakingStorage.sol index 5f63af9df..710b677bf 100644 --- a/packages/horizon/contracts/staking/HorizonStakingStorage.sol +++ b/packages/horizon/contracts/staking/HorizonStakingStorage.sol @@ -2,6 +2,9 @@ pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// forge-lint: disable-start(mixed-case-variable) + import { IHorizonStakingExtension } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingExtension.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; diff --git a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol index 974e7197b..16ba299b5 100644 --- a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol +++ b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol @@ -2,6 +2,9 @@ pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// forge-lint: disable-start(unsafe-typecast) + import { LibFixedMath } from "../../libraries/LibFixedMath.sol"; /** diff --git a/packages/horizon/contracts/staking/utilities/Managed.sol b/packages/horizon/contracts/staking/utilities/Managed.sol index 88d2fb7c1..c4b10d1c6 100644 --- a/packages/horizon/contracts/staking/utilities/Managed.sol +++ b/packages/horizon/contracts/staking/utilities/Managed.sol @@ -19,12 +19,15 @@ import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; abstract contract Managed is GraphDirectory { // -- State -- + // forge-lint: disable-next-item(mixed-case-variable) /// @notice Controller that manages this contract address private __DEPRECATED_controller; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Cache for the addresses of the contracts retrieved from the controller mapping(bytes32 contractName => address contractAddress) private __DEPRECATED_addressCache; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap for future storage variables uint256[10] private __gap; @@ -43,6 +46,7 @@ abstract contract Managed is GraphDirectory { */ error ManagedOnlyGovernor(); + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @dev Revert if the controller is paused */ @@ -51,6 +55,7 @@ abstract contract Managed is GraphDirectory { _; } + // forge-lint: disable-next-item(unwrapped-modifier-logic) /** * @dev Revert if the caller is not the governor */ diff --git a/packages/horizon/contracts/utilities/Authorizable.sol b/packages/horizon/contracts/utilities/Authorizable.sol index 6af2e677f..531809e81 100644 --- a/packages/horizon/contracts/utilities/Authorizable.sol +++ b/packages/horizon/contracts/utilities/Authorizable.sol @@ -126,6 +126,7 @@ abstract contract Authorizable is IAuthorizable { ); // Generate the message hash + // forge-lint: disable-next-item(asm-keccak256) bytes32 messageHash = keccak256( abi.encodePacked(block.chainid, address(this), "authorizeSignerProof", _proofDeadline, msg.sender) ); diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index 9c297203a..ee387c5ac 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -39,9 +39,9 @@ interface IRewardsManager { /** * @notice Set the subgraph service address - * @param subgraphService Address of the subgraph service contract + * @param newSubgraphService Address of the subgraph service contract */ - function setSubgraphService(address subgraphService) external; + function setSubgraphService(address newSubgraphService) external; /** * @notice Set the rewards eligibility oracle address diff --git a/packages/interfaces/contracts/data-service/IDataServicePausable.sol b/packages/interfaces/contracts/data-service/IDataServicePausable.sol index c95ba124a..4f951d6c3 100644 --- a/packages/interfaces/contracts/data-service/IDataServicePausable.sol +++ b/packages/interfaces/contracts/data-service/IDataServicePausable.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.22; -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - import { IDataService } from "./IDataService.sol"; /** @@ -22,6 +19,7 @@ interface IDataServicePausable is IDataService { * @param allowed The allowed status of the pause guardian */ event PauseGuardianSet(address indexed account, bool allowed); + // solhint-disable-previous-line gas-indexed-events /** * @notice Emitted when a the caller is not a pause guardian diff --git a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol index da1324cc9..2732f2cad 100644 --- a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol +++ b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol @@ -2,9 +2,6 @@ pragma solidity ^0.8.22; -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - import { IAttestation } from "./internal/IAttestation.sol"; /** @@ -68,24 +65,28 @@ interface IDisputeManager { * @param disputePeriod The dispute period in seconds. */ event DisputePeriodSet(uint64 disputePeriod); + // solhint-disable-previous-line gas-indexed-events /** * @notice Emitted when dispute deposit is set. * @param disputeDeposit The dispute deposit required to create a dispute. */ event DisputeDepositSet(uint256 disputeDeposit); + // solhint-disable-previous-line gas-indexed-events /** * @notice Emitted when max slashing cut is set. * @param maxSlashingCut The maximum slashing cut that can be set. */ event MaxSlashingCutSet(uint32 maxSlashingCut); + // solhint-disable-previous-line gas-indexed-events /** * @notice Emitted when fisherman reward cut is set. * @param fishermanRewardCut The fisherman reward cut. */ event FishermanRewardCutSet(uint32 fishermanRewardCut); + // solhint-disable-previous-line gas-indexed-events /** * @notice Emitted when subgraph service is set. @@ -359,17 +360,17 @@ interface IDisputeManager { /** * @notice Initialize this contract. * @param owner The owner of the contract - * @param arbitrator Arbitrator role - * @param disputePeriod Dispute period in seconds - * @param disputeDeposit Deposit required to create a Dispute + * @param arbitrator_ Arbitrator role + * @param disputePeriod_ Dispute period in seconds + * @param disputeDeposit_ Deposit required to create a Dispute * @param fishermanRewardCut_ Percent of slashed funds for fisherman (ppm) * @param maxSlashingCut_ Maximum percentage of indexer stake that can be slashed (ppm) */ function initialize( address owner, - address arbitrator, - uint64 disputePeriod, - uint256 disputeDeposit, + address arbitrator_, + uint64 disputePeriod_, + uint256 disputeDeposit_, uint32 fishermanRewardCut_, uint32 maxSlashingCut_ ) external; @@ -377,23 +378,23 @@ interface IDisputeManager { /** * @notice Set the dispute period. * @dev Update the dispute period to `_disputePeriod` in seconds - * @param disputePeriod Dispute period in seconds + * @param newDisputePeriod Dispute period in seconds */ - function setDisputePeriod(uint64 disputePeriod) external; + function setDisputePeriod(uint64 newDisputePeriod) external; /** * @notice Set the arbitrator address. * @dev Update the arbitrator to `_arbitrator` - * @param arbitrator The address of the arbitration contract or party + * @param newArbitrator The address of the arbitration contract or party */ - function setArbitrator(address arbitrator) external; + function setArbitrator(address newArbitrator) external; /** * @notice Set the dispute deposit required to create a dispute. * @dev Update the dispute deposit to `_disputeDeposit` Graph Tokens - * @param disputeDeposit The dispute deposit in Graph Tokens + * @param newDisputeDeposit The dispute deposit in Graph Tokens */ - function setDisputeDeposit(uint256 disputeDeposit) external; + function setDisputeDeposit(uint256 newDisputeDeposit) external; /** * @notice Set the percent reward that the fisherman gets when slashing occurs. @@ -411,9 +412,9 @@ interface IDisputeManager { /** * @notice Set the subgraph service address. * @dev Update the subgraph service to `_subgraphService` - * @param subgraphService The address of the subgraph service contract + * @param newSubgraphService The address of the subgraph service contract */ - function setSubgraphService(address subgraphService) external; + function setSubgraphService(address newSubgraphService) external; // -- Dispute -- diff --git a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol index 5b084c7a7..04685224e 100644 --- a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol +++ b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.22; -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - import { IDataServiceFees } from "../data-service/IDataServiceFees.sol"; import { IGraphPayments } from "../horizon/IGraphPayments.sol"; @@ -62,12 +59,14 @@ interface ISubgraphService is IDataServiceFees { * @param ratio The stake to fees ratio */ event StakeToFeesRatioSet(uint256 ratio); + // solhint-disable-previous-line gas-indexed-events /** * @notice Emitted when curator cuts are set * @param curationCut The curation cut */ event CurationCutSet(uint256 curationCut); + // solhint-disable-previous-line gas-indexed-events /** * @notice Thrown when trying to set a curation cut that is not a valid PPM value @@ -229,16 +228,16 @@ interface ISubgraphService is IDataServiceFees { /** * @notice Sets the stake to fees ratio - * @param stakeToFeesRatio The stake to fees ratio + * @param newStakeToFeesRatio The stake to fees ratio */ - function setStakeToFeesRatio(uint256 stakeToFeesRatio) external; + function setStakeToFeesRatio(uint256 newStakeToFeesRatio) external; /** * @notice Sets the max POI staleness * See {AllocationManagerV1Storage-maxPOIStaleness} for more details. - * @param maxPOIStaleness The max POI staleness in seconds + * @param newMaxPoiStaleness The max POI staleness in seconds */ - function setMaxPOIStaleness(uint256 maxPOIStaleness) external; + function setMaxPOIStaleness(uint256 newMaxPoiStaleness) external; /** * @notice Sets the curators payment cut for query fees @@ -250,9 +249,9 @@ interface ISubgraphService is IDataServiceFees { /** * @notice Sets the payments destination for an indexer to receive payments * @dev Emits a {PaymentsDestinationSet} event - * @param paymentsDestination The address where payments should be sent + * @param newPaymentsDestination The address where payments should be sent */ - function setPaymentsDestination(address paymentsDestination) external; + function setPaymentsDestination(address newPaymentsDestination) external; /** * @notice Gets the details of an allocation diff --git a/packages/issuance/contracts/common/BaseUpgradeable.sol b/packages/issuance/contracts/common/BaseUpgradeable.sol index ead4f6a4f..ea608e97c 100644 --- a/packages/issuance/contracts/common/BaseUpgradeable.sol +++ b/packages/issuance/contracts/common/BaseUpgradeable.sol @@ -90,14 +90,14 @@ abstract contract BaseUpgradeable is Initializable, AccessControlUpgradeable, Pa // -- Initialization -- + // solhint-disable-next-line func-name-mixedcase + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Internal function to initialize the BaseUpgradeable contract * @dev This function is used by child contracts to initialize the BaseUpgradeable contract * @param governor Address that will have the GOVERNOR_ROLE */ function __BaseUpgradeable_init(address governor) internal { - // solhint-disable-previous-line func-name-mixedcase - __AccessControl_init(); __Pausable_init(); @@ -109,6 +109,7 @@ abstract contract BaseUpgradeable is Initializable, AccessControlUpgradeable, Pa * @dev This function sets up the governor role and role admin hierarchy * @param governor Address that will have the GOVERNOR_ROLE */ + // forge-lint: disable-next-line(mixed-case-function) function __BaseUpgradeable_init_unchained(address governor) internal { // solhint-disable-previous-line func-name-mixedcase diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index 6f73b2c5d..e4509181f 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines, gas-strict-inequalities +// forge-lint: disable-start(unwrapped-modifier-logic, asm-keccak256, named-struct-fields, mixed-case-variable) import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; @@ -303,18 +304,18 @@ contract DisputeManager is } /// @inheritdoc IDisputeManager - function setArbitrator(address arbitrator) external override onlyOwner { - _setArbitrator(arbitrator); + function setArbitrator(address newArbitrator) external override onlyOwner { + _setArbitrator(newArbitrator); } /// @inheritdoc IDisputeManager - function setDisputePeriod(uint64 disputePeriod) external override onlyOwner { - _setDisputePeriod(disputePeriod); + function setDisputePeriod(uint64 newDisputePeriod) external override onlyOwner { + _setDisputePeriod(newDisputePeriod); } /// @inheritdoc IDisputeManager - function setDisputeDeposit(uint256 disputeDeposit) external override onlyOwner { - _setDisputeDeposit(disputeDeposit); + function setDisputeDeposit(uint256 newDisputeDeposit) external override onlyOwner { + _setDisputeDeposit(newDisputeDeposit); } /// @inheritdoc IDisputeManager @@ -328,8 +329,8 @@ contract DisputeManager is } /// @inheritdoc IDisputeManager - function setSubgraphService(address subgraphService_) external override onlyOwner { - _setSubgraphService(subgraphService_); + function setSubgraphService(address newSubgraphService) external override onlyOwner { + _setSubgraphService(newSubgraphService); } /// @inheritdoc IDisputeManager diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index 0ba0b3035..d14eb9ffd 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -1,10 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-strict-inequalities -// solhint-disable function-max-lines - import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IGraphTallyCollector } from "@graphprotocol/interfaces/contracts/horizon/IGraphTallyCollector.sol"; @@ -331,9 +327,9 @@ contract SubgraphService is function migrateLegacyAllocation( address indexer, address allocationId, - bytes32 subgraphDeploymentID + bytes32 subgraphDeploymentId ) external override onlyOwner { - _migrateLegacyAllocation(indexer, allocationId, subgraphDeploymentID); + _migrateLegacyAllocation(indexer, allocationId, subgraphDeploymentId); } /// @inheritdoc ISubgraphService @@ -361,9 +357,10 @@ contract SubgraphService is _setStakeToFeesRatio(stakeToFeesRatio_); } + // forge-lint: disable-next-item(mixed-case-function) /// @inheritdoc ISubgraphService - function setMaxPOIStaleness(uint256 maxPOIStaleness_) external override onlyOwner { - _setMaxPOIStaleness(maxPOIStaleness_); + function setMaxPOIStaleness(uint256 maxPoiStaleness_) external override onlyOwner { + _setMaxPoiStaleness(maxPoiStaleness_); } /// @inheritdoc ISubgraphService @@ -491,6 +488,7 @@ contract SubgraphService is * be collected. * @return The amount of fees collected */ + // solhint-disable-next-line function-max-lines function _collectQueryFees(address _indexer, bytes calldata _data) private returns (uint256) { (IGraphTallyCollector.SignedRAV memory signedRav, uint256 tokensToCollect) = abi.decode( _data, @@ -530,6 +528,7 @@ contract SubgraphService is ); uint256 balanceAfter = _graphToken().balanceOf(address(this)); + // solhint-disable-next-line gas-strict-inequalities require(balanceAfter >= balanceBefore, SubgraphServiceInconsistentCollection(balanceBefore, balanceAfter)); tokensCurators = balanceAfter - balanceBefore; } @@ -578,7 +577,7 @@ contract SubgraphService is _allocations.get(allocationId).indexer == _indexer, SubgraphServiceAllocationNotAuthorized(_indexer, allocationId) ); - return _presentPOI(allocationId, poi_, poiMetadata_, _delegationRatio, paymentsDestination[_indexer]); + return _presentPoi(allocationId, poi_, poiMetadata_, _delegationRatio, paymentsDestination[_indexer]); } /** diff --git a/packages/subgraph-service/contracts/libraries/Allocation.sol b/packages/subgraph-service/contracts/libraries/Allocation.sol index 5a4e3cb52..dd34362c6 100644 --- a/packages/subgraph-service/contracts/libraries/Allocation.sol +++ b/packages/subgraph-service/contracts/libraries/Allocation.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; +// TODO: Re-enable and fix issues when publishing a new version +// forge-lint: disable-start(mixed-case-variable, mixed-case-function) + import { IAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocation.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; diff --git a/packages/subgraph-service/contracts/libraries/Attestation.sol b/packages/subgraph-service/contracts/libraries/Attestation.sol index 25bb6651f..558326509 100644 --- a/packages/subgraph-service/contracts/libraries/Attestation.sol +++ b/packages/subgraph-service/contracts/libraries/Attestation.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities +// forge-lint: disable-start(mixed-case-variable) import { IAttestation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAttestation.sol"; @@ -104,7 +105,7 @@ library Attestation { uint8 tempUint; // solhint-disable-next-line no-inline-assembly - assembly { + assembly ("memory-safe") { // Load the 32-byte word from memory starting at `_bytes + _start + 1` // The `0x1` accounts for the fact that we want only the first byte (uint8) // of the loaded 32 bytes. @@ -128,7 +129,7 @@ library Attestation { bytes32 tempBytes32; // solhint-disable-next-line no-inline-assembly - assembly { + assembly ("memory-safe") { tempBytes32 := mload(add(add(_bytes, 0x20), _start)) } diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index c58336e35..69fd27aca 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -1,11 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.27; -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events -// solhint-disable gas-small-strings -// solhint-disable function-max-lines - import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; @@ -171,6 +166,7 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca __AllocationManager_init_unchained(); } + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract */ @@ -284,7 +280,8 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca * @param _paymentsDestination The address where indexing rewards should be sent * @return The amount of tokens collected */ - function _presentPOI( + // solhint-disable-next-line function-max-lines + function _presentPoi( address _allocationId, bytes32 _poi, bytes memory _poiMetadata, @@ -480,11 +477,11 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca /** * @notice Sets the maximum amount of time, in seconds, allowed between presenting POIs to qualify for indexing rewards * @dev Emits a {MaxPOIStalenessSet} event - * @param _maxPOIStaleness The max POI staleness in seconds + * @param _maxPoiStaleness The max POI staleness in seconds */ - function _setMaxPOIStaleness(uint256 _maxPOIStaleness) internal { - maxPOIStaleness = _maxPOIStaleness; - emit MaxPOIStalenessSet(_maxPOIStaleness); + function _setMaxPoiStaleness(uint256 _maxPoiStaleness) internal { + maxPOIStaleness = _maxPoiStaleness; + emit MaxPOIStalenessSet(_maxPoiStaleness); } /** diff --git a/packages/subgraph-service/contracts/utilities/AttestationManager.sol b/packages/subgraph-service/contracts/utilities/AttestationManager.sol index 2c45fad3a..797e021ff 100644 --- a/packages/subgraph-service/contracts/utilities/AttestationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AttestationManager.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-small-strings +// solhint-disable func-name-mixedcase +// forge-lint: disable-start(mixed-case-function, asm-keccak256) import { IAttestation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAttestation.sol"; @@ -41,7 +43,6 @@ abstract contract AttestationManager is Initializable, AttestationManagerV1Stora * @notice Initialize the AttestationManager contract and parent contracts */ function __AttestationManager_init() internal onlyInitializing { - // solhint-disable-previous-line func-name-mixedcase __AttestationManager_init_unchained(); } @@ -49,7 +50,6 @@ abstract contract AttestationManager is Initializable, AttestationManagerV1Stora * @notice Initialize the AttestationManager contract */ function __AttestationManager_init_unchained() internal onlyInitializing { - // solhint-disable-previous-line func-name-mixedcase _domainSeparator = keccak256( abi.encode( DOMAIN_TYPE_HASH, diff --git a/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol index 1559a52fa..36f36414d 100644 --- a/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol +++ b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol @@ -12,6 +12,7 @@ abstract contract AttestationManagerV1Storage { /// @dev EIP712 domain separator bytes32 internal _domainSeparator; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades uint256[50] private __gap; } diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index 4bfc1daa0..2473df343 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.27; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-indexed-events +// forge-lint: disable-start(unwrapped-modifier-logic) import { IDisputeManager } from "@graphprotocol/interfaces/contracts/subgraph-service/IDisputeManager.sol"; import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-service/ISubgraphService.sol"; From e1d391d21197617eb26cdcad719f9c68518faf1a Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:45:50 +0000 Subject: [PATCH 04/43] refactor: lint optimisations --- .../contracts/rewards/RewardsManager.sol | 2 +- .../contracts/SubgraphService.sol | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 66c569d39..1cfdabfbd 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -428,7 +428,7 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I // - the new allocations on the subgraph service uint256 subgraphAllocatedTokens = 0; address[2] memory rewardsIssuers = [address(staking()), address(subgraphService)]; - for (uint256 i = 0; i < rewardsIssuers.length; i++) { + for (uint256 i = 0; i < rewardsIssuers.length; ++i) { if (rewardsIssuers[i] != address(0)) { subgraphAllocatedTokens += IRewardsIssuer(rewardsIssuers[i]).getSubgraphAllocatedTokens( _subgraphDeploymentID diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index d14eb9ffd..a5d8a191b 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -54,7 +54,7 @@ contract SubgraphService is * @param indexer The address of the indexer */ modifier onlyRegisteredIndexer(address indexer) { - require(bytes(indexers[indexer].url).length > 0, SubgraphServiceIndexerNotRegistered(indexer)); + _checkRegisteredIndexer(indexer); _; } @@ -458,6 +458,14 @@ contract SubgraphService is return (_disputeManager().getFishermanRewardCut(), DEFAULT_MAX_VERIFIER_CUT); } + /** + * @notice Checks that an indexer is registered + * @param indexer The address of the indexer + */ + function _checkRegisteredIndexer(address indexer) private view { + require(bytes(indexers[indexer].url).length > 0, SubgraphServiceIndexerNotRegistered(indexer)); + } + /** * @notice Collect query fees * Stake equal to the amount being collected times the `stakeToFeesRatio` is locked into a stake claim. @@ -501,11 +509,11 @@ contract SubgraphService is // Check that collectionId (256 bits) is a valid address (160 bits) // collectionId is expected to be a zero padded address so it's safe to cast to uint160 - require( - uint256(signedRav.rav.collectionId) <= type(uint160).max, - SubgraphServiceInvalidCollectionId(signedRav.rav.collectionId) - ); - address allocationId = address(uint160(uint256(signedRav.rav.collectionId))); + uint256 ravCollectionId = uint256(signedRav.rav.collectionId); + // solhint-disable-next-line gas-strict-inequalities + require(ravCollectionId <= type(uint160).max, SubgraphServiceInvalidCollectionId(signedRav.rav.collectionId)); + // forge-lint: disable-next-line(unsafe-typecast) + address allocationId = address(uint160(ravCollectionId)); IAllocation.State memory allocation = _allocations.get(allocationId); // Check RAV is consistent - RAV indexer must match the allocation's indexer From 4eea0a13699495b58417791280df4ad9b257df9d Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:47:34 +0000 Subject: [PATCH 05/43] feat(compiler): 0.8.33 with via IR Solidity compiler --- docs/CompilerUpgrade0833.md | 151 ++++++++++++++++++++ packages/subgraph-service/foundry.toml | 2 + packages/subgraph-service/hardhat.config.ts | 9 +- 3 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 docs/CompilerUpgrade0833.md diff --git a/docs/CompilerUpgrade0833.md b/docs/CompilerUpgrade0833.md new file mode 100644 index 000000000..03a1773c3 --- /dev/null +++ b/docs/CompilerUpgrade0833.md @@ -0,0 +1,151 @@ +# Compiler Upgrade: Solidity 0.8.33 + viaIR + +This document captures the bytecode size changes resulting from the compiler configuration upgrade in the `subgraph-service` and `issuance` packages. + +## Configuration Changes + +### subgraph-service + +| Setting | Before | After | +| ---------------- | ------- | ------- | +| Solidity Version | 0.8.27 | 0.8.33 | +| EVM Version | paris | cancun | +| Optimizer | enabled | enabled | +| Optimizer Runs | 10 | 100 | +| viaIR | false | true | + +### issuance + +| Setting | Before | After | +| ---------------- | ------- | ------- | +| Solidity Version | 0.8.27 | 0.8.33 | +| EVM Version | cancun | cancun | +| Optimizer | enabled | enabled | +| Optimizer Runs | 100 | 100 | +| viaIR | false | true | + +## Subgraph-Service Contract Bytecode Sizes + +All contracts defined in `packages/subgraph-service/contracts/`: + +| Contract | Source File | Before (KiB) | After (KiB) | Change (KiB) | Change (%) | +| --------------------------- | ------------------------------------------------- | ------------ | ----------- | ------------ | ---------- | +| **SubgraphService** | contracts/SubgraphService.sol | 24.455 | 23.110 | **-1.345** | -5.5% | +| **DisputeManager** | contracts/DisputeManager.sol | 13.278 | 10.917 | **-2.361** | -17.8% | +| Allocation | contracts/libraries/Allocation.sol | 0.084 | 0.056 | -0.028 | -33.3% | +| Attestation | contracts/libraries/Attestation.sol | 0.084 | 0.056 | -0.028 | -33.3% | +| LegacyAllocation | contracts/libraries/LegacyAllocation.sol | 0.084 | 0.056 | -0.028 | -33.3% | +| SubgraphServiceV1Storage | contracts/SubgraphServiceStorage.sol | (abstract) | (abstract) | - | - | +| DisputeManagerV1Storage | contracts/DisputeManagerStorage.sol | (abstract) | (abstract) | - | - | +| AllocationManager | contracts/utilities/AllocationManager.sol | (abstract) | (abstract) | - | - | +| AllocationManagerV1Storage | contracts/utilities/AllocationManagerStorage.sol | (abstract) | (abstract) | - | - | +| AttestationManager | contracts/utilities/AttestationManager.sol | (abstract) | (abstract) | - | - | +| AttestationManagerV1Storage | contracts/utilities/AttestationManagerStorage.sol | (abstract) | (abstract) | - | - | +| Directory | contracts/utilities/Directory.sol | (abstract) | (abstract) | - | - | + +### Initcode Size (Subgraph-Service Contracts) + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ------------------- | ------------ | ----------- | ------------ | +| **SubgraphService** | 26.109 | 24.894 | **-1.215** | +| **DisputeManager** | 14.649 | 12.342 | **-2.307** | + +## Issuance Contract Bytecode Sizes + +All contracts defined in `packages/issuance/contracts/`: + +| Contract | Source File | Before (KiB) | After (KiB) | Change (KiB) | Change (%) | +| ---------------------------- | -------------------------------------------------- | ------------ | ----------- | ------------ | ---------- | +| **IssuanceAllocator** | contracts/allocate/IssuanceAllocator.sol | 10.444 | 10.250 | **-0.194** | -1.9% | +| **RewardsEligibilityOracle** | contracts/eligibility/RewardsEligibilityOracle.sol | 4.316 | 4.554 | +0.238 | +5.5% | +| **DirectAllocation** | contracts/allocate/DirectAllocation.sol | 2.978 | 3.393 | +0.415 | +13.9% | +| BaseUpgradeable | contracts/common/BaseUpgradeable.sol | (abstract) | (abstract) | - | - | + +### Initcode Size (Issuance Contracts) + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------------------- | ------------ | ----------- | ------------ | +| **IssuanceAllocator** | 10.817 | 10.601 | **-0.216** | +| **RewardsEligibilityOracle** | 4.666 | 4.881 | +0.215 | +| **DirectAllocation** | 3.330 | 3.723 | +0.393 | + +### Test Contracts (Issuance) + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------------------- | ------------ | ----------- | ------------ | +| IssuanceAllocatorTestHarness | 10.641 | 10.331 | -0.310 | +| MockReentrantTarget | 1.886 | 1.535 | -0.351 | +| MockNotificationTracker | 0.495 | 0.438 | -0.057 | +| MockRevertingTarget | 0.342 | 0.250 | -0.092 | +| MockSimpleTarget | 0.293 | 0.237 | -0.056 | +| MockERC165 | 0.188 | 0.141 | -0.047 | + +## Dependency Library Sizes + +Libraries from horizon and other packages compiled as part of subgraph-service: + +### Horizon Libraries + +| Library | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------- | ------------ | ----------- | ------------ | +| LinkedList | 0.084 | 0.056 | -0.028 | +| TokenUtils | 0.084 | 0.056 | -0.028 | +| UintRange | 0.084 | 0.056 | -0.028 | +| MathUtils | 0.084 | 0.056 | -0.028 | +| PPMMath | 0.084 | 0.056 | -0.028 | +| ProvisionTracker | 0.084 | 0.056 | -0.028 | + +### OpenZeppelin Libraries + +| Library | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------- | ------------ | ----------- | ------------ | +| Address | 0.084 | 0.056 | -0.028 | +| Panic | 0.084 | 0.056 | -0.028 | +| Strings | 0.084 | 0.056 | -0.028 | +| Errors | 0.084 | 0.056 | -0.028 | +| MessageHashUtils | 0.084 | 0.056 | -0.028 | +| SafeCast | 0.084 | 0.056 | -0.028 | +| ECDSA | 0.084 | 0.056 | -0.028 | +| SignedMath | 0.084 | 0.056 | -0.028 | +| Math | 0.084 | 0.056 | -0.028 | + +### Interfaces Package + +| Contract | Before (KiB) | After (KiB) | Change (KiB) | +| ---------------- | ------------ | ----------- | ------------ | +| RewardsCondition | 0.458 | 0.520 | +0.062 | + +## Key Observations + +### subgraph-service + +1. **SubgraphService now fits within mainnet limit**: The 24 KiB contract size limit was exceeded before (24.455 KiB). After the upgrade, it's safely under at 23.110 KiB. + +2. **Significant savings on main contracts**: Despite increasing optimizer runs from 10 to 100 (which typically increases size for runtime gas savings), the viaIR pipeline produced smaller bytecode: + - SubgraphService: -1.345 KiB (-5.5%) + - DisputeManager: -2.361 KiB (-17.8%) + +3. **Abstract contracts have no bytecode**: Storage contracts (e.g., `SubgraphServiceV1Storage`), utility contracts (`AllocationManager`, `AttestationManager`, `Directory`) are inherited by deployable contracts and have no standalone bytecode. + +4. **Library stub sizes reduced**: All library stubs decreased from 0.084 KiB to 0.056 KiB (-33%), indicating more efficient metadata encoding. + +### issuance + +1. **IssuanceAllocator reduced**: The main contract decreased slightly (-0.194 KiB, -1.9%) with viaIR enabled. + +2. **Smaller contracts increased**: DirectAllocation (+13.9%) and RewardsEligibilityOracle (+5.5%) increased in size. This is expected behavior as viaIR optimizations are more effective on larger contracts with complex inheritance patterns. + +3. **Test contracts all decreased**: All mock/test contracts benefited from viaIR, showing -5% to -19% reductions. + +## Why viaIR Reduces Size + +The viaIR (Intermediate Representation) compilation pipeline: + +- Uses Yul as an intermediate language +- Enables more aggressive cross-function optimizations +- Removes redundant code paths more effectively +- Particularly beneficial for large contracts with complex inheritance + +## Date + +Comparison performed: 2026-01-25 diff --git a/packages/subgraph-service/foundry.toml b/packages/subgraph-service/foundry.toml index 8972a2202..26b73ce91 100644 --- a/packages/subgraph-service/foundry.toml +++ b/packages/subgraph-service/foundry.toml @@ -7,6 +7,8 @@ cache_path = 'cache_forge' fs_permissions = [{ access = "read", path = "./"}] optimizer = true optimizer_runs = 100 +via_ir = true +evm_version = 'cancun' # Exclude test files from coverage reports no_match_coverage = "(^test/|/mocks/)" diff --git a/packages/subgraph-service/hardhat.config.ts b/packages/subgraph-service/hardhat.config.ts index 5f24dc2f5..aca08e03c 100644 --- a/packages/subgraph-service/hardhat.config.ts +++ b/packages/subgraph-service/hardhat.config.ts @@ -19,12 +19,11 @@ const baseConfig = hardhatBaseConfig(require) const config: HardhatUserConfig = { ...baseConfig, solidity: { - version: '0.8.27', + version: '0.8.33', settings: { - optimizer: { - enabled: true, - runs: 10, - }, + optimizer: { enabled: true, runs: 100 }, + evmVersion: 'cancun', + viaIR: true, }, }, sourcify: { From 214fbcd112c6ef555a3d96a3c5389aa75b97be30 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 10:20:25 +0000 Subject: [PATCH 06/43] feat: subgraph-service pragma 0.8.33 --- packages/subgraph-service/contracts/DisputeManager.sol | 2 +- packages/subgraph-service/contracts/DisputeManagerStorage.sol | 2 +- packages/subgraph-service/contracts/SubgraphService.sol | 2 +- packages/subgraph-service/contracts/SubgraphServiceStorage.sol | 2 +- .../subgraph-service/contracts/utilities/AllocationManager.sol | 2 +- .../contracts/utilities/AllocationManagerStorage.sol | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index e4509181f..a07c44c0b 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines, gas-strict-inequalities diff --git a/packages/subgraph-service/contracts/DisputeManagerStorage.sol b/packages/subgraph-service/contracts/DisputeManagerStorage.sol index 38b6e3115..e90bca0bc 100644 --- a/packages/subgraph-service/contracts/DisputeManagerStorage.sol +++ b/packages/subgraph-service/contracts/DisputeManagerStorage.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { IDisputeManager } from "@graphprotocol/interfaces/contracts/subgraph-service/IDisputeManager.sol"; import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-service/ISubgraphService.sol"; diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index a5d8a191b..deda0c351 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; diff --git a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol index 04dc4abf9..c3ad0a93a 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-service/ISubgraphService.sol"; diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 69fd27aca..9fa89306e 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGraphPayments.sol"; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; diff --git a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol index a56e649fd..cc6cbda55 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { IAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocation.sol"; import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol"; From 0d04b7083c86cd61cf051815e032ed26f196c2a0 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:49:15 +0000 Subject: [PATCH 07/43] chore: update contract pragmas for 0.8.33 --- packages/contracts/contracts/governance/Controller.sol | 2 +- packages/contracts/contracts/governance/Governed.sol | 2 +- packages/contracts/contracts/governance/Pausable.sol | 2 +- packages/contracts/contracts/rewards/RewardsManagerStorage.sol | 2 +- packages/contracts/contracts/upgrades/GraphProxy.sol | 2 +- packages/contracts/contracts/upgrades/GraphProxyAdmin.sol | 2 +- packages/contracts/contracts/upgrades/GraphProxyStorage.sol | 2 +- packages/contracts/contracts/upgrades/GraphUpgradeable.sol | 2 +- packages/contracts/contracts/utils/TokenUtils.sol | 2 +- packages/horizon/contracts/data-service/DataService.sol | 2 +- packages/horizon/contracts/data-service/DataServiceStorage.sol | 2 +- .../contracts/data-service/extensions/DataServiceFees.sol | 2 +- .../data-service/extensions/DataServiceFeesStorage.sol | 2 +- .../data-service/extensions/DataServicePausableUpgradeable.sol | 2 +- .../contracts/data-service/libraries/ProvisionTracker.sol | 2 +- .../contracts/data-service/utilities/ProvisionManager.sol | 2 +- .../data-service/utilities/ProvisionManagerStorage.sol | 2 +- packages/horizon/contracts/libraries/LibFixedMath.sol | 2 +- packages/horizon/contracts/libraries/LinkedList.sol | 2 +- packages/horizon/contracts/libraries/MathUtils.sol | 2 +- packages/horizon/contracts/libraries/PPMMath.sol | 2 +- packages/horizon/contracts/libraries/UintRange.sol | 2 +- packages/horizon/contracts/payments/GraphPayments.sol | 2 +- packages/horizon/contracts/payments/PaymentsEscrow.sol | 2 +- .../contracts/payments/collectors/GraphTallyCollector.sol | 2 +- packages/horizon/contracts/staking/HorizonStaking.sol | 2 +- packages/horizon/contracts/staking/HorizonStakingBase.sol | 2 +- packages/horizon/contracts/staking/HorizonStakingExtension.sol | 2 +- packages/horizon/contracts/staking/HorizonStakingStorage.sol | 2 +- .../horizon/contracts/staking/libraries/ExponentialRebates.sol | 2 +- packages/horizon/contracts/staking/utilities/Managed.sol | 2 +- packages/horizon/contracts/utilities/Authorizable.sol | 2 +- packages/horizon/contracts/utilities/GraphDirectory.sol | 2 +- packages/issuance/contracts/allocate/DirectAllocation.sol | 2 +- packages/issuance/contracts/allocate/IssuanceAllocator.sol | 2 +- packages/issuance/contracts/common/BaseUpgradeable.sol | 2 +- .../issuance/contracts/eligibility/RewardsEligibilityOracle.sol | 2 +- packages/subgraph-service/contracts/libraries/Allocation.sol | 2 +- packages/subgraph-service/contracts/libraries/Attestation.sol | 2 +- .../subgraph-service/contracts/libraries/LegacyAllocation.sol | 2 +- .../subgraph-service/contracts/utilities/AttestationManager.sol | 2 +- .../contracts/utilities/AttestationManagerStorage.sol | 2 +- packages/subgraph-service/contracts/utilities/Directory.sol | 2 +- 43 files changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/contracts/contracts/governance/Controller.sol b/packages/contracts/contracts/governance/Controller.sol index c850542ab..3f289ca7d 100644 --- a/packages/contracts/contracts/governance/Controller.sol +++ b/packages/contracts/contracts/governance/Controller.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-indexed-events, gas-small-strings diff --git a/packages/contracts/contracts/governance/Governed.sol b/packages/contracts/contracts/governance/Governed.sol index 8c3446b88..d20df43a2 100644 --- a/packages/contracts/contracts/governance/Governed.sol +++ b/packages/contracts/contracts/governance/Governed.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 diff --git a/packages/contracts/contracts/governance/Pausable.sol b/packages/contracts/contracts/governance/Pausable.sol index bf260cb72..d7a1824f2 100644 --- a/packages/contracts/contracts/governance/Pausable.sol +++ b/packages/contracts/contracts/governance/Pausable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-indexed-events diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index 5cc134bf7..bacfb4221 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -5,7 +5,7 @@ // TODO: Re-enable and fix issues when publishing a new version // solhint-disable named-parameters-mapping -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationDistribution.sol"; import { IRewardsEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibility.sol"; diff --git a/packages/contracts/contracts/upgrades/GraphProxy.sol b/packages/contracts/contracts/upgrades/GraphProxy.sol index b787b476a..65216a4d7 100644 --- a/packages/contracts/contracts/upgrades/GraphProxy.sol +++ b/packages/contracts/contracts/upgrades/GraphProxy.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-small-strings diff --git a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol index 97f0b2e11..e72bf3626 100644 --- a/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol +++ b/packages/contracts/contracts/upgrades/GraphProxyAdmin.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 diff --git a/packages/contracts/contracts/upgrades/GraphProxyStorage.sol b/packages/contracts/contracts/upgrades/GraphProxyStorage.sol index 828af8e23..4c3d2e4de 100644 --- a/packages/contracts/contracts/upgrades/GraphProxyStorage.sol +++ b/packages/contracts/contracts/upgrades/GraphProxyStorage.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 diff --git a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol index 827082213..466084fba 100644 --- a/packages/contracts/contracts/upgrades/GraphUpgradeable.sol +++ b/packages/contracts/contracts/upgrades/GraphUpgradeable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 diff --git a/packages/contracts/contracts/utils/TokenUtils.sol b/packages/contracts/contracts/utils/TokenUtils.sol index b1c2290f6..10c244e26 100644 --- a/packages/contracts/contracts/utils/TokenUtils.sol +++ b/packages/contracts/contracts/utils/TokenUtils.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.7.6 || 0.8.27; +pragma solidity ^0.7.6 || 0.8.27 || 0.8.33; /* solhint-disable gas-custom-errors */ // Cannot use custom errors with 0.7.6 diff --git a/packages/horizon/contracts/data-service/DataService.sol b/packages/horizon/contracts/data-service/DataService.sol index b09217b5f..8206f4924 100644 --- a/packages/horizon/contracts/data-service/DataService.sol +++ b/packages/horizon/contracts/data-service/DataService.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { IDataService } from "@graphprotocol/interfaces/contracts/data-service/IDataService.sol"; diff --git a/packages/horizon/contracts/data-service/DataServiceStorage.sol b/packages/horizon/contracts/data-service/DataServiceStorage.sol index 15ed1ff01..3ce552a7f 100644 --- a/packages/horizon/contracts/data-service/DataServiceStorage.sol +++ b/packages/horizon/contracts/data-service/DataServiceStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; /** * @title DataServiceStorage diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol index 9ccebc040..0f8cf3653 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFees.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { IDataServiceFees } from "@graphprotocol/interfaces/contracts/data-service/IDataServiceFees.sol"; import { ILinkedList } from "@graphprotocol/interfaces/contracts/horizon/internal/ILinkedList.sol"; diff --git a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol index eb80e3c04..384149201 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServiceFeesStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { IDataServiceFees } from "@graphprotocol/interfaces/contracts/data-service/IDataServiceFees.sol"; diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol index f48cbff9f..7251039b0 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { IDataServicePausable } from "@graphprotocol/interfaces/contracts/data-service/IDataServicePausable.sol"; diff --git a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol index 42f4a7de9..8f7ddff8d 100644 --- a/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol +++ b/packages/horizon/contracts/data-service/libraries/ProvisionTracker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol index ab0626cf2..ec0be49c3 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-indexed-events diff --git a/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol index 1004a324b..02631d866 100644 --- a/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol +++ b/packages/horizon/contracts/data-service/utilities/ProvisionManagerStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; /** * @title Storage layout for the {ProvisionManager} helper contract. diff --git a/packages/horizon/contracts/libraries/LibFixedMath.sol b/packages/horizon/contracts/libraries/LibFixedMath.sol index 608ae34d3..f248a513d 100644 --- a/packages/horizon/contracts/libraries/LibFixedMath.sol +++ b/packages/horizon/contracts/libraries/LibFixedMath.sol @@ -18,7 +18,7 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines, gas-strict-inequalities diff --git a/packages/horizon/contracts/libraries/LinkedList.sol b/packages/horizon/contracts/libraries/LinkedList.sol index 083b1f436..24e5610a0 100644 --- a/packages/horizon/contracts/libraries/LinkedList.sol +++ b/packages/horizon/contracts/libraries/LinkedList.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-increment-by-one, gas-strict-inequalities diff --git a/packages/horizon/contracts/libraries/MathUtils.sol b/packages/horizon/contracts/libraries/MathUtils.sol index 6c0a09a1a..ec8cc8161 100644 --- a/packages/horizon/contracts/libraries/MathUtils.sol +++ b/packages/horizon/contracts/libraries/MathUtils.sol @@ -3,7 +3,7 @@ // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; /** * @title MathUtils Library diff --git a/packages/horizon/contracts/libraries/PPMMath.sol b/packages/horizon/contracts/libraries/PPMMath.sol index 97ac73db0..a3108d88b 100644 --- a/packages/horizon/contracts/libraries/PPMMath.sol +++ b/packages/horizon/contracts/libraries/PPMMath.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities diff --git a/packages/horizon/contracts/libraries/UintRange.sol b/packages/horizon/contracts/libraries/UintRange.sol index 7c9bdfdd8..c96222464 100644 --- a/packages/horizon/contracts/libraries/UintRange.sol +++ b/packages/horizon/contracts/libraries/UintRange.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities diff --git a/packages/horizon/contracts/payments/GraphPayments.sol b/packages/horizon/contracts/payments/GraphPayments.sol index 144a2daa1..276ce2100 100644 --- a/packages/horizon/contracts/payments/GraphPayments.sol +++ b/packages/horizon/contracts/payments/GraphPayments.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines diff --git a/packages/horizon/contracts/payments/PaymentsEscrow.sol b/packages/horizon/contracts/payments/PaymentsEscrow.sol index 2a4a5845a..2bc8ed966 100644 --- a/packages/horizon/contracts/payments/PaymentsEscrow.sol +++ b/packages/horizon/contracts/payments/PaymentsEscrow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities diff --git a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol index 3d0c1bdcc..9040219fc 100644 --- a/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol +++ b/packages/horizon/contracts/payments/collectors/GraphTallyCollector.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-small-strings diff --git a/packages/horizon/contracts/staking/HorizonStaking.sol b/packages/horizon/contracts/staking/HorizonStaking.sol index 3553f04c4..7040ac343 100644 --- a/packages/horizon/contracts/staking/HorizonStaking.sol +++ b/packages/horizon/contracts/staking/HorizonStaking.sol @@ -5,7 +5,7 @@ // solhint-disable gas-increment-by-one // solhint-disable function-max-lines -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingMain } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingMain.sol"; diff --git a/packages/horizon/contracts/staking/HorizonStakingBase.sol b/packages/horizon/contracts/staking/HorizonStakingBase.sol index 9c52a2171..615de4994 100644 --- a/packages/horizon/contracts/staking/HorizonStakingBase.sol +++ b/packages/horizon/contracts/staking/HorizonStakingBase.sol @@ -3,7 +3,7 @@ // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; import { IHorizonStakingBase } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingBase.sol"; diff --git a/packages/horizon/contracts/staking/HorizonStakingExtension.sol b/packages/horizon/contracts/staking/HorizonStakingExtension.sol index 2a57df32e..3258381b2 100644 --- a/packages/horizon/contracts/staking/HorizonStakingExtension.sol +++ b/packages/horizon/contracts/staking/HorizonStakingExtension.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable function-max-lines, gas-strict-inequalities diff --git a/packages/horizon/contracts/staking/HorizonStakingStorage.sol b/packages/horizon/contracts/staking/HorizonStakingStorage.sol index 710b677bf..1469d27a2 100644 --- a/packages/horizon/contracts/staking/HorizonStakingStorage.sol +++ b/packages/horizon/contracts/staking/HorizonStakingStorage.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // forge-lint: disable-start(mixed-case-variable) diff --git a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol index 16ba299b5..9e2544533 100644 --- a/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol +++ b/packages/horizon/contracts/staking/libraries/ExponentialRebates.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // forge-lint: disable-start(unsafe-typecast) diff --git a/packages/horizon/contracts/staking/utilities/Managed.sol b/packages/horizon/contracts/staking/utilities/Managed.sol index c4b10d1c6..8839912f5 100644 --- a/packages/horizon/contracts/staking/utilities/Managed.sol +++ b/packages/horizon/contracts/staking/utilities/Managed.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { GraphDirectory } from "../../utilities/GraphDirectory.sol"; diff --git a/packages/horizon/contracts/utilities/Authorizable.sol b/packages/horizon/contracts/utilities/Authorizable.sol index 531809e81..9cbd41672 100644 --- a/packages/horizon/contracts/utilities/Authorizable.sol +++ b/packages/horizon/contracts/utilities/Authorizable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities diff --git a/packages/horizon/contracts/utilities/GraphDirectory.sol b/packages/horizon/contracts/utilities/GraphDirectory.sol index 6e657c6d7..0534ca3c7 100644 --- a/packages/horizon/contracts/utilities/GraphDirectory.sol +++ b/packages/horizon/contracts/utilities/GraphDirectory.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.27 || 0.8.33; import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; diff --git a/packages/issuance/contracts/allocate/DirectAllocation.sol b/packages/issuance/contracts/allocate/DirectAllocation.sol index cbc042c14..4c048acf2 100644 --- a/packages/issuance/contracts/allocate/DirectAllocation.sol +++ b/packages/issuance/contracts/allocate/DirectAllocation.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol"; import { ISendTokens } from "@graphprotocol/interfaces/contracts/issuance/allocate/ISendTokens.sol"; diff --git a/packages/issuance/contracts/allocate/IssuanceAllocator.sol b/packages/issuance/contracts/allocate/IssuanceAllocator.sol index 8e5fbeeb4..4b8f15291 100644 --- a/packages/issuance/contracts/allocate/IssuanceAllocator.sol +++ b/packages/issuance/contracts/allocate/IssuanceAllocator.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { TargetIssuancePerBlock, diff --git a/packages/issuance/contracts/common/BaseUpgradeable.sol b/packages/issuance/contracts/common/BaseUpgradeable.sol index ea608e97c..0b445c139 100644 --- a/packages/issuance/contracts/common/BaseUpgradeable.sol +++ b/packages/issuance/contracts/common/BaseUpgradeable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; diff --git a/packages/issuance/contracts/eligibility/RewardsEligibilityOracle.sol b/packages/issuance/contracts/eligibility/RewardsEligibilityOracle.sol index 567705e17..bd2591a44 100644 --- a/packages/issuance/contracts/eligibility/RewardsEligibilityOracle.sol +++ b/packages/issuance/contracts/eligibility/RewardsEligibilityOracle.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { IRewardsEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibility.sol"; import { IRewardsEligibilityAdministration } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibilityAdministration.sol"; diff --git a/packages/subgraph-service/contracts/libraries/Allocation.sol b/packages/subgraph-service/contracts/libraries/Allocation.sol index dd34362c6..d5018e482 100644 --- a/packages/subgraph-service/contracts/libraries/Allocation.sol +++ b/packages/subgraph-service/contracts/libraries/Allocation.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // forge-lint: disable-start(mixed-case-variable, mixed-case-function) diff --git a/packages/subgraph-service/contracts/libraries/Attestation.sol b/packages/subgraph-service/contracts/libraries/Attestation.sol index 558326509..77c3a3fc2 100644 --- a/packages/subgraph-service/contracts/libraries/Attestation.sol +++ b/packages/subgraph-service/contracts/libraries/Attestation.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-strict-inequalities diff --git a/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol index 4717cefed..97b2be1dc 100644 --- a/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol +++ b/packages/subgraph-service/contracts/libraries/LegacyAllocation.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; import { IHorizonStaking } from "@graphprotocol/interfaces/contracts/horizon/IHorizonStaking.sol"; import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol"; diff --git a/packages/subgraph-service/contracts/utilities/AttestationManager.sol b/packages/subgraph-service/contracts/utilities/AttestationManager.sol index 797e021ff..4ba57e639 100644 --- a/packages/subgraph-service/contracts/utilities/AttestationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AttestationManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-small-strings diff --git a/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol index 36f36414d..40f4c614c 100644 --- a/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol +++ b/packages/subgraph-service/contracts/utilities/AttestationManagerStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; /** * @title AttestationManagerStorage diff --git a/packages/subgraph-service/contracts/utilities/Directory.sol b/packages/subgraph-service/contracts/utilities/Directory.sol index 2473df343..09d180a5d 100644 --- a/packages/subgraph-service/contracts/utilities/Directory.sol +++ b/packages/subgraph-service/contracts/utilities/Directory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.27; +pragma solidity 0.8.33; // TODO: Re-enable and fix issues when publishing a new version // solhint-disable gas-indexed-events From fb1702527282a470963a5e6893f90f16d29c6fb1 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:35:34 +0000 Subject: [PATCH 08/43] refactor: consolidate public IssuanceAllocator interface Move public members from toolshed into IIssuanceAllocator. Toolshed interfaces should only aggregate for type generation. See docs/InterfaceConsolidationPattern.md --- packages/contracts/contracts/rewards/RewardsManager.sol | 7 ------- .../contracts/issuance/allocate/IIssuanceTarget.sol | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 1cfdabfbd..6271b072c 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -89,13 +89,6 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I */ event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService); - /** - * @notice Emitted when the issuance allocator is set - * @param oldIssuanceAllocator Previous issuance allocator address - * @param newIssuanceAllocator New issuance allocator address - */ - event IssuanceAllocatorSet(address indexed oldIssuanceAllocator, address indexed newIssuanceAllocator); - /** * @notice Emitted when the rewards eligibility oracle contract is set * @param oldRewardsEligibilityOracle Previous rewards eligibility oracle address diff --git a/packages/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol b/packages/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol index 3fe539b95..b43bc948a 100644 --- a/packages/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol +++ b/packages/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol @@ -8,6 +8,13 @@ pragma solidity ^0.7.6 || ^0.8.0; * @notice Interface for contracts that receive issuance from an issuance allocator */ interface IIssuanceTarget { + /** + * @notice New issuance allocator set + * @param oldIssuanceAllocator Old issuance allocator address + * @param newIssuanceAllocator New issuance allocator address + */ + event IssuanceAllocatorSet(address indexed oldIssuanceAllocator, address indexed newIssuanceAllocator); + /** * @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 From 79e9538118cc7ce3434f095f4716aaa83e97c251 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:31:30 +0000 Subject: [PATCH 09/43] refactor: consolidate public RewardsManager interface Move public members from toolshed into IRewardsManager. Toolshed interfaces should only aggregate for type generation. See docs/InterfaceConsolidationPattern.md --- .../contracts/rewards/RewardsManager.sol | 109 +++++----------- .../rewards/RewardsManagerStorage.sol | 23 ++-- .../contracts/rewards/IRewardsManager.sol | 118 ++++++++++++++++-- .../rewards/IRewardsManagerDeprecated.sol | 40 ++++++ .../toolshed/IRewardsManagerToolshed.sol | 46 ++----- 5 files changed, 204 insertions(+), 132 deletions(-) create mode 100644 packages/interfaces/contracts/contracts/rewards/IRewardsManagerDeprecated.sol diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 6271b072c..10c29d561 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -14,6 +14,7 @@ import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token import { RewardsManagerV6Storage } from "./RewardsManagerStorage.sol"; import { IRewardsIssuer } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; +import { IRewardsManagerDeprecated } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManagerDeprecated.sol"; import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationDistribution.sol"; import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol"; import { IRewardsEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibility.sol"; @@ -43,88 +44,19 @@ import { RewardsReclaim } from "@graphprotocol/interfaces/contracts/contracts/re * 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 RewardsManagerV6Storage, GraphUpgradeable, IERC165, IRewardsManager, IIssuanceTarget { +contract RewardsManager is + GraphUpgradeable, + IERC165, + IRewardsManager, + IIssuanceTarget, + IRewardsManagerDeprecated, + RewardsManagerV6Storage +{ using SafeMath for uint256; /// @dev Fixed point scaling factor used for decimals in reward calculations uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; - // -- Events -- - - /** - * @notice Emitted when rewards are assigned to an indexer. - * @dev We use the Horizon prefix to change the event signature which makes network subgraph development much easier - * @param indexer Address of the indexer receiving rewards - * @param allocationID Address of the allocation receiving rewards - * @param amount Amount of rewards assigned - */ - event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); - - /** - * @notice Emitted when rewards are denied to an indexer - * @param indexer Address of the indexer being denied rewards - * @param allocationID Address of the allocation being denied rewards - */ - 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 - * @param sinceBlock Block number since when the subgraph is denied - */ - event RewardsDenylistUpdated(bytes32 indexed subgraphDeploymentID, uint256 sinceBlock); - - /** - * @notice Emitted when the subgraph service is set - * @param oldSubgraphService Previous subgraph service address - * @param newSubgraphService New subgraph service address - */ - 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 - ); - - /** - * @notice Emitted when a reclaim address is set - * @param reason The reclaim reason identifier - * @param oldAddress Previous address - * @param newAddress New address - */ - event ReclaimAddressSet(bytes32 indexed reason, address indexed oldAddress, address indexed newAddress); - - /** - * @notice Emitted when rewards are reclaimed to a configured address - * @param reason The reclaim reason identifier - * @param amount Amount of rewards reclaimed - * @param indexer Address of the indexer - * @param allocationID Address of the allocation - * @param subgraphDeploymentID Subgraph deployment ID for the allocation - * @param data Additional context data for the reclaim - */ - event RewardsReclaimed( - bytes32 indexed reason, - uint256 amount, - address indexed indexer, - address indexed allocationID, - bytes32 subgraphDeploymentID, - bytes data - ); - // -- Modifiers -- /** @@ -160,7 +92,7 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I // -- Config -- /** - * @inheritdoc IRewardsManager + * @inheritdoc IRewardsManagerDeprecated * @dev When an IssuanceAllocator is set, the effective issuance will be determined by the allocator, * but this local value can still be updated for cases when the allocator is later removed. * @@ -344,6 +276,27 @@ contract RewardsManager is RewardsManagerV6Storage, GraphUpgradeable, IERC165, I : issuancePerBlock; } + /** + * @inheritdoc IRewardsManager + */ + function getIssuanceAllocator() external view override returns (IIssuanceAllocationDistribution) { + return issuanceAllocator; + } + + /** + * @inheritdoc IRewardsManager + */ + function getReclaimAddress(bytes32 reason) external view override returns (address) { + return reclaimAddresses[reason]; + } + + /** + * @inheritdoc IRewardsManager + */ + function getRewardsEligibilityOracle() external view override returns (IRewardsEligibility) { + return rewardsEligibilityOracle; + } + /** * @inheritdoc IRewardsManager * @dev Linear formula: `x = r * t` diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index bacfb4221..0de74ac07 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -11,6 +11,7 @@ import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contr import { IRewardsEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibility.sol"; import { IRewardsIssuer } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol"; import { IRewardsManager } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManager.sol"; +import { IRewardsManagerDeprecated } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsManagerDeprecated.sol"; import { Managed } from "../governance/Managed.sol"; /** @@ -63,10 +64,10 @@ contract RewardsManagerV3Storage is RewardsManagerV2Storage { * @author Edge & Node * @notice Storage layout for RewardsManager V4 */ -contract RewardsManagerV4Storage is RewardsManagerV3Storage { +abstract contract RewardsManagerV4Storage is IRewardsManagerDeprecated, RewardsManagerV3Storage { /// @notice GRT issued for indexer rewards per block /// @dev Only used when issuanceAllocator is zero address. - uint256 public issuancePerBlock; + uint256 public override issuancePerBlock; } /** @@ -74,9 +75,9 @@ contract RewardsManagerV4Storage is RewardsManagerV3Storage { * @author Edge & Node * @notice Storage layout for RewardsManager V5 */ -contract RewardsManagerV5Storage is RewardsManagerV4Storage { +abstract contract RewardsManagerV5Storage is IRewardsManager, RewardsManagerV4Storage { /// @notice Address of the subgraph service - IRewardsIssuer public subgraphService; + IRewardsIssuer public override subgraphService; } /** @@ -85,12 +86,12 @@ contract RewardsManagerV5Storage is RewardsManagerV4Storage { * @notice Storage layout for RewardsManager V6 * Includes support for Rewards Eligibility Oracle, Issuance Allocator, and reclaim addresses. */ -contract RewardsManagerV6Storage is RewardsManagerV5Storage { - /// @notice Address of the rewards eligibility oracle contract - IRewardsEligibility public rewardsEligibilityOracle; - /// @notice Address of the issuance allocator - IIssuanceAllocationDistribution public issuanceAllocator; - /// @notice Mapping of reclaim reason identifiers to reclaim addresses +abstract contract RewardsManagerV6Storage is RewardsManagerV5Storage { + /// @dev Address of the rewards eligibility oracle contract + IRewardsEligibility internal rewardsEligibilityOracle; + /// @dev Address of the issuance allocator + IIssuanceAllocationDistribution internal issuanceAllocator; + /// @dev Mapping of reclaim reason identifiers to reclaim addresses /// @dev Uses bytes32 for extensibility. See RewardsReclaim library for canonical reasons. - mapping(bytes32 => address) public reclaimAddresses; + mapping(bytes32 => address) internal reclaimAddresses; } diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index ee387c5ac..26ac8c090 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -2,12 +2,93 @@ pragma solidity ^0.7.6 || ^0.8.0; +import { IIssuanceAllocationDistribution } from "../../issuance/allocate/IIssuanceAllocationDistribution.sol"; +import { IRewardsEligibility } from "../../issuance/eligibility/IRewardsEligibility.sol"; +import { IRewardsIssuer } from "./IRewardsIssuer.sol"; + /** * @title IRewardsManager * @author Edge & Node * @notice Interface for the RewardsManager contract that handles reward distribution */ interface IRewardsManager { + /** + * @notice Emitted when rewards are assigned to an indexer (Horizon version) + * @dev We use the Horizon prefix to change the event signature which makes network subgraph development much easier + * @param indexer Address of the indexer receiving rewards + * @param allocationID Address of the allocation receiving rewards + * @param amount Amount of rewards assigned + */ + event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); + // solhint-disable-previous-line gas-indexed-events + + /** + * @notice Emitted when rewards are denied to an indexer + * @param indexer Address of the indexer being denied rewards + * @param allocationID Address of the allocation being denied rewards + */ + event RewardsDenied(address indexed indexer, address indexed allocationID); + + /** + * @notice Emitted when a subgraph is denied for claiming rewards + * @param subgraphDeploymentID Subgraph deployment ID being denied + * @param sinceBlock Block number since when the subgraph is denied + */ + event RewardsDenylistUpdated(bytes32 indexed subgraphDeploymentID, uint256 sinceBlock); + // solhint-disable-previous-line gas-indexed-events + + /** + * @notice Emitted when the subgraph service is set + * @param oldSubgraphService Previous subgraph service address + * @param newSubgraphService New subgraph service address + */ + event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService); + + /** + * @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 denied + */ + event RewardsDeniedDueToEligibility(address indexed indexer, address indexed allocationID, uint256 amount); + // solhint-disable-previous-line gas-indexed-events + + /** + * @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 + ); + + /** + * @notice New reclaim address set + * @param reason The reclaim reason (or condition) identifier (see RewardsCondition library for canonical reasons) + * @param oldAddress Previous address for this reason + * @param newAddress New address for this reason + */ + event ReclaimAddressSet(bytes32 indexed reason, address indexed oldAddress, address indexed newAddress); + + /** + * @notice Rewards reclaimed to a configured address + * @param reason The reclaim reason identifier + * @param amount Amount of rewards reclaimed + * @param indexer Address of the indexer + * @param allocationID Address of the allocation + * @param subgraphDeploymentID Subgraph deployment ID for the allocation + * @param data Additional context data for the reclaim + */ + event RewardsReclaimed( + bytes32 indexed reason, + uint256 amount, + address indexed indexer, + address indexed allocationID, + bytes32 subgraphDeploymentID, + bytes data + ); + /** * @dev Stores accumulated rewards and snapshots related to a particular SubgraphDeployment * @param accRewardsForSubgraph Accumulated rewards for the subgraph @@ -24,12 +105,6 @@ interface IRewardsManager { // -- Config -- - /** - * @notice Set the issuance per block for rewards distribution - * @param issuancePerBlock The amount of tokens to issue per block - */ - function setIssuancePerBlock(uint256 issuancePerBlock) external; - /** * @notice Sets the minimum signaled tokens on a subgraph to start accruing rewards * @dev Can be set to zero which means that this feature is not being used @@ -87,8 +162,35 @@ interface IRewardsManager { // -- Getters -- /** - * @notice Gets the effective issuance per block for rewards - * @dev Takes into account the issuance allocator if set + * @notice Get the subgraph service address + * @return The subgraph service contract + */ + function subgraphService() external view returns (IRewardsIssuer); + + /** + * @notice Get the issuance allocator address + * @dev When set, this allocator controls issuance distribution instead of issuancePerBlock + * @return The issuance allocator contract (zero address if not set) + */ + function getIssuanceAllocator() external view returns (IIssuanceAllocationDistribution); + + /** + * @notice Get the reclaim address for a specific reason + * @param reason The reclaim reason identifier + * @return The address that receives reclaimed tokens for this reason (zero address if not set) + */ + function getReclaimAddress(bytes32 reason) external view returns (address); + + /** + * @notice Get the rewards eligibility oracle address + * @return The rewards eligibility oracle contract + */ + function getRewardsEligibilityOracle() external view returns (IRewardsEligibility); + + /** + * @notice Gets the effective issuance per block, accounting for the issuance allocator + * @dev When an issuance allocator is set, returns the allocated rate for this contract. + * Otherwise falls back to the raw storage value. * @return The effective issuance per block */ function getRewardsIssuancePerBlock() external view returns (uint256); diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManagerDeprecated.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManagerDeprecated.sol new file mode 100644 index 000000000..30342ab7c --- /dev/null +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManagerDeprecated.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6 || ^0.8.0; + +/** + * @title IRewardsManagerDeprecated + * @author Edge & Node + * @notice Deprecated methods for the RewardsManager contract. + * @dev This interface collects functions that exist on the deployed contract but are superseded + * by newer alternatives on {IRewardsManager}. It includes raw storage getters, legacy setters, + * and older computed getters whose behaviour may not reflect current protocol semantics. + * The behaviour of these functions may change in future protocol upgrades and should not be + * relied upon. New and upgraded integrations should use {IRewardsManager} instead. + * + * This interface does not aim to cover every deprecated function on the contract — only those + * for which existing code has a concrete dependency. Additional deprecated functions may be + * added in future as needed. + */ +interface IRewardsManagerDeprecated { + /** + * @notice Deprecated: Get the issuance rate per block + * @dev Currently returns the raw storage value which may not reflect the effective protocol + * issuance rate. Use {IRewardsManager-getAllocatedIssuancePerBlock} instead. + * + * WARNING: The value returned by this function may diverge from the effective issuance rate + * due to issuance allocation changes. When an issuance allocator is set, the effective rate is + * determined by the allocator while this function continues to return the raw storage value. + * @return issuanceRate Issuance rate per block + */ + function issuancePerBlock() external view returns (uint256 issuanceRate); + + /** + * @notice Deprecated: Set the issuance per block for rewards distribution + * @dev Prefer using the issuance allocator via {IRewardsManager-getIssuanceAllocator} for + * new deployments. This setter only affects the raw storage value and is ignored if an + * issuance allocator is set. + * @param newIssuancePerBlock Issance rate set per block + */ + function setIssuancePerBlock(uint256 newIssuancePerBlock) external; +} diff --git a/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol b/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol index 03b584ba4..61bdd1df5 100644 --- a/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IRewardsManagerToolshed.sol @@ -1,39 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.7.6 || ^0.8.0; -// solhint-disable use-natspec - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - import { IRewardsManager } from "../contracts/rewards/IRewardsManager.sol"; - -interface IRewardsManagerToolshed is IRewardsManager { - /** - * @dev Emitted when rewards are assigned to an indexer. - */ - event RewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); - - /** - * @notice Emitted when rewards are assigned to an indexer (Horizon version) - * @dev We use the Horizon prefix to change the event signature which makes network subgraph development much easier - */ - event HorizonRewardsAssigned(address indexed indexer, address indexed allocationID, uint256 amount); - - /** - * @notice Emitted when rewards are denied to an indexer - */ - event RewardsDenied(address indexed indexer, address indexed allocationID); - - /** - * @notice Emitted when a subgraph is denied for claiming rewards - */ - event RewardsDenylistUpdated(bytes32 indexed subgraphDeploymentID, uint256 sinceBlock); - - /** - * @notice Emitted when the subgraph service is set - */ - event SubgraphServiceSet(address indexed oldSubgraphService, address indexed newSubgraphService); - - function subgraphService() external view returns (address); -} +import { IRewardsManagerDeprecated } from "../contracts/rewards/IRewardsManagerDeprecated.sol"; +import { IIssuanceTarget } from "../issuance/allocate/IIssuanceTarget.sol"; + +/** + * @title IRewardsManagerToolshed + * @author Edge & Node + * @notice Aggregate interface for RewardsManager TypeScript type generation. + * @dev Combines all RewardsManager interfaces into a single artifact for Wagmi and ethers + * type generation. Not intended for use in Solidity code. + */ +interface IRewardsManagerToolshed is IRewardsManager, IIssuanceTarget, IRewardsManagerDeprecated {} From 8b365ad052714271e0e0ffc8e7e76bdce93e4fc5 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:00:45 +0000 Subject: [PATCH 10/43] refactor: consolidate public DisputeManager interface Move public members from toolshed into IDisputeManager. Toolshed interfaces should only aggregate for type generation. See docs/InterfaceConsolidationPattern.md --- .../subgraph-service/IDisputeManager.sol | 69 +++++++++++++++++++ .../toolshed/IDisputeManagerToolshed.sol | 54 +++------------ .../contracts/DisputeManager.sol | 4 +- .../contracts/DisputeManagerStorage.sol | 16 ++--- 4 files changed, 87 insertions(+), 56 deletions(-) diff --git a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol index 2732f2cad..f0661c6f4 100644 --- a/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol +++ b/packages/interfaces/contracts/subgraph-service/IDisputeManager.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.22; import { IAttestation } from "./internal/IAttestation.sol"; +import { ISubgraphService } from "./ISubgraphService.sol"; /** * @title IDisputeManager @@ -618,4 +619,72 @@ interface IDisputeManager { IAttestation.State memory attestation1, IAttestation.State memory attestation2 ) external pure returns (bool); + + // -- Storage Getters -- + + /** + * @notice Get the dispute period. + * @return Dispute period in seconds + */ + function disputePeriod() external view returns (uint64); + + /** + * @notice Get the fisherman reward cut. + * @return Fisherman reward cut in percentage (ppm) + */ + function fishermanRewardCut() external view returns (uint32); + + /** + * @notice Get the maximum percentage that can be used for slashing indexers. + * @return Max percentage slashing for disputes + */ + function maxSlashingCut() external view returns (uint32); + + /** + * @notice Get the dispute deposit. + * @return Dispute deposit + */ + function disputeDeposit() external view returns (uint256); + + /** + * @notice Get the subgraph service address. + * @return Subgraph service address + */ + function subgraphService() external view returns (ISubgraphService); + + /** + * @notice Get the arbitrator address. + * @return Arbitrator address + */ + function arbitrator() external view returns (address); + + /** + * @notice Get dispute details. + * @param disputeId The dispute ID + * @return indexer The indexer that is being disputed + * @return fisherman The fisherman that created the dispute + * @return deposit The amount of tokens deposited by the fisherman + * @return relatedDisputeId The link to a related dispute + * @return disputeType The type of dispute + * @return status The status of the dispute + * @return createdAt The timestamp when the dispute was created + * @return cancellableAt The timestamp when the dispute can be cancelled + * @return stakeSnapshot The stake snapshot of the indexer at the time of the dispute + */ + function disputes( + bytes32 disputeId + ) + external + view + returns ( + address indexer, + address fisherman, + uint256 deposit, + bytes32 relatedDisputeId, + DisputeType disputeType, + DisputeStatus status, + uint256 createdAt, + uint256 cancellableAt, + uint256 stakeSnapshot + ); } diff --git a/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol b/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol index 8c3e4390e..8d23024d8 100644 --- a/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol +++ b/packages/interfaces/contracts/toolshed/IDisputeManagerToolshed.sol @@ -1,52 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.22; -// solhint-disable use-natspec - import { IDisputeManager } from "../subgraph-service/IDisputeManager.sol"; import { IOwnable } from "./internal/IOwnable.sol"; -interface IDisputeManagerToolshed is IDisputeManager, IOwnable { - /** - * @notice Get the dispute period. - * @return Dispute period in seconds - */ - function disputePeriod() external view returns (uint64); - - /** - * @notice Get the fisherman reward cut. - * @return Fisherman reward cut in percentage (ppm) - */ - function fishermanRewardCut() external view returns (uint32); - - /** - * @notice Get the maximum percentage that can be used for slashing indexers. - * @return Max percentage slashing for disputes - */ - function maxSlashingCut() external view returns (uint32); - - /** - * @notice Get the dispute deposit. - * @return Dispute deposit - */ - function disputeDeposit() external view returns (uint256); - - /** - * @notice Get the subgraph service address. - * @return Subgraph service address - */ - function subgraphService() external view returns (address); - - /** - * @notice Get the arbitrator address. - * @return Arbitrator address - */ - function arbitrator() external view returns (address); - - /** - * @notice Get the dispute status. - * @param disputeId The dispute ID - * @return Dispute status - */ - function disputes(bytes32 disputeId) external view returns (IDisputeManager.Dispute memory); -} +/** + * @title IDisputeManagerToolshed + * @author Edge & Node + * @notice Aggregate interface for DisputeManager TypeScript type generation. + * @dev Combines all DisputeManager interfaces into a single artifact for Wagmi and ethers + * type generation. Not intended for use in Solidity code. + */ +interface IDisputeManagerToolshed is IDisputeManager, IOwnable {} diff --git a/packages/subgraph-service/contracts/DisputeManager.sol b/packages/subgraph-service/contracts/DisputeManager.sol index a07c44c0b..130182e4b 100644 --- a/packages/subgraph-service/contracts/DisputeManager.sol +++ b/packages/subgraph-service/contracts/DisputeManager.sol @@ -47,12 +47,12 @@ import { AttestationManager } from "./utilities/AttestationManager.sol"; * bugs. We may have an active bug bounty program. */ contract DisputeManager is + IDisputeManager, Initializable, OwnableUpgradeable, GraphDirectory, AttestationManager, - DisputeManagerV1Storage, - IDisputeManager + DisputeManagerV1Storage { using TokenUtils for IGraphToken; using PPMMath for uint256; diff --git a/packages/subgraph-service/contracts/DisputeManagerStorage.sol b/packages/subgraph-service/contracts/DisputeManagerStorage.sol index e90bca0bc..cb0766023 100644 --- a/packages/subgraph-service/contracts/DisputeManagerStorage.sol +++ b/packages/subgraph-service/contracts/DisputeManagerStorage.sol @@ -12,25 +12,25 @@ import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-s * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ -abstract contract DisputeManagerV1Storage { +abstract contract DisputeManagerV1Storage is IDisputeManager { /// @notice The Subgraph Service contract address - ISubgraphService public subgraphService; + ISubgraphService public override subgraphService; /// @notice The arbitrator is solely in control of arbitrating disputes - address public arbitrator; + address public override arbitrator; /// @notice dispute period in seconds - uint64 public disputePeriod; + uint64 public override disputePeriod; /// @notice Deposit required to create a Dispute - uint256 public disputeDeposit; + uint256 public override disputeDeposit; /// @notice Percentage of indexer slashed funds to assign as a reward to fisherman in successful dispute. In PPM. - uint32 public fishermanRewardCut; + uint32 public override fishermanRewardCut; /// @notice Maximum percentage of indexer stake that can be slashed on a dispute. In PPM. - uint32 public maxSlashingCut; + uint32 public override maxSlashingCut; /// @notice List of disputes created - mapping(bytes32 disputeId => IDisputeManager.Dispute dispute) public disputes; + mapping(bytes32 disputeId => IDisputeManager.Dispute dispute) public override disputes; } From 23b77bce74889325ca18b529b4f3a72d631611fb Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:06:01 +0000 Subject: [PATCH 11/43] refactor: consolidate public AllocationManager interface Move public members from toolshed into IAllocationManager. Toolshed interfaces should only aggregate for type generation. See docs/InterfaceConsolidationPattern.md --- .../internal/IAllocationManager.sol | 165 ++++++++++++++++++ .../toolshed/internal/IAllocationManager.sol | 60 ------- .../contracts/utilities/AllocationManager.sol | 126 +------------ .../utilities/AllocationManagerStorage.sol | 9 +- 4 files changed, 180 insertions(+), 180 deletions(-) create mode 100644 packages/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol delete mode 100644 packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol diff --git a/packages/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol b/packages/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol new file mode 100644 index 000000000..5c04767c9 --- /dev/null +++ b/packages/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.22; + +/** + * @title IAllocationManager interface + * @notice Interface for allocation lifecycle management events and errors + * @author Edge & Node + * @custom:security-contact Please email security+contracts@thegraph.com if you find any + * bugs. We may have an active bug bounty program. + */ +interface IAllocationManager { + // -- Events -- + + /** + * @notice Emitted when an indexer creates an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokens The amount of tokens allocated + * @param currentEpoch The current epoch + */ + event AllocationCreated( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokens, + uint256 currentEpoch + ); + + /** + * @notice Emitted when an indexer collects indexing rewards for an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokensRewards The amount of tokens collected + * @param tokensIndexerRewards The amount of tokens collected for the indexer + * @param tokensDelegationRewards The amount of tokens collected for delegators + * @param poi The POI presented + * @param poiMetadata The metadata associated with the POI + * @param currentEpoch The current epoch + */ + event IndexingRewardsCollected( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokensRewards, + uint256 tokensIndexerRewards, + uint256 tokensDelegationRewards, + bytes32 poi, + bytes poiMetadata, + uint256 currentEpoch + ); + + /** + * @notice Emitted when an indexer resizes an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param newTokens The new amount of tokens allocated + * @param oldTokens The old amount of tokens allocated + */ + event AllocationResized( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 newTokens, + uint256 oldTokens + ); + + /** + * @notice Emitted when an indexer closes an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param tokens The amount of tokens allocated + * @param forceClosed Whether the allocation was force closed + */ + event AllocationClosed( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + uint256 tokens, + bool forceClosed + ); + + /** + * @notice Emitted when a legacy allocation is migrated into the subgraph service + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + */ + event LegacyAllocationMigrated( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId + ); + + /** + * @notice Emitted when the maximum POI staleness is updated + * @param maxPOIStaleness The max POI staleness in seconds + */ + event MaxPOIStalenessSet(uint256 maxPOIStaleness); + // solhint-disable-previous-line gas-indexed-events + + /** + * @notice Emitted when an indexer presents a POI for an allocation + * @param indexer The address of the indexer + * @param allocationId The id of the allocation + * @param subgraphDeploymentId The id of the subgraph deployment + * @param poi The POI presented + * @param poiMetadata The metadata associated with the POI + * @param condition The rewards condition determined for this POI + */ + event POIPresented( + address indexed indexer, + address indexed allocationId, + bytes32 indexed subgraphDeploymentId, + bytes32 poi, + bytes poiMetadata, + bytes32 condition + ); + + // -- Errors -- + + /** + * @notice Thrown when an allocation proof is invalid + * Both `signer` and `allocationId` should match for a valid proof. + * @param signer The address that signed the proof + * @param allocationId The id of the allocation + */ + error AllocationManagerInvalidAllocationProof(address signer, address allocationId); + + /** + * @notice Thrown when attempting to create an allocation with a zero allocation id + */ + error AllocationManagerInvalidZeroAllocationId(); + + /** + * @notice Thrown when attempting to collect indexing rewards on a closed allocation + * @param allocationId The id of the allocation + */ + error AllocationManagerAllocationClosed(address allocationId); + + /** + * @notice Thrown when attempting to resize an allocation with the same size + * @param allocationId The id of the allocation + * @param tokens The amount of tokens + */ + error AllocationManagerAllocationSameSize(address allocationId, uint256 tokens); + + // -- Getters -- + + /** + * @notice Gets the allocation provision tracker for an indexer + * @param indexer The address of the indexer + * @return The amount of tokens tracked for the indexer's allocations + */ + function allocationProvisionTracker(address indexer) external view returns (uint256); + + /** + * @notice Gets the maximum POI staleness + * @return The max POI staleness in seconds + */ + function maxPOIStaleness() external view returns (uint256); +} diff --git a/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol b/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol deleted file mode 100644 index 9e6e8b704..000000000 --- a/packages/interfaces/contracts/toolshed/internal/IAllocationManager.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.22; - -// solhint-disable use-natspec - -// TODO: Re-enable and fix issues when publishing a new version -// solhint-disable gas-indexed-events - -interface IAllocationManager { - // Events - event AllocationCreated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokens, - uint256 currentEpoch - ); - - event IndexingRewardsCollected( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokensRewards, - uint256 tokensIndexerRewards, - uint256 tokensDelegationRewards, - bytes32 poi, - bytes poiMetadata, - uint256 currentEpoch - ); - - event AllocationResized( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 newTokens, - uint256 oldTokens - ); - - event AllocationClosed( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokens, - bool forceClosed - ); - - event LegacyAllocationMigrated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId - ); - - event MaxPOIStalenessSet(uint256 maxPOIStaleness); - - // Errors - error AllocationManagerInvalidAllocationProof(address signer, address allocationId); - error AllocationManagerInvalidZeroAllocationId(); - error AllocationManagerAllocationClosed(address allocationId); - error AllocationManagerAllocationSameSize(address allocationId, uint256 tokens); -} diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 9fa89306e..0bd665b3b 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -5,6 +5,7 @@ import { IGraphPayments } from "@graphprotocol/interfaces/contracts/horizon/IGra import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizon/internal/IHorizonStakingTypes.sol"; import { IAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocation.sol"; +import { IAllocationManager } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol"; import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol"; import { RewardsReclaim } from "@graphprotocol/interfaces/contracts/contracts/rewards/RewardsReclaim.sol"; @@ -28,7 +29,12 @@ import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/ * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ -abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, AllocationManagerV1Storage { +abstract contract AllocationManager is + IAllocationManager, + EIP712Upgradeable, + GraphDirectory, + AllocationManagerV1Storage +{ using ProvisionTracker for mapping(address => uint256); using Allocation for mapping(address => IAllocation.State); using Allocation for IAllocation.State; @@ -39,123 +45,9 @@ abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, Alloca ///@dev EIP712 typehash for allocation id proof bytes32 private constant EIP712_ALLOCATION_ID_PROOF_TYPEHASH = keccak256("AllocationIdProof(address indexer,address allocationId)"); + // solhint-disable-previous-line gas-small-strings - /** - * @notice Emitted when an indexer creates an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param tokens The amount of tokens allocated - * @param currentEpoch The current epoch - */ - event AllocationCreated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokens, - uint256 currentEpoch - ); - - /** - * @notice Emitted when an indexer collects indexing rewards for an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param tokensRewards The amount of tokens collected - * @param tokensIndexerRewards The amount of tokens collected for the indexer - * @param tokensDelegationRewards The amount of tokens collected for delegators - * @param poi The POI presented - * @param poiMetadata The metadata associated with the POI - * @param currentEpoch The current epoch - */ - event IndexingRewardsCollected( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokensRewards, - uint256 tokensIndexerRewards, - uint256 tokensDelegationRewards, - bytes32 poi, - bytes poiMetadata, - uint256 currentEpoch - ); - - /** - * @notice Emitted when an indexer resizes an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param newTokens The new amount of tokens allocated - * @param oldTokens The old amount of tokens allocated - */ - event AllocationResized( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 newTokens, - uint256 oldTokens - ); - - /** - * @notice Emitted when an indexer closes an allocation - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - * @param tokens The amount of tokens allocated - * @param forceClosed Whether the allocation was force closed - */ - event AllocationClosed( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId, - uint256 tokens, - bool forceClosed - ); - - /** - * @notice Emitted when a legacy allocation is migrated into the subgraph service - * @param indexer The address of the indexer - * @param allocationId The id of the allocation - * @param subgraphDeploymentId The id of the subgraph deployment - */ - event LegacyAllocationMigrated( - address indexed indexer, - address indexed allocationId, - bytes32 indexed subgraphDeploymentId - ); - - /** - * @notice Emitted when the maximum POI staleness is updated - * @param maxPOIStaleness The max POI staleness in seconds - */ - event MaxPOIStalenessSet(uint256 maxPOIStaleness); - - /** - * @notice Thrown when an allocation proof is invalid - * Both `signer` and `allocationId` should match for a valid proof. - * @param signer The address that signed the proof - * @param allocationId The id of the allocation - */ - error AllocationManagerInvalidAllocationProof(address signer, address allocationId); - - /** - * @notice Thrown when attempting to create an allocation with a zero allocation id - */ - error AllocationManagerInvalidZeroAllocationId(); - - /** - * @notice Thrown when attempting to collect indexing rewards on a closed allocationl - * @param allocationId The id of the allocation - */ - error AllocationManagerAllocationClosed(address allocationId); - - /** - * @notice Thrown when attempting to resize an allocation with the same size - * @param allocationId The id of the allocation - * @param tokens The amount of tokens - */ - error AllocationManagerAllocationSameSize(address allocationId, uint256 tokens); - + // forge-lint: disable-next-item(mixed-case-function) /** * @notice Initializes the contract and parent contracts * @param _name The name to use for EIP712 domain separation diff --git a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol index cc6cbda55..053b32a70 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManagerStorage.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.33; import { IAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocation.sol"; +import { IAllocationManager } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol"; import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol"; /** @@ -11,7 +12,7 @@ import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph- * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ -abstract contract AllocationManagerV1Storage { +abstract contract AllocationManagerV1Storage is IAllocationManager { /// @notice Allocation details mapping(address allocationId => IAllocation.State allocation) internal _allocations; @@ -19,15 +20,17 @@ abstract contract AllocationManagerV1Storage { mapping(address allocationId => ILegacyAllocation.State allocation) internal _legacyAllocations; /// @notice Tracks allocated tokens per indexer - mapping(address indexer => uint256 tokens) public allocationProvisionTracker; + mapping(address indexer => uint256 tokens) public override allocationProvisionTracker; + // forge-lint: disable-next-item(mixed-case-variable) /// @notice Maximum amount of time, in seconds, allowed between presenting POIs to qualify for indexing rewards - uint256 public maxPOIStaleness; + uint256 public override maxPOIStaleness; /// @notice Track total tokens allocated per subgraph deployment /// @dev Used to calculate indexing rewards mapping(bytes32 subgraphDeploymentId => uint256 tokens) internal _subgraphAllocatedTokens; + // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades uint256[50] private __gap; } From b9b7a7c014c9e7797740f9b2e801bcd00c7411fc Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:00:16 +0000 Subject: [PATCH 12/43] refactor: consolidate public DataServicePausable interface Move public members from toolshed into IDataServicePausable. Toolshed interfaces should only aggregate for type generation. See docs/InterfaceConsolidationPattern.md --- .../data-service/extensions/DataServicePausable.sol | 2 +- .../extensions/DataServicePausableUpgradeable.sol | 2 +- .../contracts/data-service/IDataServicePausable.sol | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol index b9e4d0d05..7d0c8c522 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausable.sol @@ -22,7 +22,7 @@ import { DataService } from "../DataService.sol"; */ abstract contract DataServicePausable is Pausable, DataService, IDataServicePausable { /// @notice List of pause guardians and their allowed status - mapping(address pauseGuardian => bool allowed) public pauseGuardians; + mapping(address pauseGuardian => bool allowed) public override pauseGuardians; // forge-lint: disable-next-item(unwrapped-modifier-logic) /** diff --git a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol index 7251039b0..6dc2433ce 100644 --- a/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol +++ b/packages/horizon/contracts/data-service/extensions/DataServicePausableUpgradeable.sol @@ -18,7 +18,7 @@ import { DataService } from "../DataService.sol"; */ abstract contract DataServicePausableUpgradeable is PausableUpgradeable, DataService, IDataServicePausable { /// @notice List of pause guardians and their allowed status - mapping(address pauseGuardian => bool allowed) public pauseGuardians; + mapping(address pauseGuardian => bool allowed) public override pauseGuardians; // forge-lint: disable-next-item(mixed-case-variable) /// @dev Gap to allow adding variables in future upgrades diff --git a/packages/interfaces/contracts/data-service/IDataServicePausable.sol b/packages/interfaces/contracts/data-service/IDataServicePausable.sol index 4f951d6c3..e9db470cc 100644 --- a/packages/interfaces/contracts/data-service/IDataServicePausable.sol +++ b/packages/interfaces/contracts/data-service/IDataServicePausable.sol @@ -53,4 +53,11 @@ interface IDataServicePausable is IDataService { * - The contract must be paused */ function unpause() external; + + /** + * @notice Gets the allowed status of a pause guardian + * @param pauseGuardian The address of the pause guardian + * @return The allowed status of the pause guardian + */ + function pauseGuardians(address pauseGuardian) external view returns (bool); } From ce61f315b2529012c214c5abcb5d33d671a2cced Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 11:01:33 +0000 Subject: [PATCH 13/43] refactor: consolidate public SubgraphService interface Move public storage getters from ISubgraphServiceToolshed into ISubgraphService. Toolshed interfaces should only aggregate for type generation, not define unique function signatures. See docs/InterfaceConsolidationPattern.md --- .../subgraph-service/ISubgraphService.sol | 29 +++++++++ .../toolshed/ISubgraphServiceToolshed.sol | 64 +++---------------- .../contracts/SubgraphService.sol | 4 +- .../contracts/SubgraphServiceStorage.sol | 10 +-- 4 files changed, 46 insertions(+), 61 deletions(-) diff --git a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol index 04685224e..db0bdae3f 100644 --- a/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol +++ b/packages/interfaces/contracts/subgraph-service/ISubgraphService.sol @@ -301,4 +301,33 @@ interface ISubgraphService is IDataServiceFees { * @return The address of the curation contract */ function getCuration() external view returns (address); + + /** + * @notice Gets the indexer details + * @dev Note that this storage getter actually returns a {Indexer} struct, but ethers v6 is not + * good at dealing with dynamic types on return values. + * @param indexer The address of the indexer + * @return url The URL where the indexer can be reached at for queries + * @return geoHash The indexer's geo location, expressed as a geo hash + */ + function indexers(address indexer) external view returns (string memory url, string memory geoHash); + + /** + * @notice Gets the stake to fees ratio + * @return The stake to fees ratio + */ + function stakeToFeesRatio() external view returns (uint256); + + /** + * @notice Gets the curation fees cut + * @return The curation fees cut + */ + function curationFeesCut() external view returns (uint256); + + /** + * @notice Gets the payments destination + * @param indexer The address of the indexer + * @return The payments destination + */ + function paymentsDestination(address indexer) external view returns (address); } diff --git a/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol b/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol index a7a05fbcd..231458700 100644 --- a/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol +++ b/packages/interfaces/contracts/toolshed/ISubgraphServiceToolshed.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.22; -// solhint-disable use-natspec - import { ISubgraphService } from "../subgraph-service/ISubgraphService.sol"; import { IOwnable } from "./internal/IOwnable.sol"; import { IPausable } from "./internal/IPausable.sol"; @@ -11,8 +9,15 @@ import { IProvisionManager } from "./internal/IProvisionManager.sol"; import { IProvisionTracker } from "./internal/IProvisionTracker.sol"; import { IDataServicePausable } from "../data-service/IDataServicePausable.sol"; import { IMulticall } from "../contracts/base/IMulticall.sol"; -import { IAllocationManager } from "./internal/IAllocationManager.sol"; - +import { IAllocationManager } from "../subgraph-service/internal/IAllocationManager.sol"; + +/** + * @title ISubgraphServiceToolshed + * @author Edge & Node + * @notice Aggregate interface for SubgraphService TypeScript type generation. + * @dev Combines all SubgraphService interfaces into a single artifact for Wagmi and ethers + * type generation. Not intended for use in Solidity code. + */ interface ISubgraphServiceToolshed is ISubgraphService, IAllocationManager, @@ -23,53 +28,4 @@ interface ISubgraphServiceToolshed is IProvisionManager, IProvisionTracker, IMulticall -{ - /** - * @notice Gets the indexer details - * @dev Note that this storage getter actually returns a ISubgraphService.Indexer struct, but ethers v6 is not - * good at dealing with dynamic types on return values. - * @param indexer The address of the indexer - * @return url The URL where the indexer can be reached at for queries - * @return geoHash The indexer's geo location, expressed as a geo hash - */ - function indexers(address indexer) external view returns (string memory url, string memory geoHash); - - /** - * @notice Gets the allocation provision tracker - * @param indexer The address of the indexer - * @return The allocation provision tracker - */ - function allocationProvisionTracker(address indexer) external view returns (uint256); - - /** - * @notice Gets the stake to fees ratio - * @return The stake to fees ratio - */ - function stakeToFeesRatio() external view returns (uint256); - - /** - * @notice Gets the max POI staleness - * @return The max POI staleness - */ - function maxPOIStaleness() external view returns (uint256); - - /** - * @notice Gets the curation fees cut - * @return The curation fees cut - */ - function curationFeesCut() external view returns (uint256); - - /** - * @notice Gets the pause guardians - * @param pauseGuardian The address of the pause guardian - * @return The allowed status of the pause guardian - */ - function pauseGuardians(address pauseGuardian) external view returns (bool); - - /** - * @notice Gets the payments destination - * @param indexer The address of the indexer - * @return The payments destination - */ - function paymentsDestination(address indexer) external view returns (address); -} +{} diff --git a/packages/subgraph-service/contracts/SubgraphService.sol b/packages/subgraph-service/contracts/SubgraphService.sol index deda0c351..2eb8e0a9f 100644 --- a/packages/subgraph-service/contracts/SubgraphService.sol +++ b/packages/subgraph-service/contracts/SubgraphService.sol @@ -40,9 +40,9 @@ contract SubgraphService is DataServiceFees, Directory, AllocationManager, - SubgraphServiceV1Storage, IRewardsIssuer, - ISubgraphService + ISubgraphService, + SubgraphServiceV1Storage { using PPMMath for uint256; using Allocation for mapping(address => IAllocation.State); diff --git a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol index c3ad0a93a..67accbb5a 100644 --- a/packages/subgraph-service/contracts/SubgraphServiceStorage.sol +++ b/packages/subgraph-service/contracts/SubgraphServiceStorage.sol @@ -10,16 +10,16 @@ import { ISubgraphService } from "@graphprotocol/interfaces/contracts/subgraph-s * @custom:security-contact Please email security+contracts@thegraph.com if you find any * bugs. We may have an active bug bounty program. */ -abstract contract SubgraphServiceV1Storage { +abstract contract SubgraphServiceV1Storage is ISubgraphService { /// @notice Service providers registered in the data service - mapping(address indexer => ISubgraphService.Indexer details) public indexers; + mapping(address indexer => ISubgraphService.Indexer details) public override indexers; ///@notice Multiplier for how many tokens back collected query fees - uint256 public stakeToFeesRatio; + uint256 public override stakeToFeesRatio; /// @notice The cut curators take from query fee payments. In PPM. - uint256 public curationFeesCut; + uint256 public override curationFeesCut; /// @notice Destination of indexer payments - mapping(address indexer => address destination) public paymentsDestination; + mapping(address indexer => address destination) public override paymentsDestination; } From 4ef521b563f4d3f9377476e3c67808c0a446e6b8 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:04:54 +0000 Subject: [PATCH 14/43] chore: add local minimal IGraphToken interface for issuance package Creates a local OZ5-compatible IGraphToken interface to resolve dependency conflicts between issuance package (OZ5) and interfaces package (OZ3). The minimal interface includes only IERC20 + mint(), which is all the issuance contracts require. --- .../contracts/common/BaseUpgradeable.sol | 16 ++++++++++---- .../issuance/contracts/common/IGraphToken.sol | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 packages/issuance/contracts/common/IGraphToken.sol diff --git a/packages/issuance/contracts/common/BaseUpgradeable.sol b/packages/issuance/contracts/common/BaseUpgradeable.sol index 0b445c139..cd5dae620 100644 --- a/packages/issuance/contracts/common/BaseUpgradeable.sol +++ b/packages/issuance/contracts/common/BaseUpgradeable.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.33; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; -import { IGraphToken } from "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; +import { IGraphToken } from "./IGraphToken.sol"; import { IPausableControl } from "@graphprotocol/interfaces/contracts/issuance/common/IPausableControl.sol"; /** @@ -13,13 +13,21 @@ import { IPausableControl } from "@graphprotocol/interfaces/contracts/issuance/c * @author Edge & Node * @notice A base contract that provides role-based access control and pausability. * - * @dev This contract combines OpenZeppelin's AccessControl and Pausable + * @dev This contract combines OpenZeppelin's AccessControlEnumerable and Pausable * to provide a standardized way to manage access control and pausing functionality. + * Using AccessControlEnumerable (rather than base AccessControl) enables on-chain + * enumeration of role members via getRoleMemberCount() and getRoleMember(), which + * is useful for deployment verification and auditing. * It uses ERC-7201 namespaced storage pattern for better storage isolation. * This contract is abstract and meant to be inherited by other contracts. * @custom:security-contact Please email security+contracts@thegraph.com if you find any bugs. We might have an active bug bounty program. */ -abstract contract BaseUpgradeable is Initializable, AccessControlUpgradeable, PausableUpgradeable, IPausableControl { +abstract contract BaseUpgradeable is + Initializable, + AccessControlEnumerableUpgradeable, + PausableUpgradeable, + IPausableControl +{ // -- Constants -- /// @notice One million - used as the denominator for values provided as Parts Per Million (PPM) @@ -98,7 +106,7 @@ abstract contract BaseUpgradeable is Initializable, AccessControlUpgradeable, Pa * @param governor Address that will have the GOVERNOR_ROLE */ function __BaseUpgradeable_init(address governor) internal { - __AccessControl_init(); + __AccessControlEnumerable_init(); __Pausable_init(); __BaseUpgradeable_init_unchained(governor); diff --git a/packages/issuance/contracts/common/IGraphToken.sol b/packages/issuance/contracts/common/IGraphToken.sol new file mode 100644 index 000000000..dc5c17414 --- /dev/null +++ b/packages/issuance/contracts/common/IGraphToken.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title IGraphToken + * @author Edge & Node + * @notice Minimal interface for the Graph Token contract used by issuance contracts + * @dev Extends IERC20 with mint capability. This interface is compatible with OZ 5.x. + */ +interface IGraphToken is IERC20 { + /** + * @notice Mints new tokens to a specified account + * @dev Only callable by accounts with minter role + * @param to The account to mint tokens to + * @param amount The amount of tokens to mint + */ + function mint(address to, uint256 amount) external; +} From 2252c1908bde61b8359718772fd866b28145df3f Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:22:27 +0000 Subject: [PATCH 15/43] fix: correct IGraphProxyAdmin interface signatures Correct acceptProxy and acceptProxyAndCall signatures. Interface mistakenly had single-param signatures from GraphUpgradeable instead of two-param signatures from GraphProxyAdmin. --- docs/IGraphProxyAdminInterfaceFix.md | 201 ++++++++++++++++++ .../contracts/upgrades/IGraphProxyAdmin.sol | 6 +- 2 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 docs/IGraphProxyAdminInterfaceFix.md diff --git a/docs/IGraphProxyAdminInterfaceFix.md b/docs/IGraphProxyAdminInterfaceFix.md new file mode 100644 index 000000000..f17ea388c --- /dev/null +++ b/docs/IGraphProxyAdminInterfaceFix.md @@ -0,0 +1,201 @@ +# IGraphProxyAdmin Interface Signature Fix + +## Issue + +The IGraphProxyAdmin interface in `packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol` had incorrect function signatures that didn't match the actual GraphProxyAdmin contract implementation. + +### Understanding the Two Different `acceptProxy` Methods + +There are **two different contracts** with similar-sounding methods, which can cause confusion: + +1. **GraphUpgradeable** (base class for implementation contracts): + + ```solidity + // Called ON the implementation contract + function acceptProxy(IGraphProxy _proxy) external onlyProxyAdmin(_proxy) { + _proxy.acceptUpgrade(); + } + ``` + + This is inherited by implementation contracts like RewardsManager, Staking, etc. + +2. **GraphProxyAdmin** (admin contract that manages upgrades): + + ```solidity + // Called ON the admin contract, which then calls the implementation + function acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) external onlyGovernor { + _implementation.acceptProxy(_proxy); + } + ``` + + This is the admin contract that orchestrates upgrades. + +**IGraphProxyAdmin represents the second one** - the GraphProxyAdmin admin contract, not the GraphUpgradeable base class. + +### Incorrect Interface (Before) + +The interface mistakenly used the single-parameter signature from GraphUpgradeable: + +```solidity +function acceptProxy(IGraphProxy proxy) external; + +function acceptProxyAndCall(IGraphProxy proxy, bytes calldata data) external; +``` + +### Actual GraphProxyAdmin Implementation + +From `packages/contracts/contracts/upgrades/GraphProxyAdmin.sol`: + +```solidity +function acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) external onlyGovernor { + _implementation.acceptProxy(_proxy); +} + +function acceptProxyAndCall( + GraphUpgradeable _implementation, + IGraphProxy _proxy, + bytes calldata _data +) external onlyGovernor { + _implementation.acceptProxyAndCall(_proxy, _data); +} +``` + +The interface was **missing the first parameter** (`implementation` address) from both functions. It had copied the signature from GraphUpgradeable instead of using the correct GraphProxyAdmin signature. + +## Impact + +### Why This Mattered + +The deployment package (`@graphprotocol/deployment`) needs to call `acceptProxy` with the correct signature to upgrade proxy contracts. The function requires TWO parameters: + +1. The implementation contract address +2. The proxy contract address + +Because the interface was wrong, the deployment code had to work around it by loading the full contract ABI instead of using the cleaner interface ABI: + +```typescript +// packages/deployment/lib/abis.ts (old workaround) +// Note: Load from actual contract, not interface, because IGraphProxyAdmin is outdated +// Interface shows: acceptProxy(IGraphProxy proxy) +// Contract has: acceptProxy(GraphUpgradeable _implementation, IGraphProxy _proxy) +export const GRAPH_PROXY_ADMIN_ABI = loadAbi( + '@graphprotocol/contracts/artifacts/contracts/upgrades/GraphProxyAdmin.sol/GraphProxyAdmin.json', +) +``` + +### Why Horizon is Not Affected + +GraphDirectory in horizon (`packages/horizon/contracts/utilities/GraphDirectory.sol`) imports and uses IGraphProxyAdmin, but **only as a type reference**: + +```solidity +IGraphProxyAdmin private immutable GRAPH_PROXY_ADMIN; + +constructor(address controller) { + GRAPH_PROXY_ADMIN = IGraphProxyAdmin(_getContractFromController("GraphProxyAdmin")); +} + +function _graphProxyAdmin() internal view returns (IGraphProxyAdmin) { + return GRAPH_PROXY_ADMIN; +} +``` + +GraphDirectory: + +- Stores the address as an immutable reference +- Returns it via a getter function +- **Never calls any methods on IGraphProxyAdmin** (like `acceptProxy`) + +Since horizon doesn't call the methods, fixing the interface signature doesn't break horizon. + +## Fix Applied + +### Updated Interface + +```solidity +/** + * @notice Accept ownership of a proxy contract + * @param implementation The implementation contract accepting the proxy + * @param proxy The proxy contract to accept + */ +function acceptProxy(address implementation, IGraphProxy proxy) external; + +/** + * @notice Accept ownership of a proxy contract and call a function + * @param implementation The implementation contract accepting the proxy + * @param proxy The proxy contract to accept + * @param data The calldata to execute after accepting + */ +function acceptProxyAndCall(address implementation, IGraphProxy proxy, bytes calldata data) external; +``` + +**Notes on parameter type choice:** + +- Used `address` instead of `GraphUpgradeable` for the implementation parameter +- This avoids creating a dependency from interfaces package to contracts package +- The actual contract uses `GraphUpgradeable`, but `address` is compatible (Solidity allows passing addresses for contract types) +- The ABI encoding is identical - both produce the same function selector and parameter encoding + +**Call flow for context:** + +``` +Deployer/Governor + → GraphProxyAdmin.acceptProxy(implAddress, proxyAddress) ← IGraphProxyAdmin represents THIS + → implAddress.acceptProxy(proxyAddress) ← GraphUpgradeable provides this + → proxyAddress.acceptUpgrade() +``` + +### Updated Deployment Code + +Removed the workaround comment and switched to using the interface: + +```typescript +// packages/deployment/lib/abis.ts (now clean) +export const GRAPH_PROXY_ADMIN_ABI = loadAbi( + '@graphprotocol/interfaces/artifacts/contracts/contracts/upgrades/IGraphProxyAdmin.sol/IGraphProxyAdmin.json', +) +``` + +## Files Changed + +1. `packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol` + - Fixed `acceptProxy` signature + - Fixed `acceptProxyAndCall` signature + +2. `packages/deployment/lib/abis.ts` + - Removed workaround comment + - Changed to load from interface instead of full contract + +## Testing + +Build verification: + +- ✅ interfaces package builds successfully +- ✅ deployment package dependencies build successfully +- ✅ No TypeScript compilation errors +- ✅ Hardhat compilation successful + +The deployment code in `packages/deployment/lib/upgrade-implementation.ts` already calls acceptProxy with both parameters: + +```typescript +const acceptData = encodeFunctionData({ + abi: GRAPH_PROXY_ADMIN_ABI, + functionName: 'acceptProxy', + args: [pendingImpl as `0x${string}`, proxyAddress as `0x${string}`], +}) +``` + +This call now works with the corrected interface ABI. + +## Recommendation + +This fix should be safe to merge. The interface now accurately reflects the actual contract implementation, and no existing code is broken by the change since: + +1. Deployment already expects the two-parameter signature +2. Horizon only uses the type, never calls the methods +3. The fix aligns the interface with reality, reducing confusion + +## Questions for Team Review + +1. Are there other consumers of IGraphProxyAdmin that might be affected? +2. Should this be considered a breaking change requiring a major version bump of @graphprotocol/interfaces? +3. Is there a reason the interface was historically wrong (legacy compatibility concerns)? diff --git a/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol b/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol index 7c6cfad6c..77509fe9d 100644 --- a/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol +++ b/packages/interfaces/contracts/contracts/upgrades/IGraphProxyAdmin.sol @@ -66,16 +66,18 @@ interface IGraphProxyAdmin is IGoverned { /** * @notice Accept ownership of a proxy contract + * @param implementation The implementation contract accepting the proxy * @param proxy The proxy contract to accept */ - function acceptProxy(IGraphProxy proxy) external; + function acceptProxy(address implementation, IGraphProxy proxy) external; /** * @notice Accept ownership of a proxy contract and call a function + * @param implementation The implementation contract accepting the proxy * @param proxy The proxy contract to accept * @param data The calldata to execute after accepting */ - function acceptProxyAndCall(IGraphProxy proxy, bytes calldata data) external; + function acceptProxyAndCall(address implementation, IGraphProxy proxy, bytes calldata data) external; // storage From 9f1ddb44bad904960857eb7f864d597b3403c462 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:33:25 +0000 Subject: [PATCH 16/43] refactor: rename RewardsReason to RewardsCondition Rename clarifies semantic meaning: these are conditions under which rewards behavior changes (staleness, denial, etc.), not reasons explaining why something happened. The term "condition" better reflects that these are states checked during reward collection that determine how rewards are handled. --- .../contracts/rewards/RewardsManager.sol | 6 ++-- .../contracts/rewards/IRewardsManager.sol | 4 +-- ...ewardsReclaim.sol => RewardsCondition.sol} | 34 +++++++++++-------- .../contracts/utilities/AllocationManager.sol | 12 +++---- 4 files changed, 31 insertions(+), 25 deletions(-) rename packages/interfaces/contracts/contracts/rewards/{RewardsReclaim.sol => RewardsCondition.sol} (62%) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 10c29d561..c45d493c0 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -18,7 +18,7 @@ import { IRewardsManagerDeprecated } from "@graphprotocol/interfaces/contracts/c import { IIssuanceAllocationDistribution } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceAllocationDistribution.sol"; import { IIssuanceTarget } from "@graphprotocol/interfaces/contracts/issuance/allocate/IIssuanceTarget.sol"; import { IRewardsEligibility } from "@graphprotocol/interfaces/contracts/issuance/eligibility/IRewardsEligibility.sol"; -import { RewardsReclaim } from "@graphprotocol/interfaces/contracts/contracts/rewards/RewardsReclaim.sol"; +import { RewardsCondition } from "@graphprotocol/interfaces/contracts/contracts/rewards/RewardsCondition.sol"; /** * @title Rewards Manager Contract @@ -572,7 +572,7 @@ contract RewardsManager is if ( 0 < _reclaimRewards( - RewardsReclaim.SUBGRAPH_DENIED, + RewardsCondition.SUBGRAPH_DENIED, rewards, indexer, allocationID, @@ -590,7 +590,7 @@ contract RewardsManager is if ( 0 < _reclaimRewards( - RewardsReclaim.INDEXER_INELIGIBLE, + RewardsCondition.INDEXER_INELIGIBLE, rewards, indexer, allocationID, diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index 26ac8c090..e5f930363 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -132,7 +132,7 @@ interface IRewardsManager { * previous periods will be sent to the new reclaim address when they are eventually reclaimed, * regardless of which address was configured when the rewards were originally accrued. * - * @param reason The reclaim reason identifier (see RewardsReclaim library for canonical reasons) + * @param reason The reclaim reason identifier (see RewardsCondition library for canonical reasons) * @param newReclaimAddress The address to receive tokens */ function setReclaimAddress(bytes32 reason, address newReclaimAddress) external; @@ -261,7 +261,7 @@ interface IRewardsManager { * @notice Reclaim rewards for an allocation * @dev This function can only be called by an authorized rewards issuer. * Calculates pending rewards and mints them to the configured reclaim address. - * @param reason The reclaim reason identifier (see RewardsReclaim library for canonical reasons) + * @param reason The reclaim reason identifier (see RewardsCondition library for canonical reasons) * @param allocationID Allocation * @param data Arbitrary data to include in the RewardsReclaimed event for additional context * @return The amount of rewards that were reclaimed (0 if no reclaim address set) diff --git a/packages/interfaces/contracts/contracts/rewards/RewardsReclaim.sol b/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol similarity index 62% rename from packages/interfaces/contracts/contracts/rewards/RewardsReclaim.sol rename to packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol index dab4eed71..db5b4551c 100644 --- a/packages/interfaces/contracts/contracts/rewards/RewardsReclaim.sol +++ b/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol @@ -3,60 +3,66 @@ pragma solidity ^0.7.6 || ^0.8.0; /** - * @title RewardsReclaim + * @title RewardsCondition * @author Edge & Node - * @notice Canonical definitions for rewards reclaim reasons + * @notice Canonical definitions for reward condition reasons * @dev Uses bytes32 identifiers (like OpenZeppelin roles) to allow decentralized extension. * New reasons can be defined by any contract without modifying this library. * These constants provide standard reasons used across The Graph Protocol. * - * Note: bytes32(0) is reserved and cannot be used as a reclaim reason. This design prevents: + * Note: bytes32(0) is reserved as NONE and cannot be used as a reclaim reason. This design prevents: * 1. Accidental misconfiguration from setting a reclaim address for an invalid/uninitialized reason - * 2. Invalid reclaim operations when a reason identifier was not properly set + * 2. Invalid reclaim operations when a condition identifier was not properly set * The zero value serves as a sentinel to catch configuration errors at the protocol level. * - * How reclaim reasons are used depends on the specific implementation. Different contracts - * may handle multiple applicable reclaim reasons differently. + * How condition reasons are used depends on the specific implementation. Different contracts + * may handle multiple applicable conditions differently. */ -library RewardsReclaim { +library RewardsCondition { /** - * @notice Reclaim rewards - indexer failed eligibility check + * @notice No condition - rewards can be claimed normally + * @dev Used as the default/initial state when no blocking condition applies + */ + bytes32 public constant NONE = bytes32(0); + + /** + * @notice Condition - indexer failed eligibility check * @dev Indexer is not eligible to receive rewards according to eligibility oracle */ bytes32 public constant INDEXER_INELIGIBLE = keccak256("INDEXER_INELIGIBLE"); /** - * @notice Reclaim rewards - subgraph is on denylist + * @notice Condition - subgraph is on denylist * @dev Subgraph deployment has been denied rewards by availability oracle */ bytes32 public constant SUBGRAPH_DENIED = keccak256("SUBGRAPH_DENIED"); /** - * @notice Reclaim rewards - POI submitted too late + * @notice Condition - POI submitted too late * @dev Proof of Indexing was submitted after the staleness deadline */ bytes32 public constant STALE_POI = keccak256("STALE_POI"); /** - * @notice Reclaim rewards - allocation has no tokens + * @notice Condition - allocation has no tokens * @dev Altruistic allocation (zero tokens) is not eligible for rewards */ bytes32 public constant ALTRUISTIC_ALLOCATION = keccak256("ALTRUISTIC_ALLOCATION"); /** - * @notice Reclaim rewards - no POI provided + * @notice Condition - no POI provided * @dev Allocation closed without providing a Proof of Indexing */ bytes32 public constant ZERO_POI = keccak256("ZERO_POI"); /** - * @notice Reclaim rewards - allocation created in current epoch + * @notice Condition - allocation created in current epoch * @dev Allocation must exist for at least one full epoch to earn rewards */ bytes32 public constant ALLOCATION_TOO_YOUNG = keccak256("ALLOCATION_TOO_YOUNG"); /** - * @notice Reclaim rewards - allocation closed without POI + * @notice Condition - allocation closed without POI * @dev Allocation was closed without providing a Proof of Indexing */ bytes32 public constant CLOSE_ALLOCATION = keccak256("CLOSE_ALLOCATION"); diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 0bd665b3b..4682e8725 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -7,7 +7,7 @@ import { IHorizonStakingTypes } from "@graphprotocol/interfaces/contracts/horizo import { IAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocation.sol"; import { IAllocationManager } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/IAllocationManager.sol"; import { ILegacyAllocation } from "@graphprotocol/interfaces/contracts/subgraph-service/internal/ILegacyAllocation.sol"; -import { RewardsReclaim } from "@graphprotocol/interfaces/contracts/contracts/rewards/RewardsReclaim.sol"; +import { RewardsCondition } from "@graphprotocol/interfaces/contracts/contracts/rewards/RewardsCondition.sol"; import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol"; import { AllocationManagerV1Storage } from "./AllocationManagerStorage.sol"; @@ -186,14 +186,14 @@ abstract contract AllocationManager is // Mint indexing rewards if all conditions are met, otherwise reclaim with specific reason uint256 tokensRewards; if (allocation.isStale(maxPOIStaleness)) { - _graphRewardsManager().reclaimRewards(RewardsReclaim.STALE_POI, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.STALE_POI, _allocationId, ""); } else if (allocation.isAltruistic()) { - _graphRewardsManager().reclaimRewards(RewardsReclaim.ALTRUISTIC_ALLOCATION, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.ALTRUISTIC_ALLOCATION, _allocationId, ""); } else if (_poi == bytes32(0)) { - _graphRewardsManager().reclaimRewards(RewardsReclaim.ZERO_POI, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.ZERO_POI, _allocationId, ""); // solhint-disable-next-line gas-strict-inequalities } else if (_graphEpochManager().currentEpoch() <= allocation.createdAtEpoch) { - _graphRewardsManager().reclaimRewards(RewardsReclaim.ALLOCATION_TOO_YOUNG, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.ALLOCATION_TOO_YOUNG, _allocationId, ""); } else { tokensRewards = _graphRewardsManager().takeRewards(_allocationId); } @@ -333,7 +333,7 @@ abstract contract AllocationManager is // Reclaim uncollected rewards before closing uint256 reclaimedRewards = _graphRewardsManager().reclaimRewards( - RewardsReclaim.CLOSE_ALLOCATION, + RewardsCondition.CLOSE_ALLOCATION, _allocationId, "" ); From 84814443ab876e728e7a1629536063dbc50ddda2 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:30:24 +0000 Subject: [PATCH 17/43] feat: extend RewardsCondition Add NO_SIGNAL, BELOW_MINIMUM_SIGNAL, NO_ALLOCATION conditions. Enables granular reclaim routing for edge cases where rewards cannot be distributed to indexers. --- .../contracts/rewards/RewardsCondition.sol | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol b/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol index db5b4551c..cb53814fb 100644 --- a/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol +++ b/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol @@ -66,4 +66,22 @@ library RewardsCondition { * @dev Allocation was closed without providing a Proof of Indexing */ bytes32 public constant CLOSE_ALLOCATION = keccak256("CLOSE_ALLOCATION"); + + /** + * @notice Condition - no curation signal exists + * @dev Total signalled tokens is zero, so rewards cannot be distributed + */ + bytes32 public constant NO_SIGNAL = keccak256("NO_SIGNAL"); + + /** + * @notice Condition - subgraph signal below minimum threshold + * @dev Subgraph has curation signal but it's below the minimumSubgraphSignal threshold + */ + bytes32 public constant BELOW_MINIMUM_SIGNAL = keccak256("BELOW_MINIMUM_SIGNAL"); + + /** + * @notice Condition - no allocations exist for subgraph + * @dev Subgraph has no indexer allocations, so rewards cannot be distributed for this subgraph + */ + bytes32 public constant NO_ALLOCATION = keccak256("NO_ALLOCATION"); } From 354a11f91b20e37c5399642500a01647201a550f Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:48:45 +0000 Subject: [PATCH 18/43] feat: add defaultReclaimAddress Fallback address when reason-specific reclaim address not set. Adds setDefaultReclaimAddress (governor) and getDefaultReclaimAddress. --- .../contracts/rewards/RewardsManager.sol | 19 +++++++++++++++++ .../rewards/RewardsManagerStorage.sol | 1 + .../contracts/rewards/IRewardsManager.sol | 21 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index c45d493c0..b7e39d948 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -237,6 +237,18 @@ contract RewardsManager is } } + /** + * @inheritdoc IRewardsManager + */ + function setDefaultReclaimAddress(address newAddress) external override onlyGovernor { + address oldAddress = defaultReclaimAddress; + + if (oldAddress != newAddress) { + defaultReclaimAddress = newAddress; + emit DefaultReclaimAddressSet(oldAddress, newAddress); + } + } + // -- Denylist -- /** @@ -290,6 +302,13 @@ contract RewardsManager is return reclaimAddresses[reason]; } + /** + * @inheritdoc IRewardsManager + */ + function getDefaultReclaimAddress() external view override returns (address) { + return defaultReclaimAddress; + } + /** * @inheritdoc IRewardsManager */ diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index 0de74ac07..03dc462f5 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -94,4 +94,5 @@ abstract contract RewardsManagerV6Storage is RewardsManagerV5Storage { /// @dev Mapping of reclaim reason identifiers to reclaim addresses /// @dev Uses bytes32 for extensibility. See RewardsReclaim library for canonical reasons. mapping(bytes32 => address) internal reclaimAddresses; + address internal defaultReclaimAddress; } diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index e5f930363..f25571b0d 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -71,6 +71,13 @@ interface IRewardsManager { */ event ReclaimAddressSet(bytes32 indexed reason, address indexed oldAddress, address indexed newAddress); + /** + * @notice Default reclaim address changed + * @param oldAddress Previous default reclaim address + * @param newAddress New default reclaim address + */ + event DefaultReclaimAddressSet(address indexed oldAddress, address indexed newAddress); + /** * @notice Rewards reclaimed to a configured address * @param reason The reclaim reason identifier @@ -137,6 +144,14 @@ interface IRewardsManager { */ function setReclaimAddress(bytes32 reason, address newReclaimAddress) external; + /** + * @notice Set the default reclaim address used when no reason-specific address is configured + * @dev This is the fallback address used after trying all applicable reason-specific addresses. + * Set to zero to disable (rewards will be dropped if no specific address matches). + * @param newDefaultReclaimAddress The fallback address for reclaims + */ + function setDefaultReclaimAddress(address newDefaultReclaimAddress) external; + // -- Denylist -- /** @@ -181,6 +196,12 @@ interface IRewardsManager { */ function getReclaimAddress(bytes32 reason) external view returns (address); + /** + * @notice Get the default reclaim address + * @return The fallback address for reclaims when no reason-specific address is configured + */ + function getDefaultReclaimAddress() external view returns (address); + /** * @notice Get the rewards eligibility oracle address * @return The rewards eligibility oracle contract From bed8620795da11f596b18cd3e1bd0ab36def2305 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:50:01 +0000 Subject: [PATCH 19/43] doc: IRewardsManager --- .../contracts/rewards/IRewardsManager.sol | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index f25571b0d..43e153324 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -98,6 +98,20 @@ interface IRewardsManager { /** * @dev Stores accumulated rewards and snapshots related to a particular SubgraphDeployment + * + * ## Snapshot Semantics + * + * Snapshots prevent double-counting. After each update, snapshot = current value. + * New rewards = current - snapshot (delta since last update). + * + * ## Claimability + * + * When a subgraph is not claimable (denied or below minimum signal): + * - `accRewardsForSubgraph` FREEZES (no new rewards credited) + * - `accRewardsPerAllocatedToken` FREEZES (allocation-level) + * - New rewards are reclaimed via `onSubgraphAllocationUpdate()` + * - `accRewardsPerSignalSnapshot` still updates to prevent double-counting + * * @param accRewardsForSubgraph Accumulated rewards for the subgraph * @param accRewardsForSubgraphSnapshot Snapshot of accumulated rewards for the subgraph * @param accRewardsPerSignalSnapshot Snapshot of accumulated rewards per signal @@ -304,6 +318,14 @@ interface IRewardsManager { * @notice Triggers an update of rewards for a subgraph * @dev Must be called before allocation on a subgraph changes. * Hook called from the Staking contract on allocate() and close() + * + * ## Denial Behavior + * + * When the subgraph is denied: + * - Does NOT update `accRewardsPerAllocatedToken` (keeps it frozen) + * - Reclaims new rewards accrued since last snapshot (if reclaim address configured) + * - Always updates `accRewardsForSubgraphSnapshot` to prevent double-counting + * * @param subgraphDeploymentID Subgraph deployment * @return Accumulated rewards per allocated token for a subgraph */ From ece40cdbbd2a077ac236094328ef85a7d8327aa4 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:17:04 +0000 Subject: [PATCH 20/43] feat: use AccessControlEnumerable in BaseUpgradeable Switch from AccessControlUpgradeable to AccessControlEnumerableUpgradeable to enable on-chain role enumeration via getRoleMemberCount() and getRoleMember(). This enables deployment verification scripts to enumerate all role holders without relying on event indexing. --- packages/issuance/contracts/common/BaseUpgradeable.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/issuance/contracts/common/BaseUpgradeable.sol b/packages/issuance/contracts/common/BaseUpgradeable.sol index cd5dae620..771d6f0a1 100644 --- a/packages/issuance/contracts/common/BaseUpgradeable.sol +++ b/packages/issuance/contracts/common/BaseUpgradeable.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.33; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; import { IGraphToken } from "./IGraphToken.sol"; import { IPausableControl } from "@graphprotocol/interfaces/contracts/issuance/common/IPausableControl.sol"; From 8ad2f287dae95502f5d2e4a18ec819c7d82c6c8e Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:24:21 +0000 Subject: [PATCH 21/43] feat: feat: split issuance getter (allocated/raw) Renames getRewardsIssuancePerBlock() to getAllocatedIssuancePerBlock() and adds getRawIssuancePerBlock() to expose the raw storage value. This separation enables debugging IssuanceAllocator configuration and verifying allocator adjustments are applied correctly. See docs/RewardsManagerIssuanceSplit.md --- .../contracts/contracts/rewards/RewardsManager.sol | 12 +++++++++--- .../contracts/contracts/rewards/IRewardsManager.sol | 10 +++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index b7e39d948..42e9f1bff 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -279,15 +279,21 @@ contract RewardsManager is /** * @inheritdoc IRewardsManager - * @dev Gets the effective issuance per block, taking into account the IssuanceAllocator if set */ - function getRewardsIssuancePerBlock() public view override returns (uint256) { + function getAllocatedIssuancePerBlock() public view override returns (uint256) { return address(issuanceAllocator) != address(0) ? issuanceAllocator.getTargetIssuancePerBlock(address(this)).selfIssuanceRate : issuancePerBlock; } + /** + * @inheritdoc IRewardsManager + */ + function getRawIssuancePerBlock() external view override returns (uint256) { + return issuancePerBlock; + } + /** * @inheritdoc IRewardsManager */ @@ -334,7 +340,7 @@ contract RewardsManager is return 0; } - uint256 rewardsIssuancePerBlock = getRewardsIssuancePerBlock(); + uint256 rewardsIssuancePerBlock = getAllocatedIssuancePerBlock(); if (rewardsIssuancePerBlock == 0) { return 0; diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index 43e153324..ad8faf83d 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -228,7 +228,15 @@ interface IRewardsManager { * Otherwise falls back to the raw storage value. * @return The effective issuance per block */ - function getRewardsIssuancePerBlock() external view returns (uint256); + function getAllocatedIssuancePerBlock() external view returns (uint256); + + /** + * @notice Gets the raw issuance per block value from contract storage + * @dev This returns the storage value directly, ignoring the issuance allocator. + * Prefer {getAllocatedIssuancePerBlock} for the effective protocol rate. + * @return The raw issuance per block from storage + */ + function getRawIssuancePerBlock() external view returns (uint256); /** * @notice Gets the issuance of rewards per signal since last updated From f86183fe9e5f7143b34cf835f8361fd2415a957f Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:36:40 +0000 Subject: [PATCH 22/43] feat: add private _getNewRewardsPerSignal to return unclaimable rewards --- .../contracts/rewards/RewardsManager.sol | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 42e9f1bff..ef75791eb 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -330,34 +330,42 @@ contract RewardsManager is * t: time steps are in blocks since last updated * x: newly accrued rewards tokens for the period `t` * - * @return newly accrued rewards per signal since last update, scaled by FIXED_POINT_SCALING_FACTOR + * @return claimablePerSignal accrued rewards per signal since last update, scaled by FIXED_POINT_SCALING_FACTOR */ - function getNewRewardsPerSignal() public view override returns (uint256) { + function getNewRewardsPerSignal() public view override returns (uint256 claimablePerSignal) { + (claimablePerSignal, ) = _getNewRewardsPerSignal(); + } + + /** + * @notice Calculate new rewards per signal, split into claimable and unclaimable portions + * @dev Linear formula: `x = r * t` + * + * Notation: + * t: time steps are in blocks since last updated + * x: newly accrued rewards tokens for the period `t` + * + * @return claimablePerSignal Rewards per signal when signal exists, scaled by FIXED_POINT_SCALING_FACTOR + * @return unclaimableTokens Raw token amount that cannot be distributed due to zero signal + */ + function _getNewRewardsPerSignal() private view returns (uint256 claimablePerSignal, uint256 unclaimableTokens) { // Calculate time steps uint256 t = block.number.sub(accRewardsPerSignalLastBlockUpdated); // Optimization to skip calculations if zero time steps elapsed - if (t == 0) { - return 0; - } + if (t == 0) return (0, 0); uint256 rewardsIssuancePerBlock = getAllocatedIssuancePerBlock(); - if (rewardsIssuancePerBlock == 0) { - return 0; - } - - // Zero issuance if no signalled tokens - IGraphToken graphToken = graphToken(); - uint256 signalledTokens = graphToken.balanceOf(address(curation())); - if (signalledTokens == 0) { - return 0; - } + if (rewardsIssuancePerBlock == 0) return (0, 0); uint256 x = rewardsIssuancePerBlock.mul(t); + // Check signalled tokens + uint256 signalledTokens = graphToken().balanceOf(address(curation())); + if (signalledTokens == 0) return (0, x); // All unclaimable when no signal + // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number - return x.mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens); + return (x.mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens), 0); } /// @inheritdoc IRewardsManager From 8a5ba118ffbe19169978713e48c61e1cb3334cd9 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 01:05:02 +0000 Subject: [PATCH 23/43] feat: remove data param from RewardsReclaimed Unused bytes parameter removed from event and _reclaimRewards. --- .../contracts/rewards/RewardsManager.sol | 20 ++++++------------- .../contracts/rewards/IRewardsManager.sol | 7 ++----- .../contracts/utilities/AllocationManager.sol | 11 +++++----- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index ef75791eb..5f8de810d 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -562,7 +562,6 @@ contract RewardsManager is * @param indexer Address of the indexer * @param allocationID Address of the allocation * @param subgraphDeploymentID Subgraph deployment ID for the allocation - * @param data Additional context data for the reclaim * @return reclaimed The amount of rewards that were reclaimed (0 if no reclaim address set) */ function _reclaimRewards( @@ -570,13 +569,12 @@ contract RewardsManager is uint256 rewards, address indexer, address allocationID, - bytes32 subgraphDeploymentID, - bytes memory data + bytes32 subgraphDeploymentID ) private returns (uint256 reclaimed) { address target = reclaimAddresses[reason]; if (0 < rewards && target != address(0)) { graphToken().mint(target, rewards); - emit RewardsReclaimed(reason, rewards, indexer, allocationID, subgraphDeploymentID, data); + emit RewardsReclaimed(reason, rewards, indexer, allocationID, subgraphDeploymentID); reclaimed = rewards; } } @@ -609,8 +607,7 @@ contract RewardsManager is rewards, indexer, allocationID, - subgraphDeploymentID, - "" + subgraphDeploymentID ) ) { return true; // Successfully reclaimed, deny rewards @@ -627,8 +624,7 @@ contract RewardsManager is rewards, indexer, allocationID, - subgraphDeploymentID, - "" + subgraphDeploymentID ) ) { return true; // Successfully reclaimed, deny rewards @@ -673,11 +669,7 @@ contract RewardsManager is * @inheritdoc IRewardsManager * @dev bytes32(0) as a reason is reserved as a no-op and will not be reclaimed. */ - function reclaimRewards( - bytes32 reason, - address allocationID, - bytes calldata data - ) external override returns (uint256) { + function reclaimRewards(bytes32 reason, address allocationID) external override returns (uint256) { address rewardsIssuer = msg.sender; require(rewardsIssuer == address(subgraphService), "Not a rewards issuer"); @@ -686,6 +678,6 @@ contract RewardsManager is allocationID ); - return _reclaimRewards(reason, rewards, indexer, allocationID, subgraphDeploymentID, data); + return _reclaimRewards(reason, rewards, indexer, allocationID, subgraphDeploymentID); } } diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index ad8faf83d..1cf817f2d 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -85,15 +85,13 @@ interface IRewardsManager { * @param indexer Address of the indexer * @param allocationID Address of the allocation * @param subgraphDeploymentID Subgraph deployment ID for the allocation - * @param data Additional context data for the reclaim */ event RewardsReclaimed( bytes32 indexed reason, uint256 amount, address indexed indexer, address indexed allocationID, - bytes32 subgraphDeploymentID, - bytes data + bytes32 subgraphDeploymentID ); /** @@ -306,10 +304,9 @@ interface IRewardsManager { * Calculates pending rewards and mints them to the configured reclaim address. * @param reason The reclaim reason identifier (see RewardsCondition library for canonical reasons) * @param allocationID Allocation - * @param data Arbitrary data to include in the RewardsReclaimed event for additional context * @return The amount of rewards that were reclaimed (0 if no reclaim address set) */ - function reclaimRewards(bytes32 reason, address allocationID, bytes calldata data) external returns (uint256); + function reclaimRewards(bytes32 reason, address allocationID) external returns (uint256); // -- Hooks -- diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 4682e8725..49d9f0b0e 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -186,14 +186,14 @@ abstract contract AllocationManager is // Mint indexing rewards if all conditions are met, otherwise reclaim with specific reason uint256 tokensRewards; if (allocation.isStale(maxPOIStaleness)) { - _graphRewardsManager().reclaimRewards(RewardsCondition.STALE_POI, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.STALE_POI, _allocationId); } else if (allocation.isAltruistic()) { - _graphRewardsManager().reclaimRewards(RewardsCondition.ALTRUISTIC_ALLOCATION, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.ALTRUISTIC_ALLOCATION, _allocationId); } else if (_poi == bytes32(0)) { - _graphRewardsManager().reclaimRewards(RewardsCondition.ZERO_POI, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.ZERO_POI, _allocationId); // solhint-disable-next-line gas-strict-inequalities } else if (_graphEpochManager().currentEpoch() <= allocation.createdAtEpoch) { - _graphRewardsManager().reclaimRewards(RewardsCondition.ALLOCATION_TOO_YOUNG, _allocationId, ""); + _graphRewardsManager().reclaimRewards(RewardsCondition.ALLOCATION_TOO_YOUNG, _allocationId); } else { tokensRewards = _graphRewardsManager().takeRewards(_allocationId); } @@ -334,8 +334,7 @@ abstract contract AllocationManager is // Reclaim uncollected rewards before closing uint256 reclaimedRewards = _graphRewardsManager().reclaimRewards( RewardsCondition.CLOSE_ALLOCATION, - _allocationId, - "" + _allocationId ); // Take rewards snapshot to prevent other allos from counting tokens from this allo From 8787824d6074bc5c79c77a644e65b2336488c739 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:21:29 +0000 Subject: [PATCH 24/43] feat: deny rewards for period that a subgraph deployment is denied Prevents indexers from claiming rewards earned while a subgraph was denied. Pre-denial rewards remain claimable after undeny. Key changes: - _setDenied() calls onSubgraphAllocationUpdate() BEFORE state change - onSubgraphAllocationUpdate() reclaims rewards when denied - accRewardsPerAllocatedToken freezes during denied period See docs/DeniedSubgraphRewardsAnalysis.md --- docs/DeniedSubgraphRewardsAnalysis.md | 372 ++++++++++++++++++ .../contracts/rewards/RewardsManager.sol | 29 +- 2 files changed, 397 insertions(+), 4 deletions(-) create mode 100644 docs/DeniedSubgraphRewardsAnalysis.md diff --git a/docs/DeniedSubgraphRewardsAnalysis.md b/docs/DeniedSubgraphRewardsAnalysis.md new file mode 100644 index 000000000..cfefb1f45 --- /dev/null +++ b/docs/DeniedSubgraphRewardsAnalysis.md @@ -0,0 +1,372 @@ +# Denied Subgraph Rewards - Implementation Analysis + +## Overview + +This document analyzes the implementation that prevents indexers from claiming rewards earned during a subgraph's denied period, while allowing pre-denial rewards to be collected after undeny. + +## Key Changes + +### 1. `_setDenied()` (RewardsManager.sol) + +```solidity +function _setDenied(bytes32 subgraphDeploymentId, bool deny) private { + onSubgraphAllocationUpdate(subgraphDeploymentId); // Snapshot/reclaim BEFORE state change + + bool stateChange = deny == (denylist[subgraphDeploymentId] == 0); + if (stateChange) { + uint256 sinceBlock = deny ? block.number : 0; + denylist[subgraphDeploymentId] = sinceBlock; + emit RewardsDenylistUpdated(subgraphDeploymentId, sinceBlock); + } +} +``` + +Calls `onSubgraphAllocationUpdate()` **before** changing denylist state, ensuring: + +- On deny: snapshots current rewards state while `isDenied()` = false +- On undeny: reclaims remaining denied-period rewards while `isDenied()` = true + +**Idempotency guard:** The `stateChange` check ensures that redundant calls are no-ops: + +- Calling `setDenied(id, true)` when already denied does not update `denylist` or emit `RewardsDenylistUpdated` +- Calling `setDenied(id, false)` when already not denied does not update `denylist` or emit `RewardsDenylistUpdated` +- In both cases, `onSubgraphAllocationUpdate()` is still called (snapshot/reclaim still occurs), but the denylist state itself is unchanged +- This prevents the block number from being overwritten on redundant deny calls (which would lose the original deny timestamp) + +### 2. `onSubgraphAllocationUpdate()` (RewardsManager.sol) + +```solidity +function onSubgraphAllocationUpdate(bytes32 _subgraphDeploymentID) public override returns (uint256) { + Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; + (uint256 accRewardsPerAllocatedToken, uint256 accRewardsForSubgraph) = getAccRewardsPerAllocatedToken( + _subgraphDeploymentID + ); + + if (isDenied(_subgraphDeploymentID)) { + // Reclaim new rewards instead of crediting to allocators + if (subgraph.accRewardsForSubgraphSnapshot < accRewardsForSubgraph) + _reclaimRewards(RewardsCondition.SUBGRAPH_DENIED, + accRewardsForSubgraph - subgraph.accRewardsForSubgraphSnapshot, ...); + } else { + subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + } + subgraph.accRewardsForSubgraphSnapshot = accRewardsForSubgraph; + return subgraph.accRewardsPerAllocatedToken; +} +``` + +When denied: + +- Reclaims `accRewardsForSubgraph - accRewardsForSubgraphSnapshot` (actual GRT amount) +- Does NOT update `accRewardsPerAllocatedToken` (keeps it frozen) +- Updates `accRewardsForSubgraphSnapshot` to prevent double-counting + +When not denied: + +- Normal operation: updates both `accRewardsPerAllocatedToken` and snapshot + +--- + +## State Flow Scenarios + +### Scenario 1: Subgraph Gets Denied + +``` +T1: Normal operation + - accRewardsPerAllocatedToken = 100 + - accRewardsForSubgraphSnapshot = 1000 + +T2: setDenied(subgraph, true) called + - onSubgraphAllocationUpdate() called with isDenied() = FALSE + - Normal path: accRewardsPerAllocatedToken updated to 150 + - Snapshot updated + - THEN denylist set +``` + +### Scenario 2: Allocation Operations While Denied + +``` +T3: Any allocation operation (allocate/close/resize/collect) + - onSubgraphAllocationUpdate() called with isDenied() = TRUE + - New rewards (accRewardsForSubgraph - snapshot) are RECLAIMED + - accRewardsPerAllocatedToken stays FROZEN at 150 + - Snapshot updated (prevents double-counting) +``` + +### Scenario 3: Subgraph Gets Undenied + +``` +T4: setDenied(subgraph, false) called + - onSubgraphAllocationUpdate() called with isDenied() = TRUE + - Remaining denied-period rewards are RECLAIMED + - THEN denylist cleared + +T5: Next allocation operation + - isDenied() = FALSE + - Normal operation resumes from frozen state +``` + +### Scenario 4: Reward Collection + +``` +While denied: + - takeRewards() → _calcAllocationRewards() → onSubgraphAllocationUpdate() + - Returns frozen accRewardsPerAllocatedToken + - Allocation rewards = tokens × (frozen_value - allocation_snapshot) + - Only pre-denial rewards are calculated + - Collection may be blocked by soft deny in takeRewards() + +After undeny: + - Normal collection resumes + - Indexers get pre-denial rewards only (frozen value) +``` + +--- + +## Paths That Call `onSubgraphAllocationUpdate()` + +| Path | Location | Description | +| --------------------------- | ------------------------------- | ------------------------------------ | +| `_allocate()` | AllocationManager.sol:231 | Creating new allocation | +| `_closeAllocation()` | AllocationManager.sol:302, 439 | Closing allocation (normal or force) | +| `_resizeAllocation()` | AllocationManager.sol:389 | Resizing allocation | +| `_collectIndexingRewards()` | Via takeRewards() | Collecting rewards | +| `_setDenied()` | RewardsManager.sol:330 | Deny/undeny subgraph | +| `_calcAllocationRewards()` | RewardsManager.sol:593 | Calculating rewards | +| Legacy Staking | Staking.sol:897 | Legacy allocation operations | +| Legacy Staking | HorizonStakingExtension.sol:288 | Legacy extension | + +## Paths That Call `onSubgraphSignalUpdate()` + +| Path | Location | Description | +| ----------------------- | ------------------------------- | ---------------------- | +| Curation mint/burn | Curation.sol:435 | Signal changes | +| L2 Curation | L2Curation.sol:485 | L2 signal changes | +| Query fees collection | SubgraphService.sol:547 | Curation fees | +| Legacy close allocation | Staking.sol:862 | Curation fees on close | +| Legacy extension | HorizonStakingExtension.sol:458 | Curation fees | + +**Note:** `onSubgraphSignalUpdate()` updates `accRewardsForSubgraph` and `accRewardsPerSignalSnapshot`, which is separate from allocation reward accounting. + +## Operations That Do NOT Affect Allocation Rewards + +| Operation | Reason | +| ---------------------- | ----------------------------------------------------- | +| Slashing | Only affects stake on HorizonStaking, not allocations | +| Delegation | Separate from allocation rewards | +| Stake deposit/withdraw | Separate from allocation accounting | + +--- + +## Accounting Invariants + +1. **While denied:** + - `accRewardsPerAllocatedToken` is frozen + - New rewards are reclaimed via `_reclaimRewards()` + - `accRewardsForSubgraphSnapshot` is updated to prevent double-counting + +2. **Allocation snapshots:** + - Each allocation stores its `accRewardsPerAllocatedToken` at creation + - Rewards = `tokens × (current - snapshot) / SCALING_FACTOR` + - When denied, `current` = frozen value, so only pre-denial rewards + +3. **No double-counting:** + - Each `onSubgraphAllocationUpdate()` call updates snapshot + - Reclaim amount = `accRewardsForSubgraph - accRewardsForSubgraphSnapshot` + - After reclaim, snapshot = current, so next call starts fresh + +4. **No bypasses:** + - All allocation-affecting operations go through `onSubgraphAllocationUpdate()` + - Signal changes (`onSubgraphSignalUpdate`) are separate accounting + +--- + +## Edge Cases + +### All allocations close while denied + +- `getAccRewardsPerAllocatedToken()` returns 0 when no allocated tokens +- But we don't update `accRewardsPerAllocatedToken` when denied +- Frozen value is preserved for new allocations after undeny + +### Allocation created while denied + +- Gets snapshot = frozen `accRewardsPerAllocatedToken` +- After undeny, rewards = (new value - frozen value) +- Only gets post-undeny rewards ✓ + +### Multiple reclaims while denied + +- Each call reclaims only NEW rewards since last call +- Snapshot updated after each reclaim +- No double-counting ✓ + +### Redundant deny/undeny calls (idempotency) + +- `setDenied(id, true)` when already denied: no state change, no event emitted +- `setDenied(id, false)` when not denied: no state change, no event emitted +- `onSubgraphAllocationUpdate()` is still called in both cases (side effect on reward snapshots/reclaims) +- Original deny block number is preserved on redundant deny calls ✓ + +### Zero rewards to reclaim + +- Condition: `subgraph.accRewardsForSubgraphSnapshot < accRewardsForSubgraph` +- If equal, no reclaim (nothing to reclaim) +- Prevents zero-amount reclaims ✓ + +--- + +## Soft Deny in AllocationManager + +### Location + +`AllocationManager.sol:_presentPOI()` (line 283) + +### Code + +```solidity +bool canClaimNow = allocation.createdAtEpoch < _graphEpochManager().currentEpoch() + && !_graphRewardsManager().isDenied(allocation.subgraphDeploymentId); +``` + +### Behavior When Denied + +When `isDenied() = true`, `canClaimNow = false`, which causes: + +1. **`takeRewards()` is NOT called** (line 288-289 skipped) + - No rewards are minted to indexer + - No rewards are reclaimed via takeRewards path + +2. **Stale/Zero POI still reclaimed** (lines 284-287) + + ```solidity + if (allocation.isStale(maxPOIStaleness)) { + _graphRewardsManager().reclaimRewards(RewardsCondition.STALE_POI, _allocationId, ""); + } else if (_poi == bytes32(0)) { + _graphRewardsManager().reclaimRewards(RewardsCondition.ZERO_POI, _allocationId, ""); + } + ``` + + - These reclaims go through `_calcAllocationRewards()` → `onSubgraphAllocationUpdate()` + - While denied, `onSubgraphAllocationUpdate()` reclaims subgraph-level rewards + +3. **Early return at line 297** + + ```solidity + if (!canClaimNow) return 0; + ``` + + - Does NOT snapshot allocation rewards + - Does NOT clear pending rewards + - Allocation state preserved for future collection + +4. **POI is still recorded** (line 294) + + ```solidity + _allocations.presentPOI(_allocationId); + ``` + + - Prevents allocation from becoming stale + - Indexer can keep presenting POIs while denied + +### Flow Summary + +``` +_presentPOI() called while denied: +├── Stale POI? → reclaimRewards(STALE_POI) → onSubgraphAllocationUpdate() reclaims +├── Zero POI? → reclaimRewards(ZERO_POI) → onSubgraphAllocationUpdate() reclaims +├── Valid POI? → canClaimNow = false, skip takeRewards() +├── Record POI (prevents staleness) +└── Return 0 (no snapshot, no clear pending) +``` + +### Why This Works + +1. **Pre-denial rewards preserved:** + - `accRewardsPerAllocatedToken` frozen in RewardsManager + - Allocation's pending rewards not cleared + - After undeny, indexer can collect pre-denial rewards + +2. **Denied-period rewards reclaimed:** + - `onSubgraphAllocationUpdate()` reclaims subgraph-level rewards + - Called via reclaimRewards() even when valid POI + - Called via setDenied() on deny/undeny transitions + +3. **Indexer keeps allocation healthy:** + - Can present POIs to avoid staleness + - Doesn't forfeit allocation by not presenting + +### Interaction with RewardsManager + +| AllocationManager Action | RewardsManager Behavior | +| ---------------------------- | ----------------------------------------------------------------- | +| `takeRewards()` skipped | No rewards minted | +| `reclaimRewards(STALE/ZERO)` | Calls `_calcAllocationRewards()` → `onSubgraphAllocationUpdate()` | +| POI recorded | No RewardsManager interaction | +| Early return | No snapshot update in allocation | + +--- + +## Known Limitations and Design Considerations + +### 1. takeRewards() Snapshot Control + +The current implementation reclaims rewards in `onSubgraphAllocationUpdate()` when a subgraph is denied. However, the calling issuer (e.g., SubgraphService) does not have control over when this reclaim happens - it occurs automatically on any allocation update. + +**Alternative approach considered:** RewardsManager could skip reclaiming for denied subgraphs and leave the decision to the calling issuer. This would give issuers more flexibility in how they handle denied-period rewards. + +**Current behavior:** RewardsManager automatically reclaims denied-period rewards on every allocation update. This ensures rewards are reclaimed promptly but removes issuer control over the timing. + +### 2. Soft Deny vs Hard Deny + +The implementation uses two layers: + +1. **Hard deny (RewardsManager):** Freezes `accRewardsPerAllocatedToken` and reclaims new rewards +2. **Soft deny (AllocationManager):** Skips `takeRewards()` when denied, allowing pre-denial rewards to be preserved + +These layers work together but could potentially diverge if other issuers implement different soft deny behaviors. + +### 3. Legacy `_deniedRewards()` Flow + +The `isDenied()` check in `_deniedRewards()` is now effectively unused for new SubgraphService allocations since `AllocationManager._presentPOI()` implements soft deny by skipping `takeRewards()` entirely. However, it's still invoked for legacy staking (`Staking.sol`) allocations. + +**Potential cleanup:** The `isDenied()` branch in `_deniedRewards()` could be removed if legacy staking support is deprecated, as the new SubgraphService handles denied subgraphs via AllocationManager soft deny. + +### 4. Race Conditions on Deny/Undeny + +If `setDenied()` is called while an allocation operation is in flight: + +- The `onSubgraphAllocationUpdate()` call in `_setDenied()` runs first (before state change) +- This should correctly snapshot/reclaim rewards before the state transition +- However, tight timing with concurrent allocation operations could lead to edge cases + +--- + +## Test Cases Needed + +The following test scenarios should be verified: + +1. **Basic deny/undeny cycle:** + - Allocation earns rewards → subgraph denied → allocation can collect pre-denial rewards after undeny + +2. **Rewards during denied period:** + - Subgraph denied → time passes → rewards reclaimed (not available to indexers) + +3. **Multiple reclaims while denied:** + - Multiple allocation operations while denied → each reclaims only new rewards since last operation + +4. **Allocation created while denied:** + - Subgraph denied → new allocation created → subgraph undenied → allocation only earns post-undeny rewards + +5. **All allocations close while denied:** + - All allocations close while denied → frozen `accRewardsPerAllocatedToken` preserved → new allocation after undeny works correctly + +6. **Zero rewards scenario:** + - No new rewards since last update → no reclaim operation (condition check prevents zero-amount reclaims) + +7. **POI presentation while denied:** + - Indexer presents valid POI while denied → returns 0, no snapshot update, allocation state preserved + +8. **Idempotent deny/undeny (no-op cases):** + - Deny already-denied subgraph → no event emitted, denylist block number preserved + - Undeny already-not-denied subgraph → no event emitted, denylist remains zero diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 5f8de810d..7ea0d8865 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -261,13 +261,21 @@ contract RewardsManager is /** * @notice Internal: Denies to claim rewards for a subgraph. + * @dev Idempotent: redundant calls (deny when already denied, undeny when already allowed) + * skip the denylist update and event emission (but still call `onSubgraphAllocationUpdate`). + * This preserves the original deny block number on repeated deny calls. * @param subgraphDeploymentId Subgraph deployment ID * @param deny Whether to set the subgraph as denied for claiming rewards or not */ function _setDenied(bytes32 subgraphDeploymentId, bool deny) private { - uint256 sinceBlock = deny ? block.number : 0; - denylist[subgraphDeploymentId] = sinceBlock; - emit RewardsDenylistUpdated(subgraphDeploymentId, sinceBlock); + onSubgraphAllocationUpdate(subgraphDeploymentId); + + bool stateChange = deny == (denylist[subgraphDeploymentId] == 0); + if (stateChange) { + uint256 sinceBlock = deny ? block.number : 0; + denylist[subgraphDeploymentId] = sinceBlock; + emit RewardsDenylistUpdated(subgraphDeploymentId, sinceBlock); + } } /// @inheritdoc IRewardsManager @@ -463,7 +471,20 @@ contract RewardsManager is (uint256 accRewardsPerAllocatedToken, uint256 accRewardsForSubgraph) = getAccRewardsPerAllocatedToken( _subgraphDeploymentID ); - subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + + if (isDenied(_subgraphDeploymentID)) { + if (subgraph.accRewardsForSubgraphSnapshot < accRewardsForSubgraph) + _reclaimRewards( + RewardsCondition.SUBGRAPH_DENIED, + accRewardsForSubgraph - subgraph.accRewardsForSubgraphSnapshot, + address(0), + address(0), + _subgraphDeploymentID, + "" + ); + } else { + subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + } subgraph.accRewardsForSubgraphSnapshot = accRewardsForSubgraph; return subgraph.accRewardsPerAllocatedToken; } From d99bf7bfd2ffcceb416bd408312944734d6f3eb5 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:55:56 +0000 Subject: [PATCH 25/43] refactor: simplify AllocationManager reward collection and add denial handling Major refactor of _presentPoi() for clarity and denial support: - Extract _distributeIndexingRewards() to separate distribution concerns - Single condition determination block instead of scattered if-else - POIPresented event now emits condition for off-chain visibility - Remove unnecessary ALTRUISTIC_ALLOCATION reclaim - Add SUBGRAPH_DENIED condition handling See docs/AllocationManagerRewardCollectionRefactor.md --- .../contracts/utilities/AllocationManager.sol | 192 ++++++++++-------- 1 file changed, 102 insertions(+), 90 deletions(-) diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 49d9f0b0e..9f0434581 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -132,45 +132,29 @@ abstract contract AllocationManager is /** * @notice Present a POI to collect indexing rewards for an allocation - * This function will mint indexing rewards using the {RewardsManager} and distribute them to the indexer and delegators. + * Mints indexing rewards using the {RewardsManager} and distributes them to the indexer and delegators. * - * Conditions to qualify for indexing rewards: + * Requirements for indexing rewards: * - POI must be non-zero - * - POI must not be stale, i.e: older than `maxPOIStaleness` - * - allocation must not be altruistic (allocated tokens = 0) - * - allocation must be open for at least one epoch + * - POI must not be stale (older than `maxPOIStaleness`) + * - Allocation must be open for at least one epoch (returns early with 0 if too young) * - * Note that indexers are required to periodically (at most every `maxPOIStaleness`) present POIs to collect rewards. - * Rewards will not be issued to stale POIs, which means that indexers are advised to present a zero POI if they are - * unable to present a valid one to prevent being locked out of future rewards. + * When rewards cannot be claimed, they are reclaimed with reason STALE_POI or ZERO_POI. + * Altruistic allocations and too-young allocations skip reclaim (nothing to reclaim / allow claiming later). * - * Note on allocation duration restriction: this is required to ensure that non protocol chains have a valid block number for - * which to calculate POIs. EBO posts once per epoch typically at each epoch change, so we restrict rewards to allocations - * that have gone through at least one epoch change. + * Note: Indexers should present POIs at least every `maxPOIStaleness` to avoid being locked out of rewards. + * A zero POI can be presented if a valid one is unavailable, to prevent staleness and slashing. * - * Reclaim target hierarchy: - * When rewards cannot be minted, they are reclaimed with a specific reason. The following conditions are checked - * in order, and the first matching condition determines which reclaim reason is used: - * 1. STALE_POI - if allocation is stale (lastPOI older than maxPOIStaleness) - * 2. ALTRUISTIC_ALLOCATION - if allocation has zero tokens - * 3. ZERO_POI - if POI is bytes32(0) - * 4. ALLOCATION_TOO_YOUNG - if allocation was created in the current epoch - * Each reason may have a different reclaim address configured in the RewardsManager. If multiple conditions - * apply simultaneously, only the first matching condition's reclaim address receives the rewards. - * - * Retroactive reclaim address changes: - * Any change to a reclaim address in the RewardsManager takes effect immediately and retroactively. - * All unclaimed rewards from previous periods will be sent to the new reclaim address when they are - * eventually reclaimed, regardless of which address was configured when the rewards were originally accrued. + * Note: Reclaim address changes in RewardsManager apply retroactively to all unclaimed rewards. * * Emits a {IndexingRewardsCollected} event. * * @param _allocationId The id of the allocation to collect rewards for * @param _poi The POI being presented - * @param _poiMetadata The metadata associated with the POI. The data and encoding format is for off-chain components to define, this function will only emit the value in an event as-is. + * @param _poiMetadata Metadata associated with the POI, emitted as-is for off-chain components * @param _delegationRatio The delegation ratio to consider when locking tokens * @param _paymentsDestination The address where indexing rewards should be sent - * @return The amount of tokens collected + * @return rewardsCollected Indexing rewards collected */ // solhint-disable-next-line function-max-lines function _presentPoi( @@ -179,85 +163,70 @@ abstract contract AllocationManager is bytes memory _poiMetadata, uint32 _delegationRatio, address _paymentsDestination - ) internal returns (uint256) { + ) internal returns (uint256 rewardsCollected) { IAllocation.State memory allocation = _allocations.get(_allocationId); require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); + _allocations.presentPOI(_allocationId); // Always record POI presentation to prevent staleness + // Scoped for stack management + { + // Determine rewards condition + bytes32 condition = RewardsCondition.NONE; + if (allocation.isStale(maxPOIStaleness)) condition = RewardsCondition.STALE_POI; + else if (_poi == bytes32(0)) + condition = RewardsCondition.ZERO_POI; + // solhint-disable-next-line gas-strict-inequalities + else if (_graphEpochManager().currentEpoch() <= allocation.createdAtEpoch) + condition = RewardsCondition.ALLOCATION_TOO_YOUNG; + else if (_graphRewardsManager().isDenied(allocation.subgraphDeploymentId)) + condition = RewardsCondition.SUBGRAPH_DENIED; + + emit POIPresented( + allocation.indexer, + _allocationId, + allocation.subgraphDeploymentId, + _poi, + _poiMetadata, + condition + ); - // Mint indexing rewards if all conditions are met, otherwise reclaim with specific reason - uint256 tokensRewards; - if (allocation.isStale(maxPOIStaleness)) { - _graphRewardsManager().reclaimRewards(RewardsCondition.STALE_POI, _allocationId); - } else if (allocation.isAltruistic()) { - _graphRewardsManager().reclaimRewards(RewardsCondition.ALTRUISTIC_ALLOCATION, _allocationId); - } else if (_poi == bytes32(0)) { - _graphRewardsManager().reclaimRewards(RewardsCondition.ZERO_POI, _allocationId); - // solhint-disable-next-line gas-strict-inequalities - } else if (_graphEpochManager().currentEpoch() <= allocation.createdAtEpoch) { - _graphRewardsManager().reclaimRewards(RewardsCondition.ALLOCATION_TOO_YOUNG, _allocationId); - } else { - tokensRewards = _graphRewardsManager().takeRewards(_allocationId); + // Early return skips the overallocation check intentionally to avoid loss of uncollected rewards + if (condition == RewardsCondition.ALLOCATION_TOO_YOUNG || condition == RewardsCondition.SUBGRAPH_DENIED) + return 0; + + bool rewardsReclaimable = condition == RewardsCondition.STALE_POI || condition == RewardsCondition.ZERO_POI; + if (rewardsReclaimable) _graphRewardsManager().reclaimRewards(condition, _allocationId); + else rewardsCollected = _graphRewardsManager().takeRewards(_allocationId); } - // ... but we still take a snapshot to ensure the rewards are not accumulated for the next valid POI + // Snapshot rewards to prevent accumulation for next POI, then clear pending _allocations.snapshotRewards( _allocationId, _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId) ); - _allocations.presentPOI(_allocationId); - - // Any pending rewards should have been collected now _allocations.clearPendingRewards(_allocationId); - uint256 tokensIndexerRewards = 0; - uint256 tokensDelegationRewards = 0; - if (tokensRewards != 0) { - // Distribute rewards to delegators - uint256 delegatorCut = _graphStaking().getDelegationFeeCut( - allocation.indexer, - address(this), - IGraphPayments.PaymentTypes.IndexingRewards + // Scoped for stack management + { + (uint256 tokensIndexerRewards, uint256 tokensDelegationRewards) = _distributeIndexingRewards( + allocation, + rewardsCollected, + _paymentsDestination ); - IHorizonStakingTypes.DelegationPool memory delegationPool = _graphStaking().getDelegationPool( + + emit IndexingRewardsCollected( allocation.indexer, - address(this) + _allocationId, + allocation.subgraphDeploymentId, + rewardsCollected, + tokensIndexerRewards, + tokensDelegationRewards, + _poi, + _poiMetadata, + _graphEpochManager().currentEpoch() ); - // If delegation pool has no shares then we don't need to distribute rewards to delegators - tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0; - if (tokensDelegationRewards > 0) { - _graphToken().approve(address(_graphStaking()), tokensDelegationRewards); - _graphStaking().addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards); - } - - // Distribute rewards to indexer - tokensIndexerRewards = tokensRewards - tokensDelegationRewards; - if (tokensIndexerRewards > 0) { - if (_paymentsDestination == address(0)) { - _graphToken().approve(address(_graphStaking()), tokensIndexerRewards); - _graphStaking().stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards); - } else { - _graphToken().pushTokens(_paymentsDestination, tokensIndexerRewards); - } - } } - emit IndexingRewardsCollected( - allocation.indexer, - _allocationId, - allocation.subgraphDeploymentId, - tokensRewards, - tokensIndexerRewards, - tokensDelegationRewards, - _poi, - _poiMetadata, - _graphEpochManager().currentEpoch() - ); - - // Check if the indexer is over-allocated and force close the allocation if necessary - if (_isOverAllocated(allocation.indexer, _delegationRatio)) { - _closeAllocation(_allocationId, true); - } - - return tokensRewards; + if (_isOverAllocated(allocation.indexer, _delegationRatio)) _closeAllocation(_allocationId, true); } /** @@ -395,6 +364,49 @@ abstract contract AllocationManager is return !allocationProvisionTracker.check(_graphStaking(), _indexer, _delegationRatio); } + /** + * @notice Distributes indexing rewards to delegators and indexer + * @param _allocation The allocation state + * @param _rewardsCollected Total rewards to distribute + * @param _paymentsDestination Where to send indexer rewards (0 = stake) + * @return tokensIndexerRewards Amount sent to indexer + * @return tokensDelegationRewards Amount sent to delegation pool + */ + function _distributeIndexingRewards( + IAllocation.State memory _allocation, + uint256 _rewardsCollected, + address _paymentsDestination + ) private returns (uint256 tokensIndexerRewards, uint256 tokensDelegationRewards) { + if (_rewardsCollected == 0) return (0, 0); + + // Calculate and distribute delegator share + uint256 delegatorCut = _graphStaking().getDelegationFeeCut( + _allocation.indexer, + address(this), + IGraphPayments.PaymentTypes.IndexingRewards + ); + IHorizonStakingTypes.DelegationPool memory pool = _graphStaking().getDelegationPool( + _allocation.indexer, + address(this) + ); + tokensDelegationRewards = pool.shares > 0 ? _rewardsCollected.mulPPM(delegatorCut) : 0; + if (tokensDelegationRewards > 0) { + _graphToken().approve(address(_graphStaking()), tokensDelegationRewards); + _graphStaking().addToDelegationPool(_allocation.indexer, address(this), tokensDelegationRewards); + } + + // Distribute indexer share + tokensIndexerRewards = _rewardsCollected - tokensDelegationRewards; + if (tokensIndexerRewards > 0) { + if (_paymentsDestination == address(0)) { + _graphToken().approve(address(_graphStaking()), tokensIndexerRewards); + _graphStaking().stakeToProvision(_allocation.indexer, address(this), tokensIndexerRewards); + } else { + _graphToken().pushTokens(_paymentsDestination, tokensIndexerRewards); + } + } + } + /** * @notice Verifies ownership of an allocation id by verifying an EIP712 allocation proof * @dev Requirements: From df0e0b868946ac852007ec41122824c6104b6619 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:52:25 +0000 Subject: [PATCH 26/43] docs: AllocationManager --- .../contracts/utilities/AllocationManager.sol | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index 9f0434581..b51b367ec 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -139,8 +139,32 @@ abstract contract AllocationManager is * - POI must not be stale (older than `maxPOIStaleness`) * - Allocation must be open for at least one epoch (returns early with 0 if too young) * - * When rewards cannot be claimed, they are reclaimed with reason STALE_POI or ZERO_POI. - * Altruistic allocations and too-young allocations skip reclaim (nothing to reclaim / allow claiming later). + * ## Reward Paths + * + * Rewards follow one of three paths based on allocation and POI state: + * + * **CLAIMED** (normal path): Valid POI, not stale, allocation mature, subgraph not denied + * - Calls `takeRewards()` to mint tokens to this contract + * - Distributes to indexer (stake or payments destination) and delegators + * - Snapshots allocation to prevent double-counting + * + * **RECLAIMED** (redirect path): STALE_POI or ZERO_POI conditions + * - Calls `reclaimRewards()` to mint tokens to configured reclaim address + * - If no reclaim address configured, rewards are dropped (not minted) + * - Snapshots allocation to prevent double-counting + * + * **DEFERRED** (early return): ALLOCATION_TOO_YOUNG or SUBGRAPH_DENIED conditions + * - Returns 0 without calling take or reclaim + * - Does NOT snapshot allocation (preserves rewards for later collection) + * - Allows rewards to be claimed when condition clears + * + * ## Subgraph Denial (Soft Deny) + * + * When a subgraph is denied, this function implements "soft deny": + * - Returns early without claiming or reclaiming + * - Allocation state is preserved (pending rewards not cleared) + * - Pre-denial rewards remain claimable after undeny + * - Ongoing issuance during denial is reclaimed at RewardsManager level (hard deny) * * Note: Indexers should present POIs at least every `maxPOIStaleness` to avoid being locked out of rewards. * A zero POI can be presented if a valid one is unavailable, to prevent staleness and slashing. @@ -288,9 +312,19 @@ abstract contract AllocationManager is /** * @notice Close an allocation * Does not require presenting a POI, use {_collectIndexingRewards} to present a POI and collect rewards - * @dev Note that allocations are nowlong lived. All service payments, including indexing rewards, should be collected periodically - * without the need of closing the allocation. Allocations should only be closed when indexers want to reclaim the allocated - * tokens for other purposes. + * @dev Allocations are long-lived. All service payments, including indexing rewards, should be collected + * periodically without closing. Allocations should only be closed when indexers want to reclaim tokens. + * + * ## Reward Handling on Close + * + * Uncollected rewards are reclaimed with CLOSE_ALLOCATION reason: + * - If reclaim address configured: tokens minted to that address + * - If no reclaim address: rewards are dropped (not minted anywhere) + * + * ## Known Limitation + * + * `clearPendingRewards()` is only called when `0 < reclaimedRewards`. This means: + * - If no reclaim address is configured, `accRewardsPending` may remain non-zero * * Emits a {AllocationClosed} event * From 620dec50a2b2867cd8114352a1768cc2fc44d799 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:54:28 +0000 Subject: [PATCH 27/43] docs: RewardsManager --- .../contracts/rewards/RewardsManager.sol | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 7ea0d8865..f570feb75 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -24,14 +24,27 @@ import { RewardsCondition } from "@graphprotocol/interfaces/contracts/contracts/ * @title Rewards Manager Contract * @author Edge & Node * @notice Manages rewards distribution for indexers and delegators in the Graph Protocol - * @dev Tracks how inflationary GRT rewards should be handed out. Relies on the Curation contract - * and the Staking contract. Signaled GRT in Curation determine what percentage of the tokens go - * towards each subgraph. Then each Subgraph can have multiple Indexers Staked on it. Thus, the - * total rewards for the Subgraph are split up for each Indexer based on much they have Staked on - * that Subgraph. * - * @dev If an `issuanceAllocator` is set, it is used to determine the amount of GRT to be issued per block. - * Otherwise, the `issuancePerBlock` variable is used. In relation to the IssuanceAllocator, this contract + * @dev ## Token Accounting Model + * + * Rewards use a two-level accumulation model with snapshot-based safety: + * + * **Level 1 - Signal Distribution (cross-subgraph):** + * - `accRewardsPerSignal` accumulates rewards per signaled token globally + * - Each subgraph gets rewards proportional to its curation signal + * - `accRewardsForSubgraph` tracks total rewards allocated to each subgraph + * + * **Level 2 - Allocation Distribution (within-subgraph):** + * - `accRewardsPerAllocatedToken` scales subgraph rewards to indexer allocations + * - Each allocation tracks its starting snapshot to calculate its share + * + * Accumulation invariants: + * - Snapshots prevent double-counting: each allocation's reward = (current - snapshot) × tokens + * - Accumulator values never decrease + * - Tokens are minted at claim time + * + * @dev If an `issuanceAllocator` is set, it determines GRT issued per block. + * Otherwise, the `issuancePerBlock` storage value is used. This contract * is a self-minting target responsible for directly minting allocated GRT. * * Note: @@ -381,7 +394,12 @@ contract RewardsManager is return accRewardsPerSignal.add(getNewRewardsPerSignal()); } - /// @inheritdoc IRewardsManager + /** + * @inheritdoc IRewardsManager + * @dev Returns accumulated rewards for external callers. + * New rewards are only included if the subgraph is claimable (neither denied nor below minimum signal). + * Reclaim for non-claimable subgraphs is handled in `onSubgraphAllocationUpdate()`. + */ function getAccRewardsForSubgraph(bytes32 _subgraphDeploymentID) public view override returns (uint256) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; From 1e17d3e980c4e77e961f5e84c21b83ca016cb960 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:54:55 +0000 Subject: [PATCH 28/43] docs: RewardsManagerStorage --- .../rewards/RewardsManagerStorage.sol | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol index 03dc462f5..14a8061b0 100644 --- a/packages/contracts/contracts/rewards/RewardsManagerStorage.sol +++ b/packages/contracts/contracts/rewards/RewardsManagerStorage.sol @@ -24,18 +24,30 @@ contract RewardsManagerV1Storage is Managed { /// @dev Deprecated issuance rate variable (no longer used) uint256 private __DEPRECATED_issuanceRate; // solhint-disable-line var-name-mixedcase - /// @notice Accumulated rewards per signal + + /// @notice Accumulated rewards per signal (fixed-point, scaled by 1e18) + /// @dev Never decreases. Only increases via updateAccRewardsPerSignal(). + /// Represents the cumulative GRT rewards per signaled token since contract deployment. uint256 public accRewardsPerSignal; + /// @notice Block number when accumulated rewards per signal was last updated + /// @dev Used to calculate time delta for new reward accrual. Must be updated atomically + /// with accRewardsPerSignal to maintain accounting consistency. uint256 public accRewardsPerSignalLastBlockUpdated; /// @notice Address of role allowed to deny rewards on subgraphs address public subgraphAvailabilityOracle; /// @notice Subgraph related rewards: subgraph deployment ID => subgraph rewards + /// @dev Accumulation state tracked per subgraph. mapping(bytes32 => IRewardsManager.Subgraph) public subgraphs; /// @notice Subgraph denylist: subgraph deployment ID => block when added or zero (if not denied) + /// @dev **Denial Semantics**: + /// - Non-zero value: subgraph is denied since that block number + /// - Zero value: subgraph is not denied + /// - When denied: accRewardsPerAllocatedToken freezes (stops updating) + /// - New rewards during denial are reclaimed (if reclaim address configured) or dropped mapping(bytes32 => uint256) public denylist; } @@ -88,11 +100,21 @@ abstract contract RewardsManagerV5Storage is IRewardsManager, RewardsManagerV4St */ abstract contract RewardsManagerV6Storage is RewardsManagerV5Storage { /// @dev Address of the rewards eligibility oracle contract + /// When set, indexers must pass eligibility check to claim rewards. + /// Zero address disables eligibility checks. IRewardsEligibility internal rewardsEligibilityOracle; + /// @dev Address of the issuance allocator + /// When set, determines GRT issued per block. Zero address uses issuancePerBlock storage value. IIssuanceAllocationDistribution internal issuanceAllocator; + /// @dev Mapping of reclaim reason identifiers to reclaim addresses - /// @dev Uses bytes32 for extensibility. See RewardsReclaim library for canonical reasons. + /// @dev Uses bytes32 for extensibility. See RewardsCondition library for canonical reasons. + /// **IMPORTANT**: Changes to reclaim addresses are retroactive. When an address is changed, + /// ALL future reclaims for that reason go to the new address, regardless of when the + /// rewards were originally accrued. Zero address means rewards are dropped (not minted). mapping(bytes32 => address) internal reclaimAddresses; + /// @dev Default fallback address for reclaiming rewards when no reason-specific address is configured. + /// Zero address means rewards are dropped (not minted) if no specific reclaim address matches. address internal defaultReclaimAddress; } From 781bf79e859684b7f936191e69d50c028ce6215d Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 00:55:24 +0000 Subject: [PATCH 29/43] docs: rewards accounting --- docs/DeniedSubgraphRewardsAnalysis.md | 391 +++----------------------- docs/RewardAccountingSafety.md | 172 +++++++++++ 2 files changed, 214 insertions(+), 349 deletions(-) create mode 100644 docs/RewardAccountingSafety.md diff --git a/docs/DeniedSubgraphRewardsAnalysis.md b/docs/DeniedSubgraphRewardsAnalysis.md index cfefb1f45..b594433bd 100644 --- a/docs/DeniedSubgraphRewardsAnalysis.md +++ b/docs/DeniedSubgraphRewardsAnalysis.md @@ -1,372 +1,65 @@ -# Denied Subgraph Rewards - Implementation Analysis +# Subgraph Denial: Reward Behaviour ## Overview -This document analyzes the implementation that prevents indexers from claiming rewards earned during a subgraph's denied period, while allowing pre-denial rewards to be collected after undeny. +When a subgraph is denied, indexers cannot claim rewards for the denial period, but pre-denial rewards remain claimable after the subgraph is undenied. -## Key Changes +## Reward Disposition by Period -### 1. `_setDenied()` (RewardsManager.sol) +| Period | Rewards | Disposition | +| ----------------- | ----------------------------- | -------------------------------------------------------- | +| **Pre-denial** | Rewards accrued before denial | Claimable after undeny | +| **During denial** | Rewards issued while denied | Reclaimed to protocol (or dropped if no reclaim address) | +| **Post-undeny** | Rewards accrued after undeny | Claimable normally | -```solidity -function _setDenied(bytes32 subgraphDeploymentId, bool deny) private { - onSubgraphAllocationUpdate(subgraphDeploymentId); // Snapshot/reclaim BEFORE state change +## How Denial Affects Allocations - bool stateChange = deny == (denylist[subgraphDeploymentId] == 0); - if (stateChange) { - uint256 sinceBlock = deny ? block.number : 0; - denylist[subgraphDeploymentId] = sinceBlock; - emit RewardsDenylistUpdated(subgraphDeploymentId, sinceBlock); - } -} -``` +### Existing Allocations (created before denial) -Calls `onSubgraphAllocationUpdate()` **before** changing denylist state, ensuring: +- Pre-denial rewards are preserved in the allocation's snapshot +- Cannot claim while denied (returns 0) +- After undeny, can claim pre-denial rewards +- Denial-period rewards are not available (reclaimed at protocol level) -- On deny: snapshots current rewards state while `isDenied()` = false -- On undeny: reclaims remaining denied-period rewards while `isDenied()` = true +### New Allocations (created while denied) -**Idempotency guard:** The `stateChange` check ensures that redundant calls are no-ops: +- Created with current frozen reward state as baseline +- Only earn rewards after subgraph is undenied +- Cannot earn backdated rewards for denial period -- Calling `setDenied(id, true)` when already denied does not update `denylist` or emit `RewardsDenylistUpdated` -- Calling `setDenied(id, false)` when already not denied does not update `denylist` or emit `RewardsDenylistUpdated` -- In both cases, `onSubgraphAllocationUpdate()` is still called (snapshot/reclaim still occurs), but the denylist state itself is unchanged -- This prevents the block number from being overwritten on redundant deny calls (which would lose the original deny timestamp) +### POI Presentation While Denied -### 2. `onSubgraphAllocationUpdate()` (RewardsManager.sol) +- Indexers can (and should) continue presenting POIs +- Prevents allocations from becoming stale +- Returns 0 rewards but maintains allocation health -```solidity -function onSubgraphAllocationUpdate(bytes32 _subgraphDeploymentID) public override returns (uint256) { - Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; - (uint256 accRewardsPerAllocatedToken, uint256 accRewardsForSubgraph) = getAccRewardsPerAllocatedToken( - _subgraphDeploymentID - ); +## Two-Layer Denial System - if (isDenied(_subgraphDeploymentID)) { - // Reclaim new rewards instead of crediting to allocators - if (subgraph.accRewardsForSubgraphSnapshot < accRewardsForSubgraph) - _reclaimRewards(RewardsCondition.SUBGRAPH_DENIED, - accRewardsForSubgraph - subgraph.accRewardsForSubgraphSnapshot, ...); - } else { - subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; - } - subgraph.accRewardsForSubgraphSnapshot = accRewardsForSubgraph; - return subgraph.accRewardsPerAllocatedToken; -} -``` +### Hard Deny (RewardsManager) -When denied: +- Freezes `accRewardsPerAllocatedToken` - no new rewards credited to allocations +- Reclaims ongoing issuance to configured reclaim address +- Operates at subgraph level (affects all allocations) -- Reclaims `accRewardsForSubgraph - accRewardsForSubgraphSnapshot` (actual GRT amount) -- Does NOT update `accRewardsPerAllocatedToken` (keeps it frozen) -- Updates `accRewardsForSubgraphSnapshot` to prevent double-counting +### Soft Deny (AllocationManager) -When not denied: +- Skips `takeRewards()` call when subgraph is denied +- Preserves allocation state for future claiming +- Returns early without modifying allocation snapshots -- Normal operation: updates both `accRewardsPerAllocatedToken` and snapshot - ---- - -## State Flow Scenarios - -### Scenario 1: Subgraph Gets Denied - -``` -T1: Normal operation - - accRewardsPerAllocatedToken = 100 - - accRewardsForSubgraphSnapshot = 1000 - -T2: setDenied(subgraph, true) called - - onSubgraphAllocationUpdate() called with isDenied() = FALSE - - Normal path: accRewardsPerAllocatedToken updated to 150 - - Snapshot updated - - THEN denylist set -``` - -### Scenario 2: Allocation Operations While Denied - -``` -T3: Any allocation operation (allocate/close/resize/collect) - - onSubgraphAllocationUpdate() called with isDenied() = TRUE - - New rewards (accRewardsForSubgraph - snapshot) are RECLAIMED - - accRewardsPerAllocatedToken stays FROZEN at 150 - - Snapshot updated (prevents double-counting) -``` - -### Scenario 3: Subgraph Gets Undenied - -``` -T4: setDenied(subgraph, false) called - - onSubgraphAllocationUpdate() called with isDenied() = TRUE - - Remaining denied-period rewards are RECLAIMED - - THEN denylist cleared - -T5: Next allocation operation - - isDenied() = FALSE - - Normal operation resumes from frozen state -``` - -### Scenario 4: Reward Collection - -``` -While denied: - - takeRewards() → _calcAllocationRewards() → onSubgraphAllocationUpdate() - - Returns frozen accRewardsPerAllocatedToken - - Allocation rewards = tokens × (frozen_value - allocation_snapshot) - - Only pre-denial rewards are calculated - - Collection may be blocked by soft deny in takeRewards() - -After undeny: - - Normal collection resumes - - Indexers get pre-denial rewards only (frozen value) -``` - ---- - -## Paths That Call `onSubgraphAllocationUpdate()` - -| Path | Location | Description | -| --------------------------- | ------------------------------- | ------------------------------------ | -| `_allocate()` | AllocationManager.sol:231 | Creating new allocation | -| `_closeAllocation()` | AllocationManager.sol:302, 439 | Closing allocation (normal or force) | -| `_resizeAllocation()` | AllocationManager.sol:389 | Resizing allocation | -| `_collectIndexingRewards()` | Via takeRewards() | Collecting rewards | -| `_setDenied()` | RewardsManager.sol:330 | Deny/undeny subgraph | -| `_calcAllocationRewards()` | RewardsManager.sol:593 | Calculating rewards | -| Legacy Staking | Staking.sol:897 | Legacy allocation operations | -| Legacy Staking | HorizonStakingExtension.sol:288 | Legacy extension | - -## Paths That Call `onSubgraphSignalUpdate()` - -| Path | Location | Description | -| ----------------------- | ------------------------------- | ---------------------- | -| Curation mint/burn | Curation.sol:435 | Signal changes | -| L2 Curation | L2Curation.sol:485 | L2 signal changes | -| Query fees collection | SubgraphService.sol:547 | Curation fees | -| Legacy close allocation | Staking.sol:862 | Curation fees on close | -| Legacy extension | HorizonStakingExtension.sol:458 | Curation fees | - -**Note:** `onSubgraphSignalUpdate()` updates `accRewardsForSubgraph` and `accRewardsPerSignalSnapshot`, which is separate from allocation reward accounting. - -## Operations That Do NOT Affect Allocation Rewards - -| Operation | Reason | -| ---------------------- | ----------------------------------------------------- | -| Slashing | Only affects stake on HorizonStaking, not allocations | -| Delegation | Separate from allocation rewards | -| Stake deposit/withdraw | Separate from allocation accounting | - ---- - -## Accounting Invariants - -1. **While denied:** - - `accRewardsPerAllocatedToken` is frozen - - New rewards are reclaimed via `_reclaimRewards()` - - `accRewardsForSubgraphSnapshot` is updated to prevent double-counting - -2. **Allocation snapshots:** - - Each allocation stores its `accRewardsPerAllocatedToken` at creation - - Rewards = `tokens × (current - snapshot) / SCALING_FACTOR` - - When denied, `current` = frozen value, so only pre-denial rewards - -3. **No double-counting:** - - Each `onSubgraphAllocationUpdate()` call updates snapshot - - Reclaim amount = `accRewardsForSubgraph - accRewardsForSubgraphSnapshot` - - After reclaim, snapshot = current, so next call starts fresh - -4. **No bypasses:** - - All allocation-affecting operations go through `onSubgraphAllocationUpdate()` - - Signal changes (`onSubgraphSignalUpdate`) are separate accounting - ---- +Together: hard deny prevents new rewards accumulating; soft deny preserves pre-denial rewards. ## Edge Cases -### All allocations close while denied - -- `getAccRewardsPerAllocatedToken()` returns 0 when no allocated tokens -- But we don't update `accRewardsPerAllocatedToken` when denied -- Frozen value is preserved for new allocations after undeny - -### Allocation created while denied - -- Gets snapshot = frozen `accRewardsPerAllocatedToken` -- After undeny, rewards = (new value - frozen value) -- Only gets post-undeny rewards ✓ - -### Multiple reclaims while denied - -- Each call reclaims only NEW rewards since last call -- Snapshot updated after each reclaim -- No double-counting ✓ - -### Redundant deny/undeny calls (idempotency) - -- `setDenied(id, true)` when already denied: no state change, no event emitted -- `setDenied(id, false)` when not denied: no state change, no event emitted -- `onSubgraphAllocationUpdate()` is still called in both cases (side effect on reward snapshots/reclaims) -- Original deny block number is preserved on redundant deny calls ✓ - -### Zero rewards to reclaim - -- Condition: `subgraph.accRewardsForSubgraphSnapshot < accRewardsForSubgraph` -- If equal, no reclaim (nothing to reclaim) -- Prevents zero-amount reclaims ✓ - ---- - -## Soft Deny in AllocationManager - -### Location - -`AllocationManager.sol:_presentPOI()` (line 283) - -### Code - -```solidity -bool canClaimNow = allocation.createdAtEpoch < _graphEpochManager().currentEpoch() - && !_graphRewardsManager().isDenied(allocation.subgraphDeploymentId); -``` - -### Behavior When Denied - -When `isDenied() = true`, `canClaimNow = false`, which causes: - -1. **`takeRewards()` is NOT called** (line 288-289 skipped) - - No rewards are minted to indexer - - No rewards are reclaimed via takeRewards path - -2. **Stale/Zero POI still reclaimed** (lines 284-287) - - ```solidity - if (allocation.isStale(maxPOIStaleness)) { - _graphRewardsManager().reclaimRewards(RewardsCondition.STALE_POI, _allocationId, ""); - } else if (_poi == bytes32(0)) { - _graphRewardsManager().reclaimRewards(RewardsCondition.ZERO_POI, _allocationId, ""); - } - ``` - - - These reclaims go through `_calcAllocationRewards()` → `onSubgraphAllocationUpdate()` - - While denied, `onSubgraphAllocationUpdate()` reclaims subgraph-level rewards - -3. **Early return at line 297** - - ```solidity - if (!canClaimNow) return 0; - ``` - - - Does NOT snapshot allocation rewards - - Does NOT clear pending rewards - - Allocation state preserved for future collection - -4. **POI is still recorded** (line 294) - - ```solidity - _allocations.presentPOI(_allocationId); - ``` - - - Prevents allocation from becoming stale - - Indexer can keep presenting POIs while denied - -### Flow Summary - -``` -_presentPOI() called while denied: -├── Stale POI? → reclaimRewards(STALE_POI) → onSubgraphAllocationUpdate() reclaims -├── Zero POI? → reclaimRewards(ZERO_POI) → onSubgraphAllocationUpdate() reclaims -├── Valid POI? → canClaimNow = false, skip takeRewards() -├── Record POI (prevents staleness) -└── Return 0 (no snapshot, no clear pending) -``` - -### Why This Works - -1. **Pre-denial rewards preserved:** - - `accRewardsPerAllocatedToken` frozen in RewardsManager - - Allocation's pending rewards not cleared - - After undeny, indexer can collect pre-denial rewards - -2. **Denied-period rewards reclaimed:** - - `onSubgraphAllocationUpdate()` reclaims subgraph-level rewards - - Called via reclaimRewards() even when valid POI - - Called via setDenied() on deny/undeny transitions - -3. **Indexer keeps allocation healthy:** - - Can present POIs to avoid staleness - - Doesn't forfeit allocation by not presenting - -### Interaction with RewardsManager - -| AllocationManager Action | RewardsManager Behavior | -| ---------------------------- | ----------------------------------------------------------------- | -| `takeRewards()` skipped | No rewards minted | -| `reclaimRewards(STALE/ZERO)` | Calls `_calcAllocationRewards()` → `onSubgraphAllocationUpdate()` | -| POI recorded | No RewardsManager interaction | -| Early return | No snapshot update in allocation | - ---- - -## Known Limitations and Design Considerations - -### 1. takeRewards() Snapshot Control - -The current implementation reclaims rewards in `onSubgraphAllocationUpdate()` when a subgraph is denied. However, the calling issuer (e.g., SubgraphService) does not have control over when this reclaim happens - it occurs automatically on any allocation update. - -**Alternative approach considered:** RewardsManager could skip reclaiming for denied subgraphs and leave the decision to the calling issuer. This would give issuers more flexibility in how they handle denied-period rewards. - -**Current behavior:** RewardsManager automatically reclaims denied-period rewards on every allocation update. This ensures rewards are reclaimed promptly but removes issuer control over the timing. - -### 2. Soft Deny vs Hard Deny - -The implementation uses two layers: - -1. **Hard deny (RewardsManager):** Freezes `accRewardsPerAllocatedToken` and reclaims new rewards -2. **Soft deny (AllocationManager):** Skips `takeRewards()` when denied, allowing pre-denial rewards to be preserved - -These layers work together but could potentially diverge if other issuers implement different soft deny behaviors. - -### 3. Legacy `_deniedRewards()` Flow - -The `isDenied()` check in `_deniedRewards()` is now effectively unused for new SubgraphService allocations since `AllocationManager._presentPOI()` implements soft deny by skipping `takeRewards()` entirely. However, it's still invoked for legacy staking (`Staking.sol`) allocations. - -**Potential cleanup:** The `isDenied()` branch in `_deniedRewards()` could be removed if legacy staking support is deprecated, as the new SubgraphService handles denied subgraphs via AllocationManager soft deny. - -### 4. Race Conditions on Deny/Undeny - -If `setDenied()` is called while an allocation operation is in flight: - -- The `onSubgraphAllocationUpdate()` call in `_setDenied()` runs first (before state change) -- This should correctly snapshot/reclaim rewards before the state transition -- However, tight timing with concurrent allocation operations could lead to edge cases - ---- - -## Test Cases Needed - -The following test scenarios should be verified: - -1. **Basic deny/undeny cycle:** - - Allocation earns rewards → subgraph denied → allocation can collect pre-denial rewards after undeny - -2. **Rewards during denied period:** - - Subgraph denied → time passes → rewards reclaimed (not available to indexers) - -3. **Multiple reclaims while denied:** - - Multiple allocation operations while denied → each reclaims only new rewards since last operation - -4. **Allocation created while denied:** - - Subgraph denied → new allocation created → subgraph undenied → allocation only earns post-undeny rewards - -5. **All allocations close while denied:** - - All allocations close while denied → frozen `accRewardsPerAllocatedToken` preserved → new allocation after undeny works correctly - -6. **Zero rewards scenario:** - - No new rewards since last update → no reclaim operation (condition check prevents zero-amount reclaims) +| Scenario | Behavior | +| ---------------------------------- | ------------------------------------------------------------------------------- | +| All allocations close while denied | Frozen reward state preserved; new allocations after undeny use frozen baseline | +| Redundant deny call | No state change; original deny block preserved | +| Redundant undeny call | No state change | +| Zero reclaim address | Denial-period rewards dropped (never minted) | -7. **POI presentation while denied:** - - Indexer presents valid POI while denied → returns 0, no snapshot update, allocation state preserved +## Safety Guarantees -8. **Idempotent deny/undeny (no-op cases):** - - Deny already-denied subgraph → no event emitted, denylist block number preserved - - Undeny already-not-denied subgraph → no event emitted, denylist remains zero +1. **No double-counting**: Snapshot mechanism ensures each reward period is counted once +2. **No lost pre-denial rewards**: Frozen state preserves indexer's earned rewards +3. **Idempotent operations**: Redundant deny/undeny calls are safe no-ops diff --git a/docs/RewardAccountingSafety.md b/docs/RewardAccountingSafety.md new file mode 100644 index 000000000..2a4144c64 --- /dev/null +++ b/docs/RewardAccountingSafety.md @@ -0,0 +1,172 @@ +# Reward Accounting Safety + +This document describes the mechanisms that prevent reward mis-accounting (double-counting or unintentional loss). + +## Two-Level Accumulation Model + +Rewards flow through two levels before reaching allocations: + +``` +Global Issuance + │ + ▼ (proportional to signal) +┌──────────────────────────────────────────────┐ +│ Level 1: Signal → Subgraph │ +│ accRewardsPerSignal → accRewardsForSubgraph │ +└──────────────────────────────────────────────┘ + │ + ▼ (proportional to allocated tokens) +┌─────────────────────────────────────────┐ +│ Level 2: Subgraph → Allocation │ +│ accRewardsPerAllocatedToken → claim │ +└─────────────────────────────────────────┘ +``` + +Each level uses the same pattern: an accumulator increases over time, and participants snapshot their starting point to calculate their share. + +## Core Safety Mechanism: Snapshots + +**Principle**: Rewards = (current_accumulator - snapshot) × tokens + +Snapshots prevent double-counting by recording each participant's starting point: + +| Component | Accumulator | Snapshot | Prevents | +| ---------- | ----------------------------- | ----------------------------- | --------------------------------------------- | +| Subgraph | `accRewardsPerSignal` | `accRewardsPerSignalSnapshot` | Same rewards credited to multiple subgraphs | +| Allocation | `accRewardsPerAllocatedToken` | Stored in allocation state | Same rewards claimed twice by same allocation | + +After any update, snapshot = current accumulator. Next calculation starts from zero delta. + +## Key Invariants + +### 1. Monotonic Accumulators + +`accRewardsPerSignal` and `accRewardsPerAllocatedToken` only increase (never decrease). + +**Exception**: `accRewardsPerAllocatedToken` freezes (stops increasing) when subgraph is denied or below minimum signal. It never decreases. + +**Why it matters**: Decreasing accumulators would cause negative reward calculations or allow re-claiming past rewards. + +### 2. Snapshot Consistency + +After every state update, snapshot equals current accumulator value. + +**Why it matters**: Stale snapshots would allow the same reward period to be counted multiple times. + +### 3. Update-Before-Change + +Accumulators must be updated BEFORE any state change that affects reward distribution: + +- Before `issuancePerBlock` changes → call `updateAccRewardsPerSignal()` +- Before signal changes → call `onSubgraphSignalUpdate()` +- Before allocation changes → call `onSubgraphAllocationUpdate()` + +**Why it matters**: Changing distribution parameters without first crediting accrued rewards would lose or misattribute those rewards. + +## Critical Call Ordering + +### Allocation Creation + +```solidity +// In AllocationManager._allocate(): +_allocationData = _getAllocationData(_subgraphDeploymentId); // ① Calls onSubgraphAllocationUpdate +_allocations.create(...); // ② Creates allocation +_allocations.snapshotRewards(..., onSubgraphAllocationUpdate()); // ③ Updates snapshot +``` + +**Why this order matters**: + +- Step ① with zero allocations → triggers NO_ALLOCATION reclaim for gap period +- Step ② creates allocation → now allocatedTokens > 0 +- Step ③ same block → newRewards ≈ 0, just confirms snapshot + +**If reversed**: Gap-period rewards would be distributed to accumulator but no allocation could claim them (all snapshots would be at/above post-distribution level). + +### Reward Claiming + +```solidity +// In AllocationManager._presentPoi(): +rewards = takeRewards(_allocationId); // ① Mints rewards +snapshotRewards(_allocationId, onSubgraphAllocationUpdate(...)); // ② Updates snapshot +clearPendingRewards(_allocationId); // ③ Clears pending +``` + +**Why this order matters**: + +- Step ① calculates and mints based on current snapshot +- Step ② updates snapshot to current accumulator +- Future claims start from new snapshot (zero delta for same block) + +## Reclaim as Safety Net + +Every reward path that cannot reach an allocation has a reclaim handler: + +| Condition | When Triggered | Reclaim Reason | +| -------------------- | ------------------------------------------------------- | ------------------------ | +| No global signal | `updateAccRewardsPerSignal()` with signalledTokens = 0 | `NO_SIGNAL` | +| Subgraph denied | `onSubgraphAllocationUpdate()` | `SUBGRAPH_DENIED` | +| Below minimum signal | `onSubgraphAllocationUpdate()` | `BELOW_MINIMUM_SIGNAL` | +| No allocations | `onSubgraphAllocationUpdate()` with allocatedTokens = 0 | `NO_ALLOCATION` | +| Indexer ineligible | `takeRewards()` | `INDEXER_INELIGIBLE` | +| Stale/zero POI | `_presentPoi()` | `STALE_POI` / `ZERO_POI` | +| Allocation close | `_closeAllocation()` | `CLOSE_ALLOCATION` | + +**Reclaim priority**: reason-specific address → defaultReclaimAddress → dropped (no mint) + +## Potential Failure Modes (Mitigated) + +| Failure Mode | How Prevented | +| ---------------------------- | -------------------------------------------------------------------------------- | +| Double-mint same rewards | Snapshot updated after every claim; same-block calls return ~0 | +| Rewards stuck in accumulator | NO_ALLOCATION reclaim before allocation creation | +| Gap period loss | `_getAllocationData` calls `onSubgraphAllocationUpdate` before allocation exists | +| Denial-period accumulation | `accRewardsPerAllocatedToken` freezes; new rewards reclaimed | +| Signal change mid-period | `onSubgraphSignalUpdate` hook called before signal changes | + +## Division of Responsibility + +RewardsManager and issuers share responsibility for correct reward accounting: + +**RewardsManager** handles what it can observe: + +- Reclaims rewards when subgraph conditions prevent distribution (denied, below minimum, zero allocations) +- Denies rewards at claim time when indexer is ineligible +- Maintains accumulator and snapshot state + +**Issuers** control claim timing and can defer: + +- AllocationManager defers claims for `SUBGRAPH_DENIED` and `ALLOCATION_TOO_YOUNG` by returning early +- This preserves allocation state so rewards remain claimable after conditions change +- RM cannot know issuer intent, so issuers must decide when to attempt claims + +**Example - Subgraph Denial**: + +1. RM freezes `accRewardsPerAllocatedToken` and reclaims new subgraph-level rewards +2. AM detects denial and skips `takeRewards()` call entirely (soft deny) +3. Pre-denial rewards preserved in allocation snapshot +4. After undeny, AM can claim the preserved rewards + +## Issuer Requirements + +RewardsManager relies on issuers to maintain shared state correctly. + +**Required hook**: + +| Hook | When to Call | +| ---------------------------- | ------------------------- | +| `onSubgraphAllocationUpdate` | Before allocation changes | + +Note: If the issuer collects curation fees (`curation.collect()`), it must also call `onSubgraphSignalUpdate` before the collect since that changes signal. SubgraphService does this in `_collectQueryFees`. + +**Allocation snapshot management**: + +Allocation snapshots are stored in issuer contracts, not RewardsManager. After each `takeRewards()` or `reclaimRewards()` call, issuers must update the allocation's snapshot to the current `accRewardsPerAllocatedToken`. Failure to snapshot allows the same rewards to be claimed again. + +**Authorized issuers**: SubgraphService (active), Staking (deprecated, legacy allocations only) + +## Other Hook Callers + +| Hook | Caller | Trigger | +| --------------------------- | ------------------------- | ---------------------------------------------- | +| `updateAccRewardsPerSignal` | RewardsManager (internal) | Before `issuancePerBlock` or allocator changes | +| `onSubgraphSignalUpdate` | Curation | Before mint/burn signal | From 45f5af0712a970aa3c8998d1cb4585534ad1809b Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 01:11:02 +0000 Subject: [PATCH 30/43] refactor: extract _getSubgraphRewardsState Consolidate reward claimability checks into single helper. Returns new rewards, signal amount, and denial condition (SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, or NONE). --- .../contracts/rewards/RewardsManager.sol | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index f570feb75..24efcb582 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -402,18 +402,8 @@ contract RewardsManager is */ function getAccRewardsForSubgraph(bytes32 _subgraphDeploymentID) public view override returns (uint256) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; - - // Get tokens signalled on the subgraph - uint256 subgraphSignalledTokens = curation().getCurationPoolTokens(_subgraphDeploymentID); - - // Only accrue rewards if over a threshold - // solhint-disable-next-line gas-strict-inequalities - uint256 newRewards = (subgraphSignalledTokens >= minimumSubgraphSignal) // Accrue new rewards since last snapshot - ? getAccRewardsPerSignal().sub(subgraph.accRewardsPerSignalSnapshot).mul(subgraphSignalledTokens).div( - FIXED_POINT_SCALING_FACTOR - ) - : 0; - return subgraph.accRewardsForSubgraph.add(newRewards); + (uint256 newRewards, , bytes32 condition) = _getSubgraphRewardsState(_subgraphDeploymentID); + return subgraph.accRewardsForSubgraph.add(condition == RewardsCondition.NONE ? newRewards : 0); } /// @inheritdoc IRewardsManager @@ -451,6 +441,34 @@ contract RewardsManager is return (subgraph.accRewardsPerAllocatedToken.add(newRewardsPerAllocatedToken), accRewardsForSubgraph); } + // -- Internal Helpers -- + + /** + * @notice Calculate new rewards and claimability state for a subgraph + * @dev Returns the new rewards based on signal and the condition indicating why rewards + * may not be claimable (SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, or NONE if claimable). + * @param _subgraphDeploymentID Subgraph deployment + * @return newRewards The rewards that would accrue based on signal (may not be claimable) + * @return signalledTokens The subgraph's current signal + * @return condition The condition: NONE if claimable, otherwise the denial reason + */ + function _getSubgraphRewardsState( + bytes32 _subgraphDeploymentID + ) private view returns (uint256 newRewards, uint256 signalledTokens, bytes32 condition) { + Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; + signalledTokens = curation().getCurationPoolTokens(_subgraphDeploymentID); + uint256 accRewardsPerSignalDelta = getAccRewardsPerSignal().sub(subgraph.accRewardsPerSignalSnapshot); + newRewards = accRewardsPerSignalDelta.mul(signalledTokens).div(FIXED_POINT_SCALING_FACTOR); + + if (isDenied(_subgraphDeploymentID)) { + condition = RewardsCondition.SUBGRAPH_DENIED; + } else if (signalledTokens < minimumSubgraphSignal) { + condition = RewardsCondition.BELOW_MINIMUM_SIGNAL; + } else { + condition = RewardsCondition.NONE; + } + } + // -- Updates -- /** From 7b8992ee20e0069db9fbcb55976bbd6c21bbaee2 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 01:12:15 +0000 Subject: [PATCH 31/43] refactor: _deniedRewards Simplify denial logic with single reclaim attempt. Evaluates both denial conditions upfront, emits appropriate events, then uses first applicable reclaim address. Prefers SUBGRAPH_DENIED over INDEXER_INELIGIBLE when both apply. --- .../contracts/rewards/RewardsManager.sol | 57 +++++++------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 24efcb582..ca1b6f4ed 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -642,12 +642,12 @@ contract RewardsManager is * @param indexer Address of the indexer * @param allocationID Address of the allocation * @param subgraphDeploymentID Subgraph deployment ID for the allocation - * @return denied True if rewards should be denied (either reclaimed or dropped), false if they should be minted - * @dev First successful reclaim wins - checks performed in order with short-circuit on reclaim: - * 1. Subgraph deny list: emit RewardsDenied. If reclaim address set → reclaim and return (STOP, eligibility not checked) - * 2. Indexer eligibility: Checked if subgraph not denied OR denied without reclaim address. Emit RewardsDeniedDueToEligibility. If reclaim address set → reclaim and return - * Multiple denial events may be emitted only when multiple checks fail without reclaim addresses configured. - * Any failing check without a reclaim address still denies rewards (drops them without minting). + * @return denied True if rewards are denied (either reclaimed or dropped), false if they should be minted + * @dev Emits denial events, then attempts reclaim. + * Prefers subgraph denial over indexer ineligibility as reason when both apply. + * First configured applicable reclaim address is used. + * If rewards denied but no specific address is configured, the default reclaim address is used. + * If no applicable reclaim address is configured, rewards are not minted. */ function _deniedRewards( uint256 rewards, @@ -655,39 +655,20 @@ contract RewardsManager is address allocationID, bytes32 subgraphDeploymentID ) private returns (bool denied) { - if (isDenied(subgraphDeploymentID)) { - emit RewardsDenied(indexer, allocationID); - if ( - 0 < - _reclaimRewards( - RewardsCondition.SUBGRAPH_DENIED, - rewards, - indexer, - allocationID, - subgraphDeploymentID - ) - ) { - return true; // Successfully reclaimed, deny rewards - } - denied = true; // Denied but no reclaim address - } + bool isDeniedSubgraph = isDenied(subgraphDeploymentID); + bool isIneligible = address(rewardsEligibilityOracle) != address(0) && + !rewardsEligibilityOracle.isEligible(indexer); + if (!isDeniedSubgraph && !isIneligible) return false; - if (address(rewardsEligibilityOracle) != address(0) && !rewardsEligibilityOracle.isEligible(indexer)) { - emit RewardsDeniedDueToEligibility(indexer, allocationID, rewards); - if ( - 0 < - _reclaimRewards( - RewardsCondition.INDEXER_INELIGIBLE, - rewards, - indexer, - allocationID, - subgraphDeploymentID - ) - ) { - return true; // Successfully reclaimed, deny rewards - } - denied = true; // Denied but no reclaim address - } + if (isDeniedSubgraph) emit RewardsDenied(indexer, allocationID); + if (isIneligible) emit RewardsDeniedDueToEligibility(indexer, allocationID, rewards); + + bytes32 reason = isDeniedSubgraph ? RewardsCondition.SUBGRAPH_DENIED : RewardsCondition.NONE; + if (isIneligible && (!isDeniedSubgraph || reclaimAddresses[reason] == address(0))) + reason = RewardsCondition.INDEXER_INELIGIBLE; + + _reclaimRewards(reason, rewards, indexer, allocationID, subgraphDeploymentID); + return true; } /** From 80300d7cae463ae8865447ea6ea722759df46392 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 01:13:56 +0000 Subject: [PATCH 32/43] refactor: _reclaimRewards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add fallback to defaultReclaimAddress. Priority: reason-specific address → default address → drop. Early return on zero rewards. --- .../contracts/rewards/RewardsManager.sol | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index ca1b6f4ed..d392b9c89 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -613,27 +613,36 @@ contract RewardsManager is } /** - * @notice Common function to reclaim rewards to a configured address - * @param reason The reclaim reason identifier + * @notice Reclaim rewards to reason-specific address or default fallback + * @param reason Reclaim reason identifier * @param rewards Amount of rewards to reclaim * @param indexer Address of the indexer - * @param allocationID Address of the allocation - * @param subgraphDeploymentID Subgraph deployment ID for the allocation - * @return reclaimed The amount of rewards that were reclaimed (0 if no reclaim address set) + * @param allocationId Address of the allocation + * @param subgraphDeploymentId Subgraph deployment ID for the allocation + * @return Amount reclaimed (0 if no target address configured) + * + * @dev ## Reclaim Priority + * + * 1. Try the reason-specific address + * 2. If not configured, try defaultReclaimAddress + * 3. If neither configured, rewards are dropped (not minted), returns 0 */ function _reclaimRewards( bytes32 reason, uint256 rewards, address indexer, - address allocationID, - bytes32 subgraphDeploymentID - ) private returns (uint256 reclaimed) { + address allocationId, + bytes32 subgraphDeploymentId + ) private returns (uint256) { + if (rewards == 0) return 0; + address target = reclaimAddresses[reason]; - if (0 < rewards && target != address(0)) { - graphToken().mint(target, rewards); - emit RewardsReclaimed(reason, rewards, indexer, allocationID, subgraphDeploymentID); - reclaimed = rewards; - } + if (target == address(0)) target = defaultReclaimAddress; + if (target == address(0)) return 0; // Dropped, not reclaimed + + graphToken().mint(target, rewards); + emit RewardsReclaimed(reason, rewards, indexer, allocationId, subgraphDeploymentId); + return rewards; } /** From bc7a67fec15d4352df73d1397af2af1bbda06c64 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 01:15:24 +0000 Subject: [PATCH 33/43] feat: reclaim for no signal in updateAccRewardsPerSignal Reclaim rewards when no signal exists. Uses _getNewRewardsPerSignal to detect zero-signal periods and routes issuance to NO_SIGNAL reclaim address instead of dropping. --- .../contracts/contracts/rewards/RewardsManager.sol | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index d392b9c89..207b6bce1 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -475,9 +475,20 @@ contract RewardsManager is * @inheritdoc IRewardsManager * @dev Must be called before `issuancePerBlock` or `total signalled GRT` changes. * Called from the Curation contract on mint() and burn() + * + * ## Zero Signal Handling + * + * When total signalled tokens is zero, issuance for the period is reclaimed + * (if NO_SIGNAL reclaim address is configured) rather than being lost. */ function updateAccRewardsPerSignal() public override returns (uint256) { - accRewardsPerSignal = getAccRewardsPerSignal(); + (uint256 claimablePerSignal, uint256 unclaimableTokens) = _getNewRewardsPerSignal(); + if (claimablePerSignal == 0 && unclaimableTokens == 0) return accRewardsPerSignal; + + if (0 < unclaimableTokens) + _reclaimRewards(RewardsCondition.NO_SIGNAL, unclaimableTokens, address(0), address(0), bytes32(0)); + + accRewardsPerSignal = accRewardsPerSignal.add(claimablePerSignal); accRewardsPerSignalLastBlockUpdated = block.number; return accRewardsPerSignal; } From 150b7a04ecf200d199f20695f7846f51713e9c6a Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 01:17:11 +0000 Subject: [PATCH 34/43] refactor: _getSubgraphAllocatedTokens Extract helper to sum allocated tokens across all issuers. Enables consistent token counting for reward calculations when multiple reward issuers exist (Staking + SubgraphService). --- .../contracts/rewards/RewardsManager.sol | 81 +++++++++++++++---- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 207b6bce1..9e81ff67b 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -9,7 +9,6 @@ import { IERC165 } from "@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 "@graphprotocol/interfaces/contracts/contracts/token/IGraphToken.sol"; import { RewardsManagerV6Storage } from "./RewardsManagerStorage.sol"; import { IRewardsIssuer } from "@graphprotocol/interfaces/contracts/contracts/rewards/IRewardsIssuer.sol"; @@ -469,6 +468,24 @@ contract RewardsManager is } } + /** + * @notice Get total allocated tokens for a subgraph across all issuers + * @param _subgraphDeploymentID Subgraph deployment + * @return Total tokens allocated to this subgraph + */ + function _getSubgraphAllocatedTokens(bytes32 _subgraphDeploymentID) private view returns (uint256) { + uint256 subgraphAllocatedTokens = 0; + address[2] memory rewardsIssuers = [address(staking()), address(subgraphService)]; + for (uint256 i = 0; i < rewardsIssuers.length; ++i) { + if (rewardsIssuers[i] != address(0)) { + subgraphAllocatedTokens += IRewardsIssuer(rewardsIssuers[i]).getSubgraphAllocatedTokens( + _subgraphDeploymentID + ); + } + } + return subgraphAllocatedTokens; + } + // -- Updates -- /** @@ -512,28 +529,58 @@ contract RewardsManager is /** * @inheritdoc IRewardsManager * @dev Hook called from the Staking contract on allocate() and close() + * + * ## Claimability Behavior + * + * When a subgraph is not claimable (denied or below minimum signal): + * - `accRewardsPerAllocatedToken` is NOT updated (frozen) + * - New rewards are reclaimed with the appropriate reason (SUBGRAPH_DENIED or BELOW_MINIMUM_SIGNAL) + * - `accRewardsPerSignalSnapshot` is updated to prevent double-reclaim + * + * When claimable: + * - `accRewardsForSubgraph` and `accRewardsPerAllocatedToken` are updated normally + * - Allocations can claim their proportional share + * + * @return accRewardsPerAllocatedToken Current `accRewardsPerAllocatedToken` (frozen while subgraph is not claimable) */ - function onSubgraphAllocationUpdate(bytes32 _subgraphDeploymentID) public override returns (uint256) { + function onSubgraphAllocationUpdate( + bytes32 _subgraphDeploymentID + ) public override returns (uint256 accRewardsPerAllocatedToken) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; - (uint256 accRewardsPerAllocatedToken, uint256 accRewardsForSubgraph) = getAccRewardsPerAllocatedToken( + + (uint256 newRewards, uint256 signalledTokens, bytes32 condition) = _getSubgraphRewardsState( _subgraphDeploymentID ); + subgraph.accRewardsPerSignalSnapshot = getAccRewardsPerSignal(); + accRewardsPerAllocatedToken = subgraph.accRewardsPerAllocatedToken; + if (newRewards == 0) return accRewardsPerAllocatedToken; + + // Fallback: if denied but no reclaim address, try BELOW_MINIMUM_SIGNAL instead + if ( + condition == RewardsCondition.SUBGRAPH_DENIED && + reclaimAddresses[condition] == address(0) && + signalledTokens < minimumSubgraphSignal + ) { + condition = RewardsCondition.BELOW_MINIMUM_SIGNAL; + } - if (isDenied(_subgraphDeploymentID)) { - if (subgraph.accRewardsForSubgraphSnapshot < accRewardsForSubgraph) - _reclaimRewards( - RewardsCondition.SUBGRAPH_DENIED, - accRewardsForSubgraph - subgraph.accRewardsForSubgraphSnapshot, - address(0), - address(0), - _subgraphDeploymentID, - "" - ); - } else { - subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + if (condition != RewardsCondition.NONE) { + _reclaimRewards(condition, newRewards, address(0), address(0), _subgraphDeploymentID); + return accRewardsPerAllocatedToken; } - subgraph.accRewardsForSubgraphSnapshot = accRewardsForSubgraph; - return subgraph.accRewardsPerAllocatedToken; + + uint256 subgraphAllocatedTokens = _getSubgraphAllocatedTokens(_subgraphDeploymentID); + if (subgraphAllocatedTokens == 0) { + _reclaimRewards(RewardsCondition.NO_ALLOCATION, newRewards, address(0), address(0), _subgraphDeploymentID); + return accRewardsPerAllocatedToken; + } + + subgraph.accRewardsForSubgraph = subgraph.accRewardsForSubgraph.add(newRewards); + accRewardsPerAllocatedToken = subgraph.accRewardsPerAllocatedToken.add( + newRewards.mul(FIXED_POINT_SCALING_FACTOR).div(subgraphAllocatedTokens) + ); + subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + subgraph.accRewardsForSubgraphSnapshot = subgraph.accRewardsForSubgraph; } /// @inheritdoc IRewardsManager From f830204ddd446ff5c000e96d1b25b5138a59a8d3 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:27:45 +0000 Subject: [PATCH 35/43] test: test updated rewards reclaim logic - Adjust expected rewards to account for pre-allocation reclaim - Use evm_setAutomine to batch signal mints in same block - Move b1 snapshot after allocation creation - Check only subgraph1 rewards (subgraph2 has no allocation) - rewards-calculations: update expected subgraph rewards from 1400 to 1000 - rewards-distribution: batch signals in same block, check sg1 only - rewards-interface: update IRewardsManager interface ID --- .../unit/rewards/rewards-calculations.test.ts | 3 +- .../unit/rewards/rewards-distribution.test.ts | 17 +- .../unit/rewards/rewards-interface.test.ts | 2 +- .../unit/rewards/rewards-reclaim.test.ts | 418 ++++++++++++++++-- .../tests/unit/rewards/rewards.test.ts | 20 +- .../contracts/tests/MockSubgraphService.sol | 7 +- .../test/unit/mocks/MockRewardsManager.sol | 8 +- 7 files changed, 431 insertions(+), 44 deletions(-) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts index b100905b0..30bae0483 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts @@ -370,7 +370,8 @@ describe('Rewards - Calculations', () => { await helpers.mine(ISSUANCE_RATE_PERIODS) // Prepare expected results - const expectedSubgraphRewards = toGRT('1400') // 7 blocks since signaling to when we do getAccRewardsForSubgraph + // Note: rewards from signal to allocation (2 blocks) are reclaimed since no allocations exist yet + const expectedSubgraphRewards = toGRT('1000') // 5 blocks since allocation to when we do getAccRewardsForSubgraph const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens // Update diff --git a/packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts index 8da4c222f..d4a55c1b9 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-distribution.test.ts @@ -690,12 +690,12 @@ describe('Rewards - Distribution', () => { // signal in two subgraphs in the same block const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2] + await hre.network.provider.send('evm_setAutomine', [false]) for (const sub of subgraphs) { await curation.connect(curator1).mint(sub, toGRT('1500'), 0) } - - // snapshot block before any accrual (we substract 1 because accrual starts after the first mint happens) - const b1 = await epochManager.blockNum().then((x) => x.toNumber() - 1) + await hre.network.provider.send('evm_mine') + await hre.network.provider.send('evm_setAutomine', [true]) // allocate const tokensToAllocate = toGRT('12500') @@ -715,6 +715,9 @@ describe('Rewards - Distribution', () => { .then((tx) => tx.data), ]) + // snapshot block after allocation (rewards before allocation were reclaimed for subgraph1) + const b1 = await epochManager.blockNum().then((x) => x.toNumber()) + // move time fwd await helpers.mineEpoch(epochManager) @@ -728,8 +731,12 @@ describe('Rewards - Distribution', () => { const accrual = await getRewardsAccrual(subgraphs) const b2 = await epochManager.blockNum().then((x) => x.toNumber()) - // round comparison because there is a small precision error due to dividing and accrual per signal - expect(toRound(accrual.all)).eq(toRound(ISSUANCE_PER_BLOCK.mul(b2 - b1))) + // Only check subgraph1 (with allocation) - subgraph2 has no allocation so its rewards + // are calculated from signal time, not from allocation time + // Each subgraph gets half the issuance (equal signal) + // Small tolerance for fixed-point arithmetic rounding + const expectedSg1Rewards = ISSUANCE_PER_BLOCK.div(2).mul(b2 - b1) + expect(toRound(accrual.sg1.mul(100).div(expectedSg1Rewards))).eq(toRound(BigNumber.from(100))) }) }) }) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts index a085c0b2c..132790e51 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-interface.test.ts @@ -58,7 +58,7 @@ describe('RewardsManager interfaces', () => { }) it('IRewardsManager should have stable interface ID', () => { - expect(IRewardsManager__factory.interfaceId).to.equal('0xa0a2f219') + expect(IRewardsManager__factory.interfaceId).to.equal('0x36b70adb') }) }) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts index 5cfe7f2a1..773c37670 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts @@ -17,6 +17,9 @@ const { HashZero } = constants const INDEXER_INELIGIBLE = utils.id('INDEXER_INELIGIBLE') const SUBGRAPH_DENIED = utils.id('SUBGRAPH_DENIED') const CLOSE_ALLOCATION = utils.id('CLOSE_ALLOCATION') +const NO_SIGNAL = utils.id('NO_SIGNAL') +const NO_ALLOCATION = utils.id('NO_ALLOCATION') +const BELOW_MINIMUM_SIGNAL = utils.id('BELOW_MINIMUM_SIGNAL') describe('Rewards - Reclaim Addresses', () => { const graph = hre.graph() @@ -223,7 +226,7 @@ describe('Rewards - Reclaim Addresses', () => { // RewardsReclaimed emitted with actual indexer/allocationID (allocation-level reclaim) await expect(tx) .emit(rewardsManager, 'RewardsReclaimed') - .withArgs(SUBGRAPH_DENIED, toGRT('1400'), indexer1.address, allocationID1, subgraphDeploymentID1, '0x') + .withArgs(SUBGRAPH_DENIED, toGRT('1400'), indexer1.address, allocationID1, subgraphDeploymentID1) // Reclaim wallet received the pre-denial rewards const balanceAfter = await grt.balanceOf(reclaimWallet.address) @@ -289,7 +292,7 @@ describe('Rewards - Reclaim Addresses', () => { .withArgs(indexer1.address, allocationID1, expectedRewards) await expect(tx) .emit(rewardsManager, 'RewardsReclaimed') - .withArgs(INDEXER_INELIGIBLE, expectedRewards, indexer1.address, allocationID1, subgraphDeploymentID1, '0x') + .withArgs(INDEXER_INELIGIBLE, expectedRewards, indexer1.address, allocationID1, subgraphDeploymentID1) // Check reclaim wallet received the rewards const balanceAfter = await grt.balanceOf(reclaimWallet.address) @@ -422,6 +425,56 @@ describe('Rewards - Reclaim Addresses', () => { expect(balanceAfter.sub(balanceBefore)).eq(0) }) + it('should reclaim to INDEXER_INELIGIBLE when both fail but only INDEXER_INELIGIBLE address configured (pre-denial allocation)', async function () { + // This tests the ternary in _deniedRewards that falls back to INDEXER_INELIGIBLE + // when SUBGRAPH_DENIED address is not configured + + // Setup ONLY INDEXER_INELIGIBLE reclaim address (not SUBGRAPH_DENIED) + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, reclaimWallet.address) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setRewardsEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation FIRST (before denial) - this is the key difference + await setupIndexerAllocation() + + // Mine blocks to accrue rewards + await helpers.mineEpoch(epochManager) + + // NOW deny the subgraph (after allocation exists) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + const expectedRewards = toGRT('1400') + + // Check balance before + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Close allocation - pre-denial rewards flow through _deniedRewards + // Both conditions are true, but SUBGRAPH_DENIED address is not set + // So it should fall back to INDEXER_INELIGIBLE + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // RewardsDenied IS emitted (allocation-level denial for pre-denial rewards) + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + // RewardsDeniedDueToEligibility IS emitted + await expect(tx).emit(rewardsManager, 'RewardsDeniedDueToEligibility') + // RewardsReclaimed should emit with INDEXER_INELIGIBLE reason (fallback) + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + // INDEXER_INELIGIBLE wallet should receive rewards (fallback from SUBGRAPH_DENIED) + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter.sub(balanceBefore)).gte(expectedRewards) + }) + it('should drop rewards when both fail and neither address configured', async function () { // Do NOT set any reclaim addresses @@ -528,12 +581,7 @@ describe('Rewards - Reclaim Addresses', () => { const balanceBefore = await grt.balanceOf(reclaimWallet.address) // Call reclaimRewards via mock subgraph service - const tx = await mockSubgraphService.callReclaimRewards( - rewardsManager.address, - CLOSE_ALLOCATION, - allocationID1, - '0x', - ) + const tx = await mockSubgraphService.callReclaimRewards(rewardsManager.address, CLOSE_ALLOCATION, allocationID1) // Verify event was emitted (don't check exact amount, it depends on rewards calculation) await expect(tx).emit(rewardsManager, 'RewardsReclaimed') @@ -567,12 +615,7 @@ describe('Rewards - Reclaim Addresses', () => { await helpers.mineEpoch(epochManager) // Call reclaimRewards via mock subgraph service - should not emit RewardsReclaimed - const tx = await mockSubgraphService.callReclaimRewards( - rewardsManager.address, - CLOSE_ALLOCATION, - allocationID1, - '0x', - ) + const tx = await mockSubgraphService.callReclaimRewards(rewardsManager.address, CLOSE_ALLOCATION, allocationID1) await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') }) @@ -597,26 +640,18 @@ describe('Rewards - Reclaim Addresses', () => { rewardsManager.address, CLOSE_ALLOCATION, allocationID1, - '0x', ) expect(result).eq(0) - const tx = await mockSubgraphService.callReclaimRewards( - rewardsManager.address, - CLOSE_ALLOCATION, - allocationID1, - '0x', - ) + const tx = await mockSubgraphService.callReclaimRewards(rewardsManager.address, CLOSE_ALLOCATION, allocationID1) await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') }) it('should reject when called by unauthorized address', async function () { // Try to call reclaimRewards directly from indexer1 (not the subgraph service) - // Note: Contract types need to be regenerated after interface changes - // Using manual encoding for now const abiCoder = hre.ethers.utils.defaultAbiCoder - const selector = hre.ethers.utils.id('reclaimRewards(bytes32,address,bytes)').slice(0, 10) - const params = abiCoder.encode(['bytes32', 'address', 'bytes'], [CLOSE_ALLOCATION, allocationID1, '0x']) + const selector = hre.ethers.utils.id('reclaimRewards(bytes32,address)').slice(0, 10) + const params = abiCoder.encode(['bytes32', 'address'], [CLOSE_ALLOCATION, allocationID1]) const data = selector + params.slice(2) const tx = indexer1.sendTransaction({ @@ -626,4 +661,337 @@ describe('Rewards - Reclaim Addresses', () => { await expect(tx).revertedWith('Not a rewards issuer') }) }) + + describe('setDefaultReclaimAddress', function () { + it('should reject if not governor', async function () { + const tx = rewardsManager.connect(indexer1).setDefaultReclaimAddress(reclaimWallet.address) + await expect(tx).revertedWith('Only Controller governor') + }) + + it('should set default reclaim address if governor', async function () { + const tx = rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + await expect(tx) + .emit(rewardsManager, 'DefaultReclaimAddressSet') + .withArgs(constants.AddressZero, reclaimWallet.address) + + // Verify the getter returns the correct value + expect(await rewardsManager.getDefaultReclaimAddress()).eq(reclaimWallet.address) + }) + + it('should allow setting to zero address', async function () { + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setDefaultReclaimAddress(constants.AddressZero) + await expect(tx) + .emit(rewardsManager, 'DefaultReclaimAddressSet') + .withArgs(reclaimWallet.address, constants.AddressZero) + }) + + it('should not emit event when setting same address', async function () { + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + await expect(tx).to.not.emit(rewardsManager, 'DefaultReclaimAddressSet') + }) + }) + + describe('default reclaim address fallback', function () { + beforeEach(async function () { + await setupIndexerAllocation() + // Set governor as the subgraph availability oracle for setDenied calls + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + }) + + it('should use default reclaim address when reason-specific not set', async function () { + // Set default but NOT SUBGRAPH_DENIED specific + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + // Deny the subgraph + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Mine blocks to accrue rewards + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger reclaim via onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should reclaim to default address with SUBGRAPH_DENIED reason + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should prefer reason-specific address over default', async function () { + // Set both default AND SUBGRAPH_DENIED specific + await rewardsManager.connect(governor).setDefaultReclaimAddress(otherWallet.address) + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + + // Deny the subgraph + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Mine blocks to accrue rewards + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + const otherBalanceBefore = await grt.balanceOf(otherWallet.address) + + // Trigger reclaim + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + const otherBalanceAfter = await grt.balanceOf(otherWallet.address) + + // Should go to reason-specific, not default + expect(balanceAfter).gt(balanceBefore) + expect(otherBalanceAfter).eq(otherBalanceBefore) + }) + }) + + describe('reclaim NO_SIGNAL - zero total signal', function () { + it('should reclaim when no signal and NO_SIGNAL address set', async function () { + // Set reclaim address for NO_SIGNAL + await rewardsManager.connect(governor).setReclaimAddress(NO_SIGNAL, reclaimWallet.address) + + // Don't create any signal - just mine blocks + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger updateAccRewardsPerSignal (called internally when signal changes, or directly) + const tx = rewardsManager.connect(governor).updateAccRewardsPerSignal() + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should drop rewards when no signal and no reclaim address', async function () { + // Don't set any reclaim address - just mine blocks + await helpers.mine(5) + + // Trigger updateAccRewardsPerSignal + const tx = rewardsManager.connect(governor).updateAccRewardsPerSignal() + + // Should not emit RewardsReclaimed (dropped) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should use default reclaim address for NO_SIGNAL when specific not set', async function () { + // Set default but NOT NO_SIGNAL specific + await rewardsManager.connect(governor).setDefaultReclaimAddress(reclaimWallet.address) + + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + const tx = rewardsManager.connect(governor).updateAccRewardsPerSignal() + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + }) + + describe('reclaim NO_ALLOCATION - signal but no allocations', function () { + it('should reclaim when signal exists but no allocations and NO_ALLOCATION address set', async function () { + // Set reclaim address for NO_ALLOCATION + await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATION, reclaimWallet.address) + + // Create signal but NO allocation + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Mine blocks to accrue rewards + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger onSubgraphAllocationUpdate - will see signal but no allocations + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should drop rewards when no allocations and no reclaim address', async function () { + // Create signal but NO allocation, and don't set reclaim address + const signalled1 = toGRT('1500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + await helpers.mine(5) + + // Trigger onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should not emit RewardsReclaimed (dropped) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + }) + + describe('reclaim BELOW_MINIMUM_SIGNAL', function () { + const MINIMUM_SIGNAL = toGRT('1000') + + beforeEach(async function () { + // Set minimum signal threshold + await rewardsManager.connect(governor).setMinimumSubgraphSignal(MINIMUM_SIGNAL) + }) + + it('should reclaim when signal below minimum and BELOW_MINIMUM_SIGNAL address set', async function () { + // Set reclaim address for BELOW_MINIMUM_SIGNAL + await rewardsManager.connect(governor).setReclaimAddress(BELOW_MINIMUM_SIGNAL, reclaimWallet.address) + + // Create signal BELOW minimum (minimum is 1000, we signal 500) + const signalled1 = toGRT('500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Mine blocks + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + + it('should not reclaim when signal at or above minimum', async function () { + // Set reclaim address + await rewardsManager.connect(governor).setReclaimAddress(BELOW_MINIMUM_SIGNAL, reclaimWallet.address) + + // Create signal AT minimum + const signalled1 = MINIMUM_SIGNAL + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Also need an allocation for rewards to accumulate normally + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + await helpers.mine(5) + + // Trigger onSubgraphAllocationUpdate + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should NOT emit RewardsReclaimed for BELOW_MINIMUM_SIGNAL + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should drop rewards when below minimum and no reclaim address', async function () { + // Don't set reclaim address + // Create signal BELOW minimum + const signalled1 = toGRT('500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + await helpers.mine(5) + + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should not emit RewardsReclaimed (dropped) + await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + }) + + it('should use BELOW_MINIMUM_SIGNAL when denied but SUBGRAPH_DENIED address not configured', async function () { + // This tests line 574: the branch where subgraph is denied but reclaim address is zero, + // so it falls back to BELOW_MINIMUM_SIGNAL + + // Set BELOW_MINIMUM_SIGNAL address but NOT SUBGRAPH_DENIED + await rewardsManager.connect(governor).setReclaimAddress(BELOW_MINIMUM_SIGNAL, reclaimWallet.address) + + // Setup denylist + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Create signal BELOW minimum (minimum is 1000, we signal 500) + const signalled1 = toGRT('500') + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Mine blocks + await helpers.mine(5) + + const balanceBefore = await grt.balanceOf(reclaimWallet.address) + + // Trigger onSubgraphAllocationUpdate + // Subgraph is denied but no SUBGRAPH_DENIED address, so should fall back to BELOW_MINIMUM_SIGNAL + const tx = rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) + + // Should reclaim to BELOW_MINIMUM_SIGNAL address + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + const balanceAfter = await grt.balanceOf(reclaimWallet.address) + expect(balanceAfter).gt(balanceBefore) + }) + }) + + describe('dual denial - SUBGRAPH_DENIED takes precedence when configured', function () { + it('should reclaim to SUBGRAPH_DENIED when both conditions true and SUBGRAPH_DENIED address configured (pre-denial allocation)', async function () { + // This tests line 747-748: when both denied and ineligible, and SUBGRAPH_DENIED IS configured + + // Setup BOTH reclaim addresses + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, reclaimWallet.address) + await rewardsManager.connect(governor).setReclaimAddress(INDEXER_INELIGIBLE, otherWallet.address) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + + // Setup eligibility oracle that denies + const MockRewardsEligibilityOracleFactory = await hre.ethers.getContractFactory( + 'contracts/tests/MockRewardsEligibilityOracle.sol:MockRewardsEligibilityOracle', + ) + const mockOracle = await MockRewardsEligibilityOracleFactory.deploy(false) // Deny + await mockOracle.deployed() + await rewardsManager.connect(governor).setRewardsEligibilityOracle(mockOracle.address) + + // Align with the epoch boundary + await helpers.mineEpoch(epochManager) + + // Create allocation FIRST (before denial) + await setupIndexerAllocation() + + // Mine blocks to accrue rewards + await helpers.mineEpoch(epochManager) + + // NOW deny the subgraph (after allocation exists) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID1, true) + + // Check balances before + const subgraphDeniedBalanceBefore = await grt.balanceOf(reclaimWallet.address) + const indexerIneligibleBalanceBefore = await grt.balanceOf(otherWallet.address) + + // Close allocation - pre-denial rewards flow through _deniedRewards + // Both conditions are true, SUBGRAPH_DENIED IS configured, so it should use SUBGRAPH_DENIED + const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + + // RewardsDenied IS emitted + await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) + // RewardsDeniedDueToEligibility IS emitted + await expect(tx).emit(rewardsManager, 'RewardsDeniedDueToEligibility') + // RewardsReclaimed should emit with SUBGRAPH_DENIED reason + await expect(tx).emit(rewardsManager, 'RewardsReclaimed') + + // SUBGRAPH_DENIED wallet should receive rewards (not INDEXER_INELIGIBLE) + const subgraphDeniedBalanceAfter = await grt.balanceOf(reclaimWallet.address) + const indexerIneligibleBalanceAfter = await grt.balanceOf(otherWallet.address) + + expect(subgraphDeniedBalanceAfter.sub(subgraphDeniedBalanceBefore)).gt(0) + expect(indexerIneligibleBalanceAfter.sub(indexerIneligibleBalanceBefore)).eq(0) + }) + }) }) diff --git a/packages/contracts-test/tests/unit/rewards/rewards.test.ts b/packages/contracts-test/tests/unit/rewards/rewards.test.ts index 97d11ae01..bdf13a83d 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards.test.ts @@ -479,7 +479,8 @@ describe('Rewards', () => { await helpers.mine(ISSUANCE_RATE_PERIODS) // Prepare expected results - const expectedSubgraphRewards = toGRT('1400') // 7 blocks since signaling to when we do getAccRewardsForSubgraph + // Note: rewards from signal to allocation (2 blocks) are reclaimed since no allocations exist yet + const expectedSubgraphRewards = toGRT('1000') // 5 blocks since allocation to when we do getAccRewardsForSubgraph const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens // Update @@ -1313,12 +1314,12 @@ describe('Rewards', () => { // signal in two subgraphs in the same block const subgraphs = [subgraphDeploymentID1, subgraphDeploymentID2] + await hre.network.provider.send('evm_setAutomine', [false]) for (const sub of subgraphs) { await curation.connect(curator1).mint(sub, toGRT('1500'), 0) } - - // snapshot block before any accrual (we substract 1 because accrual starts after the first mint happens) - const b1 = await epochManager.blockNum().then((x) => x.toNumber() - 1) + await hre.network.provider.send('evm_mine') + await hre.network.provider.send('evm_setAutomine', [true]) // allocate const tokensToAllocate = toGRT('12500') @@ -1338,6 +1339,9 @@ describe('Rewards', () => { .then((tx) => tx.data), ]) + // snapshot block after allocation (rewards before allocation were reclaimed for subgraph1) + const b1 = await epochManager.blockNum().then((x) => x.toNumber()) + // move time fwd await helpers.mineEpoch(epochManager) @@ -1351,8 +1355,12 @@ describe('Rewards', () => { const accrual = await getRewardsAccrual(subgraphs) const b2 = await epochManager.blockNum().then((x) => x.toNumber()) - // round comparison because there is a small precision error due to dividing and accrual per signal - expect(toRound(accrual.all)).eq(toRound(ISSUANCE_PER_BLOCK.mul(b2 - b1))) + // Only check subgraph1 (with allocation) - subgraph2 has no allocation so its rewards + // are calculated from signal time, not from allocation time + // Each subgraph gets half the issuance (equal signal) + // Small tolerance for fixed-point arithmetic rounding + const expectedSg1Rewards = ISSUANCE_PER_BLOCK.div(2).mul(b2 - b1) + expect(toRound(accrual.sg1.mul(100).div(expectedSg1Rewards))).eq(toRound(BigNumber.from(100))) }) }) }) diff --git a/packages/contracts/contracts/tests/MockSubgraphService.sol b/packages/contracts/contracts/tests/MockSubgraphService.sol index 75049b399..cdee9ab6a 100644 --- a/packages/contracts/contracts/tests/MockSubgraphService.sol +++ b/packages/contracts/contracts/tests/MockSubgraphService.sol @@ -108,20 +108,17 @@ contract MockSubgraphService is IRewardsIssuer { * @param rewardsManager Address of the RewardsManager contract * @param reason Reason identifier for reclaiming rewards * @param allocationId The allocation ID - * @param contextData Additional context data for the reclaim * @return Amount of rewards reclaimed */ function callReclaimRewards( address rewardsManager, bytes32 reason, - address allocationId, - bytes calldata contextData + address allocationId ) external returns (uint256) { // Call reclaimRewards on the RewardsManager // solhint-disable-next-line avoid-low-level-calls (bool success, bytes memory data) = rewardsManager.call( - // solhint-disable-next-line gas-small-strings - abi.encodeWithSignature("reclaimRewards(bytes32,address,bytes)", reason, allocationId, contextData) + abi.encodeWithSignature("reclaimRewards(bytes32,address)", reason, allocationId) ); require(success, "reclaimRewards call failed"); return abi.decode(data, (uint256)); diff --git a/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol b/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol index dd9b10dc6..b9d4df5e2 100644 --- a/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol +++ b/packages/subgraph-service/test/unit/mocks/MockRewardsManager.sol @@ -51,7 +51,9 @@ contract MockRewardsManager is IRewardsManager { function setReclaimAddress(bytes32, address) external {} - function reclaimRewards(bytes32, address _allocationId, bytes calldata) external view returns (uint256) { + function setDefaultReclaimAddress(address) external {} + + function reclaimRewards(bytes32, address _allocationId) external view returns (uint256) { address rewardsIssuer = msg.sender; (bool isActive, , , uint256 tokens, uint256 accRewardsPerAllocatedToken, ) = IRewardsIssuer(rewardsIssuer) .getAllocationData(_allocationId); @@ -78,6 +80,10 @@ contract MockRewardsManager is IRewardsManager { return address(0); } + function getDefaultReclaimAddress() external pure returns (address) { + return address(0); + } + function getRewardsEligibilityOracle() external pure returns (IRewardsEligibility) { return IRewardsEligibility(address(0)); } From 2324bce9a012a830e2fbdcc88dadd7a17a0f3709 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 04:24:13 +0000 Subject: [PATCH 36/43] fixup! feat: reclaim for no signal in updateAccRewardsPerSignal --- packages/contracts/contracts/rewards/RewardsManager.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 9e81ff67b..1f3dfb11a 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -499,8 +499,9 @@ contract RewardsManager is * (if NO_SIGNAL reclaim address is configured) rather than being lost. */ function updateAccRewardsPerSignal() public override returns (uint256) { + if (accRewardsPerSignalLastBlockUpdated == block.number) return accRewardsPerSignal; + (uint256 claimablePerSignal, uint256 unclaimableTokens) = _getNewRewardsPerSignal(); - if (claimablePerSignal == 0 && unclaimableTokens == 0) return accRewardsPerSignal; if (0 < unclaimableTokens) _reclaimRewards(RewardsCondition.NO_SIGNAL, unclaimableTokens, address(0), address(0), bytes32(0)); From bbe4a9ffdd56e7c00127c0ca8a0c8313dc897355 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 04:32:08 +0000 Subject: [PATCH 37/43] test: zero issuance timestamp update Add tests to verify timestamp is always updated when transitioning between zero and non-zero issuance rates, preventing over-issuance of rewards for periods when issuance was disabled. --- .../tests/unit/rewards/rewards-config.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts index b9cbf4dfe..3e510e1c1 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-config.test.ts @@ -5,6 +5,7 @@ import { RewardsManager } from '@graphprotocol/contracts' import { GraphNetworkContracts, helpers, randomHexBytes, toBN, toGRT } from '@graphprotocol/sdk' import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' +import { BigNumber } from 'ethers' import hre from 'hardhat' import { NetworkFixture } from '../lib/fixtures' @@ -89,6 +90,92 @@ describe('Rewards - Configuration', () => { expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await helpers.latestBlock()) }) + + it('should update timestamp when transitioning from zero to non-zero issuance', async function () { + // Add some signal so rewards can be calculated + await curation.connect(curator1).mint(subgraphDeploymentID1, toGRT('10000'), 0) + + // Mine some blocks with rewards active + await helpers.mine(10) + + // Set issuance to zero - this updates timestamp correctly + await rewardsManager.connect(governor).setIssuancePerBlock(0) + const blockAfterZeroIssuance = await helpers.latestBlock() + + // Verify timestamp was updated + const timestampAfterZero = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + expect(timestampAfterZero).to.equal(blockAfterZeroIssuance) + + // Mine blocks during zero issuance period + await helpers.mine(10) + + // Set issuance back to non-zero + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + const blockAfterRestore = await helpers.latestBlock() + + // Timestamp should be updated when transitioning from zero issuance + const timestampAfterRestore = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + expect(timestampAfterRestore).to.equal( + blockAfterRestore, + 'Timestamp should be updated when transitioning from zero issuance', + ) + }) + + it('should not over-issue rewards after zero issuance period', async function () { + // Add signal + await curation.connect(curator1).mint(subgraphDeploymentID1, toGRT('10000'), 0) + + // Get signalled tokens for calculation + const signalledTokens = await grt.balanceOf(curation.address) + + // Mine some blocks with rewards active + await helpers.mine(10) + + // Capture rewards and timestamp before zero issuance period + await rewardsManager.connect(governor).updateAccRewardsPerSignal() + const rewardsAfterFirstPeriod = await rewardsManager.accRewardsPerSignal() + + // Set issuance to zero + await rewardsManager.connect(governor).setIssuancePerBlock(0) + const timestampAfterZeroSet = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + + // Mine blocks during zero issuance - NO rewards should accumulate + await helpers.mine(10) + + // Restore issuance - record the block when non-zero issuance starts + await rewardsManager.connect(governor).setIssuancePerBlock(ISSUANCE_PER_BLOCK) + const timestampAfterRestore = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + + // Mine more blocks with rewards active + await helpers.mine(10) + + // Update and check final rewards + await rewardsManager.connect(governor).updateAccRewardsPerSignal() + const finalRewards = await rewardsManager.accRewardsPerSignal() + const finalTimestamp = await rewardsManager.accRewardsPerSignalLastBlockUpdated() + + // The actual rewards increase from first period to final + const rewardsIncrease = finalRewards.sub(rewardsAfterFirstPeriod) + + // Calculate expected rewards based on ACTUAL blocks where issuance was active + const FIXED_POINT_SCALING_FACTOR = BigNumber.from(10).pow(18) + const activeBlocksAfterRestore = finalTimestamp.sub(timestampAfterRestore) + const expectedIncrease = ISSUANCE_PER_BLOCK.mul(activeBlocksAfterRestore) + .mul(FIXED_POINT_SCALING_FACTOR) + .div(signalledTokens) + + // Key assertion: timestamp should advance during zero issuance period + expect(timestampAfterRestore.toNumber()).to.be.greaterThan( + timestampAfterZeroSet.toNumber(), + 'Timestamp should advance when setting non-zero issuance', + ) + + // Allow some tolerance for block timing (1 block variance) + const tolerance = ISSUANCE_PER_BLOCK.mul(1).mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens) + + // Rewards should match the active period only + expect(rewardsIncrease).to.be.closeTo(expectedIncrease, tolerance, 'Rewards should match active period only') + }) }) describe('subgraph availability service', function () { From a0122826fe8fbcbad0fe865a5b3e3fb1a0b6d25d Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:00:43 +0000 Subject: [PATCH 38/43] refactor: cache storage variables in _onSubgraphSignalUpdate Avoid redundant SLOADs by using local variables for accRewardsForSubgraph and the already-cached accRewardsPerAllocatedToken. --- packages/contracts/contracts/rewards/RewardsManager.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 1f3dfb11a..f9fd511b2 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -576,12 +576,13 @@ contract RewardsManager is return accRewardsPerAllocatedToken; } - subgraph.accRewardsForSubgraph = subgraph.accRewardsForSubgraph.add(newRewards); - accRewardsPerAllocatedToken = subgraph.accRewardsPerAllocatedToken.add( + uint256 accRewardsForSubgraph = subgraph.accRewardsForSubgraph.add(newRewards); + accRewardsPerAllocatedToken = accRewardsPerAllocatedToken.add( newRewards.mul(FIXED_POINT_SCALING_FACTOR).div(subgraphAllocatedTokens) ); + subgraph.accRewardsForSubgraph = accRewardsForSubgraph; subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; - subgraph.accRewardsForSubgraphSnapshot = subgraph.accRewardsForSubgraph; + subgraph.accRewardsForSubgraphSnapshot = accRewardsForSubgraph; } /// @inheritdoc IRewardsManager From ea399396f0b37a3dd8c0b60cbf487faeb8a9de94 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:17:21 +0000 Subject: [PATCH 39/43] refactor: cache storage in updateAccRewardsPerSignal and onSubgraphSignalUpdate Avoid redundant SLOADs by using local variables instead of re-reading storage after writes. --- .../contracts/contracts/rewards/RewardsManager.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index f9fd511b2..99a8e34f6 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -506,9 +506,10 @@ contract RewardsManager is if (0 < unclaimableTokens) _reclaimRewards(RewardsCondition.NO_SIGNAL, unclaimableTokens, address(0), address(0), bytes32(0)); - accRewardsPerSignal = accRewardsPerSignal.add(claimablePerSignal); + uint256 newAccRewardsPerSignal = accRewardsPerSignal.add(claimablePerSignal); + accRewardsPerSignal = newAccRewardsPerSignal; accRewardsPerSignalLastBlockUpdated = block.number; - return accRewardsPerSignal; + return newAccRewardsPerSignal; } /** @@ -522,9 +523,10 @@ contract RewardsManager is // Updates the accumulated rewards for a subgraph Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; - subgraph.accRewardsForSubgraph = getAccRewardsForSubgraph(_subgraphDeploymentID); + uint256 accRewardsForSubgraph = getAccRewardsForSubgraph(_subgraphDeploymentID); + subgraph.accRewardsForSubgraph = accRewardsForSubgraph; subgraph.accRewardsPerSignalSnapshot = accRewardsPerSignal; - return subgraph.accRewardsForSubgraph; + return accRewardsForSubgraph; } /** From 025410f39966787885228050d07dad7f312f1106 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Wed, 4 Feb 2026 13:55:25 +0000 Subject: [PATCH 40/43] fix: consistent reward reclaim for non-claimable subgraphs in both hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, rewards were silently dropped when signal/allocation hooks were called on non-claimable subgraphs, causing permanent loss. Both hooks now consistently reclaim rewards for SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, and NO_ALLOCATION conditions. Hook Changes (onSubgraphSignalUpdate & onSubgraphAllocationUpdate): - Add reward reclaim logic for all non-claimable conditions - When not claimable: reclaim immediately, accRewardsForSubgraph NOT updated - When claimable: add to accRewardsForSubgraph, increase accRewardsPerAllocatedToken - Centralize condition checking in _getSubgraphRewardsState helper - Implement fallback chain: when multiple conditions apply, prefer ones with configured reclaim addresses (DENIED → BELOW_MINIMUM → NO_ALLOCATION) - Use updateAccRewardsPerSignal for same-block caching and prompt NO_SIGNAL reclaim - Add early return when both snapshots already current (gas optimization) - Skip storage writes when accumulators unchanged (gas optimization) View Function Changes: - getAccRewardsForSubgraph: exclude new rewards for all reclaim conditions - getAccRewardsPerAllocatedToken: only include rewards when claimable - getRewards: reflect deterministic exclusions (denied, below minimum, no allocations) - Note: indexer eligibility is NOT checked by view functions - it can change independently and is only validated at claim time Implementation Details: - _getSubgraphRewardsState: centralized helper returns condition + allocated tokens - Preserves accRewardsPerAllocatedToken value when subgraphAllocatedTokens=0 (returns stored value instead of 0, maintains accumulator for when allocations return) - Track undistributed rewards (accRewardsForSubgraph - snapshot) for proper reclaim Testing: - Add rewards-signal-allocation-update.test.ts: comprehensive tests for same-block signal/allocation updates and edge cases where per-signal delta=0 - Update existing tests for new accumulator behavior and reclaim conditions Documentation: - Add RewardConditions.md: complete reference table of all conditions, handling layers, and reward outcomes - Remove DeniedSubgraphRewardsAnalysis.md (content consolidated) - Update RewardAccountingSafety.md accumulator behavior description --- docs/DeniedSubgraphRewardsAnalysis.md | 65 -- docs/RewardAccountingSafety.md | 61 +- docs/RewardConditions.md | 222 +++++++ .../unit/rewards/rewards-calculations.test.ts | 84 ++- .../rewards-eligibility-oracle.test.ts | 152 ++++- .../unit/rewards/rewards-reclaim.test.ts | 134 ++++- .../rewards-signal-allocation-update.test.ts | 560 ++++++++++++++++++ .../rewards/rewards-subgraph-service.test.ts | 5 +- .../tests/unit/rewards/rewards.test.ts | 99 +++- .../contracts/rewards/RewardsManager.sol | 281 ++++----- .../contracts/rewards/IRewardsManager.sol | 36 +- .../contracts/rewards/RewardsCondition.sol | 67 +-- 12 files changed, 1363 insertions(+), 403 deletions(-) delete mode 100644 docs/DeniedSubgraphRewardsAnalysis.md create mode 100644 docs/RewardConditions.md create mode 100644 packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts diff --git a/docs/DeniedSubgraphRewardsAnalysis.md b/docs/DeniedSubgraphRewardsAnalysis.md deleted file mode 100644 index b594433bd..000000000 --- a/docs/DeniedSubgraphRewardsAnalysis.md +++ /dev/null @@ -1,65 +0,0 @@ -# Subgraph Denial: Reward Behaviour - -## Overview - -When a subgraph is denied, indexers cannot claim rewards for the denial period, but pre-denial rewards remain claimable after the subgraph is undenied. - -## Reward Disposition by Period - -| Period | Rewards | Disposition | -| ----------------- | ----------------------------- | -------------------------------------------------------- | -| **Pre-denial** | Rewards accrued before denial | Claimable after undeny | -| **During denial** | Rewards issued while denied | Reclaimed to protocol (or dropped if no reclaim address) | -| **Post-undeny** | Rewards accrued after undeny | Claimable normally | - -## How Denial Affects Allocations - -### Existing Allocations (created before denial) - -- Pre-denial rewards are preserved in the allocation's snapshot -- Cannot claim while denied (returns 0) -- After undeny, can claim pre-denial rewards -- Denial-period rewards are not available (reclaimed at protocol level) - -### New Allocations (created while denied) - -- Created with current frozen reward state as baseline -- Only earn rewards after subgraph is undenied -- Cannot earn backdated rewards for denial period - -### POI Presentation While Denied - -- Indexers can (and should) continue presenting POIs -- Prevents allocations from becoming stale -- Returns 0 rewards but maintains allocation health - -## Two-Layer Denial System - -### Hard Deny (RewardsManager) - -- Freezes `accRewardsPerAllocatedToken` - no new rewards credited to allocations -- Reclaims ongoing issuance to configured reclaim address -- Operates at subgraph level (affects all allocations) - -### Soft Deny (AllocationManager) - -- Skips `takeRewards()` call when subgraph is denied -- Preserves allocation state for future claiming -- Returns early without modifying allocation snapshots - -Together: hard deny prevents new rewards accumulating; soft deny preserves pre-denial rewards. - -## Edge Cases - -| Scenario | Behavior | -| ---------------------------------- | ------------------------------------------------------------------------------- | -| All allocations close while denied | Frozen reward state preserved; new allocations after undeny use frozen baseline | -| Redundant deny call | No state change; original deny block preserved | -| Redundant undeny call | No state change | -| Zero reclaim address | Denial-period rewards dropped (never minted) | - -## Safety Guarantees - -1. **No double-counting**: Snapshot mechanism ensures each reward period is counted once -2. **No lost pre-denial rewards**: Frozen state preserves indexer's earned rewards -3. **Idempotent operations**: Redundant deny/undeny calls are safe no-ops diff --git a/docs/RewardAccountingSafety.md b/docs/RewardAccountingSafety.md index 2a4144c64..ed554c9f2 100644 --- a/docs/RewardAccountingSafety.md +++ b/docs/RewardAccountingSafety.md @@ -30,10 +30,10 @@ Each level uses the same pattern: an accumulator increases over time, and partic Snapshots prevent double-counting by recording each participant's starting point: -| Component | Accumulator | Snapshot | Prevents | -| ---------- | ----------------------------- | ----------------------------- | --------------------------------------------- | -| Subgraph | `accRewardsPerSignal` | `accRewardsPerSignalSnapshot` | Same rewards credited to multiple subgraphs | -| Allocation | `accRewardsPerAllocatedToken` | Stored in allocation state | Same rewards claimed twice by same allocation | +| Component | Accumulator | Snapshot | Prevents | +| ---------- | ----------------------------- | ----------------------------- | ----------------------------------------------- | +| Subgraph | `accRewardsPerSignal` | `accRewardsPerSignalSnapshot` | Same subgraph counting same reward period twice | +| Allocation | `accRewardsPerAllocatedToken` | Stored in allocation state | Same allocation claiming same rewards twice | After any update, snapshot = current accumulator. Next calculation starts from zero delta. @@ -41,9 +41,15 @@ After any update, snapshot = current accumulator. Next calculation starts from z ### 1. Monotonic Accumulators -`accRewardsPerSignal` and `accRewardsPerAllocatedToken` only increase (never decrease). +All accumulators only increase (never decrease): -**Exception**: `accRewardsPerAllocatedToken` freezes (stops increasing) when subgraph is denied or below minimum signal. It never decreases. +| Accumulator | Behavior When Not Claimable | +| ----------------------------- | ----------------------------------------------------------- | +| `accRewardsPerSignal` | Always increases | +| `accRewardsForSubgraph` | Stops increasing (rewards reclaimed, not accumulated) | +| `accRewardsPerAllocatedToken` | Stops increasing (rewards reclaimed instead of distributed) | + +When a subgraph is not claimable (denied or below minimum signal), rewards are reclaimed directly without updating `accRewardsForSubgraph`. This means `accRewardsForSubgraph` only tracks rewards that are actually distributed to allocations. **Why it matters**: Decreasing accumulators would cause negative reward calculations or allow re-claiming past rewards. @@ -86,7 +92,7 @@ _allocations.snapshotRewards(..., onSubgraphAllocationUpdate()); // ③ Updates ```solidity // In AllocationManager._presentPoi(): -rewards = takeRewards(_allocationId); // ① Mints rewards +rewards = takeRewards(_allocationId); // ① Mints rewards snapshotRewards(_allocationId, onSubgraphAllocationUpdate(...)); // ② Updates snapshot clearPendingRewards(_allocationId); // ③ Clears pending ``` @@ -101,27 +107,27 @@ clearPendingRewards(_allocationId); // ③ Clears p Every reward path that cannot reach an allocation has a reclaim handler: -| Condition | When Triggered | Reclaim Reason | -| -------------------- | ------------------------------------------------------- | ------------------------ | -| No global signal | `updateAccRewardsPerSignal()` with signalledTokens = 0 | `NO_SIGNAL` | -| Subgraph denied | `onSubgraphAllocationUpdate()` | `SUBGRAPH_DENIED` | -| Below minimum signal | `onSubgraphAllocationUpdate()` | `BELOW_MINIMUM_SIGNAL` | -| No allocations | `onSubgraphAllocationUpdate()` with allocatedTokens = 0 | `NO_ALLOCATION` | -| Indexer ineligible | `takeRewards()` | `INDEXER_INELIGIBLE` | -| Stale/zero POI | `_presentPoi()` | `STALE_POI` / `ZERO_POI` | -| Allocation close | `_closeAllocation()` | `CLOSE_ALLOCATION` | +| Condition | When Triggered | Reclaim Reason | +| -------------------- | ------------------------------------------------------------ | ------------------------ | +| No global signal | `updateAccRewardsPerSignal()` with signalledTokens = 0 | `NO_SIGNAL` | +| Subgraph denied | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `SUBGRAPH_DENIED` | +| Below minimum signal | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `BELOW_MINIMUM_SIGNAL` | +| No allocations | `onSubgraphSignalUpdate()` or `onSubgraphAllocationUpdate()` | `NO_ALLOCATION` | +| Indexer ineligible | `takeRewards()` | `INDEXER_INELIGIBLE` | +| Stale/zero POI | `_presentPoi()` | `STALE_POI` / `ZERO_POI` | +| Allocation close | `_closeAllocation()` | `CLOSE_ALLOCATION` | **Reclaim priority**: reason-specific address → defaultReclaimAddress → dropped (no mint) ## Potential Failure Modes (Mitigated) -| Failure Mode | How Prevented | -| ---------------------------- | -------------------------------------------------------------------------------- | -| Double-mint same rewards | Snapshot updated after every claim; same-block calls return ~0 | -| Rewards stuck in accumulator | NO_ALLOCATION reclaim before allocation creation | -| Gap period loss | `_getAllocationData` calls `onSubgraphAllocationUpdate` before allocation exists | -| Denial-period accumulation | `accRewardsPerAllocatedToken` freezes; new rewards reclaimed | -| Signal change mid-period | `onSubgraphSignalUpdate` hook called before signal changes | +| Failure Mode | How Prevented | +| ---------------------------- | ------------------------------------------------------------------------------------ | +| Double-mint same rewards | Snapshot updated after every claim; same-block calls return ~0 | +| Rewards stuck in accumulator | NO_ALLOCATION reclaim before allocation creation | +| Gap period loss | `_getAllocationData` calls `onSubgraphAllocationUpdate` before allocation exists | +| Denial-period accumulation | `accRewardsForSubgraph` tracks; `accRewardsPerAllocatedToken` frozen; diff reclaimed | +| Signal change mid-period | `onSubgraphSignalUpdate` hook called before signal changes | ## Division of Responsibility @@ -139,12 +145,11 @@ RewardsManager and issuers share responsibility for correct reward accounting: - This preserves allocation state so rewards remain claimable after conditions change - RM cannot know issuer intent, so issuers must decide when to attempt claims -**Example - Subgraph Denial**: +**Example - Subgraph Denial** (see [RewardConditions.md](./RewardConditions.md#subgraph_denied) for full details): -1. RM freezes `accRewardsPerAllocatedToken` and reclaims new subgraph-level rewards -2. AM detects denial and skips `takeRewards()` call entirely (soft deny) -3. Pre-denial rewards preserved in allocation snapshot -4. After undeny, AM can claim the preserved rewards +- RM: Reclaims new rewards; freezes `accRewardsPerAllocatedToken` +- AM: Defers claim; preserves pre-denial rewards in allocation snapshot +- After undeny: AM can claim the preserved pre-denial rewards ## Issuer Requirements diff --git a/docs/RewardConditions.md b/docs/RewardConditions.md new file mode 100644 index 000000000..606a1881e --- /dev/null +++ b/docs/RewardConditions.md @@ -0,0 +1,222 @@ +# Reward Conditions: Collection and Reclaim Reference + +Quick reference for all reward conditions and how they are handled across RewardsManager and AllocationManager. + +## Summary Table + +| Condition | Identifier | Handled By | Action | Rewards Outcome | +| ---------------------- | ----------------------------------- | ----------------- | ------------------------- | ------------------------------------- | +| `NONE` | `bytes32(0)` | — | Normal path | Claimed by indexer | +| `NO_SIGNAL` | `keccak256("NO_SIGNAL")` | RewardsManager | Reclaim | To reclaim address | +| `SUBGRAPH_DENIED` | `keccak256("SUBGRAPH_DENIED")` | Both | Reclaim (RM) / Defer (AM) | New: reclaimed; Pre-denial: preserved | +| `BELOW_MINIMUM_SIGNAL` | `keccak256("BELOW_MINIMUM_SIGNAL")` | RewardsManager | Reclaim | To reclaim address | +| `NO_ALLOCATION` | `keccak256("NO_ALLOCATION")` | RewardsManager | Reclaim | To reclaim address | +| `INDEXER_INELIGIBLE` | `keccak256("INDEXER_INELIGIBLE")` | RewardsManager | Reclaim | To reclaim address | +| `STALE_POI` | `keccak256("STALE_POI")` | AllocationManager | Reclaim | To reclaim address | +| `ZERO_POI` | `keccak256("ZERO_POI")` | AllocationManager | Reclaim | To reclaim address | +| `ALLOCATION_TOO_YOUNG` | `keccak256("ALLOCATION_TOO_YOUNG")` | AllocationManager | Defer | Preserved for later | +| `CLOSE_ALLOCATION` | `keccak256("CLOSE_ALLOCATION")` | AllocationManager | Reclaim | To reclaim address | + +## Reward Distribution Levels + +Rewards flow through three levels, with reclaim possible at each: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Level 0: Global Issuance │ +│ ───────────────────────────────────────────────────────────────── │ +│ updateAccRewardsPerSignal() │ +│ │ +│ Reclaim: NO_SIGNAL (when total signalled tokens = 0) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ proportional to signal +┌─────────────────────────────────────────────────────────────────────┐ +│ Level 1: Subgraph │ +│ ───────────────────────────────────────────────────────────────── │ +│ onSubgraphSignalUpdate() / onSubgraphAllocationUpdate() │ +│ │ +│ Reclaim: SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, NO_ALLOCATION │ +│ │ +│ Behavior: │ +│ - accRewardsForSubgraph only increases when claimable │ +│ - accRewardsPerAllocatedToken only increases when claimable │ +│ - Non-claimable rewards are reclaimed immediately, not stored │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ proportional to allocated tokens +┌─────────────────────────────────────────────────────────────────────┐ +│ Level 2: Allocation │ +│ ───────────────────────────────────────────────────────────────── │ +│ takeRewards() / reclaimRewards() / _presentPoi() │ +│ │ +│ Reclaim: INDEXER_INELIGIBLE (at takeRewards) │ +│ STALE_POI, ZERO_POI, CLOSE_ALLOCATION (at _presentPoi) │ +│ │ +│ Defer: SUBGRAPH_DENIED, ALLOCATION_TOO_YOUNG (preserves state) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Condition Details + +### Global Level (RewardsManager.updateAccRewardsPerSignal) + +#### NO_SIGNAL + +- **Trigger**: Total signalled tokens across all subgraphs = 0 +- **Effect**: Issuance cannot be distributed proportionally to signal +- **Handling**: Reclaim to configured address (or drop if unconfigured) + +### Subgraph Level (RewardsManager.onSubgraphAllocationUpdate) + +#### SUBGRAPH_DENIED + +- **Trigger**: `isDenied(subgraphDeploymentId)` returns true +- **Effect**: `accRewardsPerAllocatedToken` stops increasing +- **Handling**: New rewards reclaimed; pre-denial rewards preserved in allocation snapshots +- **Note**: If no SUBGRAPH_DENIED reclaim address AND signal < minimum, reclaims as BELOW_MINIMUM_SIGNAL instead + +**Reward disposition by period:** + +| Period | Disposition | +| ------------- | -------------------------------------------------------- | +| Pre-denial | Claimable after undeny | +| During denial | Reclaimed to protocol (or dropped if no reclaim address) | +| Post-undeny | Claimable normally | + +**Effect on allocations:** + +- _Existing allocations_: Pre-denial rewards preserved; cannot claim while denied; claimable after undeny +- _New allocations (created while denied)_: Start with frozen baseline; only earn rewards after undeny +- _POI presentation_: Indexers should continue presenting POIs to prevent staleness (returns 0 but maintains allocation health) + +**Edge cases:** + +| Scenario | Behavior | +| ---------------------------------- | --------------------------------------------------------- | +| All allocations close while denied | Frozen state preserved; new allocations use that baseline | +| Redundant deny/undeny calls | No state change (idempotent) | +| Zero reclaim address | Denial-period rewards dropped (never minted) | + +#### BELOW_MINIMUM_SIGNAL + +- **Trigger**: Subgraph signal < `minimumSubgraphSignal` (and not denied) +- **Effect**: `accRewardsPerAllocatedToken` stops increasing +- **Handling**: Rewards reclaimed to configured address + +#### NO_ALLOCATION + +- **Trigger**: Subgraph has signal but zero allocated tokens +- **Effect**: Rewards cannot be distributed to allocations +- **Handling**: Reclaim to configured address +- **Note**: Triggered when condition is NONE but no allocations exist, or when original condition has no reclaim address + +### Allocation Level (RewardsManager.takeRewards) + +#### INDEXER_INELIGIBLE + +- **Trigger**: `eligibilityOracle.isEligible(indexer)` returns false at claim time +- **Effect**: Indexer cannot claim earned rewards +- **Handling**: Rewards reclaimed to configured address +- **Precedence**: SUBGRAPH_DENIED takes precedence if both apply + +### Allocation Level (AllocationManager.\_presentPoi) + +Conditions checked in order (first match wins): + +#### STALE_POI + +- **Trigger**: `maxPOIStaleness` < Time since last POI +- **Effect**: Allocation locked out due to inactivity +- **Handling**: Rewards reclaimed; allocation snapshotted; pending cleared + +#### ZERO_POI + +- **Trigger**: POI submitted is `bytes32(0)` +- **Effect**: No proof of indexing work provided +- **Handling**: Rewards reclaimed; allocation snapshotted; pending cleared + +#### ALLOCATION_TOO_YOUNG + +- **Trigger**: `currentEpoch <= allocation.createdAtEpoch` +- **Effect**: Allocation hasn't existed for a full epoch +- **Handling**: **Deferred** (returns 0, no snapshot update, rewards preserved) + +#### SUBGRAPH_DENIED (soft deny) + +- **Trigger**: `isDenied(subgraphDeploymentId)` at POI presentation +- **Effect**: Cannot claim while denied +- **Handling**: **Deferred** (returns 0, no snapshot update, pre-denial rewards preserved) + +#### CLOSE_ALLOCATION + +- **Trigger**: Allocation being closed (force or normal) +- **Effect**: Uncollected rewards cannot go to indexer +- **Handling**: Rewards reclaimed; allocation snapshotted + +## Action Types + +### Reclaim + +Rewards are minted to a configured reclaim address: + +1. Try reason-specific: `reclaimAddresses[condition]` +2. Fallback: `defaultReclaimAddress` +3. If neither configured: rewards dropped (not minted) + +Emits `RewardsReclaimed(reason, rewards, indexer, allocationId, subgraphDeploymentId)` + +### Defer + +Rewards are preserved for later collection: + +- Returns 0 without modifying allocation state +- No snapshot update (preserves claim position) +- Allows claiming when condition clears + +### Claim (Normal) + +Rewards minted to rewards issuer for distribution: + +- Emits `HorizonRewardsAssigned` +- Allocation snapshotted to prevent double-claim +- Pending rewards cleared + +## Reclaim Address Configuration + +```solidity +// Governor-only functions +setReclaimAddress(bytes32 reason, address newAddress) // Per-condition +setDefaultReclaimAddress(address newAddress) // Fallback + +// Example configuration +reclaimAddresses[SUBGRAPH_DENIED] = treasuryAddress; +reclaimAddresses[INDEXER_INELIGIBLE] = treasuryAddress; +reclaimAddresses[NO_SIGNAL] = treasuryAddress; +defaultReclaimAddress = treasuryAddress; // Catch-all +``` + +**Important**: Changes apply retroactively to all future reclaims. + +## Key Behaviors + +### Snapshot Updates + +| Action | Updates Snapshot | Clears Pending | +| ------------ | ---------------- | -------------- | +| Claim (NONE) | Yes | Yes | +| Reclaim | Yes | Yes | +| Defer | No | No | + +### Accumulator Behavior When Not Claimable + +| Field | Behavior | +| ----------------------------- | ---------------------------------------------- | +| `accRewardsForSubgraph` | Does NOT increase (rewards reclaimed directly) | +| `accRewardsPerAllocatedToken` | Does NOT increase (rewards not distributed) | +| New rewards | Reclaimed immediately to configured address | +| Pre-existing stored rewards | Still shown as distributable in view functions | + +## Related Documentation + +- [RewardAccountingSafety.md](./RewardAccountingSafety.md) - Safety mechanisms and invariants diff --git a/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts index 30bae0483..716cd1f0a 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-calculations.test.ts @@ -44,11 +44,13 @@ describe('Rewards - Calculations', () => { // Derive some channel keys for each indexer used to sign attestations const channelKey1 = deriveChannelKey() + const channelKey2 = deriveChannelKey() const subgraphDeploymentID1 = randomHexBytes() const subgraphDeploymentID2 = randomHexBytes() const allocationID1 = channelKey1.address + const allocationID2 = channelKey2.address const metadata = HashZero @@ -229,39 +231,53 @@ describe('Rewards - Calculations', () => { describe('getAccRewardsForSubgraph', function () { it('accrued for each subgraph', async function () { - // Curator1 - Update total signalled + // Option B model: rewards only accumulate when allocations exist + const tokensToAllocate = toGRT('12500') const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - const tracker1 = await RewardsTracker.create() - - // Curator2 - Update total signalled const signalled2 = toGRT('500') - await curation.connect(curator2).mint(subgraphDeploymentID2, signalled2, 0) - // Snapshot - const tracker2 = await RewardsTracker.create() - await tracker1.snapshot() + // Setup both subgraphs with signal first + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + await curation.connect(curator2).mint(subgraphDeploymentID2, signalled2, 0) - // Jump - await helpers.mine(ISSUANCE_RATE_PERIODS) + // Setup both allocations + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) - // Snapshot - await tracker1.snapshot() - await tracker2.snapshot() + await staking.connect(indexer2).stake(tokensToAllocate) + await staking + .connect(indexer2) + .allocateFrom( + indexer2.address, + subgraphDeploymentID2, + tokensToAllocate, + allocationID2, + metadata, + await channelKey2.generateProof(indexer2.address), + ) - // Calculate rewards - const rewardsPerSignal1 = tracker1.accumulated - const rewardsPerSignal2 = tracker2.accumulated - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) - const expectedRewardsSG2 = rewardsPerSignal2.mul(signalled2).div(WeiPerEther) + // Jump to accumulate more rewards + await helpers.mine(ISSUANCE_RATE_PERIODS) // Get rewards from contract const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) const contractRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) - // Check - expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) - expect(toRound(expectedRewardsSG2)).eq(toRound(contractRewardsSG2)) + // Both subgraphs should have non-zero rewards + expect(contractRewardsSG1).to.be.gt(0) + expect(contractRewardsSG2).to.be.gt(0) + + // SG1 should have more rewards than SG2 (has more signal and allocation was created first) + expect(contractRewardsSG1).to.be.gt(contractRewardsSG2) }) it('should return zero rewards when subgraph signal is below minimum threshold', async function () { @@ -287,7 +303,22 @@ describe('Rewards - Calculations', () => { // Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - // Snapshot + + // Allocate - Option B requires allocation for rewards to accumulate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1).stake(tokensToAllocate) + await staking + .connect(indexer1) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Snapshot after allocation const tracker1 = await RewardsTracker.create() // Jump @@ -370,9 +401,10 @@ describe('Rewards - Calculations', () => { await helpers.mine(ISSUANCE_RATE_PERIODS) // Prepare expected results - // Note: rewards from signal to allocation (2 blocks) are reclaimed since no allocations exist yet - const expectedSubgraphRewards = toGRT('1000') // 5 blocks since allocation to when we do getAccRewardsForSubgraph - const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens + // Option B model: accRewardsForSubgraph only tracks distributable rewards + // 2 blocks before allocation = reclaimed (NO_ALLOCATION), 5 blocks after = distributable + const expectedSubgraphRewards = toGRT('1000') // 5 blocks × 200 GRT/block + const expectedRewardsAT = toGRT('0.08') // 1000 GRT / 12500 allocated tokens // Update await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID1) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts index 6c9fa37aa..ee60c3dd2 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-eligibility-oracle.test.ts @@ -6,13 +6,25 @@ import { RewardsManager } from '@graphprotocol/contracts' import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' -import { constants } from 'ethers' +import { BigNumber, constants } from 'ethers' import hre from 'hardhat' import { NetworkFixture } from '../lib/fixtures' const { HashZero } = constants +// Tolerance for fixed-point arithmetic rounding errors (matching Foundry tests) +const REWARDS_TOLERANCE = 20000 + +// Helper to check approximate equality for rewards (allows for rounding errors in fixed-point math) +function expectApproxEq(actual: BigNumber, expected: BigNumber, message: string) { + const diff = actual.sub(expected).abs() + expect( + diff.lte(REWARDS_TOLERANCE), + `${message}: difference ${diff.toString()} exceeds tolerance ${REWARDS_TOLERANCE}`, + ).to.be.true +} + describe('Rewards - Eligibility Oracle', () => { const graph = hre.graph() let curator1: SignerWithAddress @@ -195,10 +207,25 @@ describe('Rewards - Eligibility Oracle', () => { const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be denied due to eligibility - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDeniedDueToEligibility') - .withArgs(indexer1.address, allocationID1, expectedIndexingRewards) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsDeniedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(rewardsDeniedEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + const event = rewardsDeniedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') }) it('should allow rewards when rewards eligibility oracle approves', async function () { @@ -225,10 +252,25 @@ describe('Rewards - Eligibility Oracle', () => { const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be assigned normally - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'HorizonRewardsAssigned') - .withArgs(indexer1.address, allocationID1, expectedIndexingRewards) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsAssignedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'HorizonRewardsAssigned') + + expect(rewardsAssignedEvents.length).to.equal(1, 'HorizonRewardsAssigned event not found') + const event = rewardsAssignedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') }) }) @@ -292,10 +334,25 @@ describe('Rewards - Eligibility Oracle', () => { const expectedIndexingRewards = toGRT('1400') // Close allocation - REO should be checked - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDeniedDueToEligibility') - .withArgs(indexer1.address, allocationID1, expectedIndexingRewards) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsDeniedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(rewardsDeniedEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + const event = rewardsDeniedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') }) it('should handle indexer becoming ineligible mid-allocation', async function () { @@ -322,10 +379,25 @@ describe('Rewards - Eligibility Oracle', () => { const expectedIndexingRewards = toGRT('1600') // Close allocation - should be denied at close time (not creation time) - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDeniedDueToEligibility') - .withArgs(indexer1.address, allocationID1, expectedIndexingRewards) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsDeniedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'RewardsDeniedDueToEligibility') + + expect(rewardsDeniedEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + const event = rewardsDeniedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') }) it('should handle indexer becoming eligible mid-allocation', async function () { @@ -352,10 +424,25 @@ describe('Rewards - Eligibility Oracle', () => { const expectedIndexingRewards = toGRT('1600') // Close allocation - should now be allowed - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'HorizonRewardsAssigned') - .withArgs(indexer1.address, allocationID1, expectedIndexingRewards) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsAssignedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'HorizonRewardsAssigned') + + expect(rewardsAssignedEvents.length).to.equal(1, 'HorizonRewardsAssigned event not found') + const event = rewardsAssignedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') }) it('should handle denylist being added mid-allocation', async function () { @@ -422,10 +509,25 @@ describe('Rewards - Eligibility Oracle', () => { const expectedIndexingRewards = toGRT('1400') // Close allocation - should get rewards (no eligibility check when REO is zero) - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'HorizonRewardsAssigned') - .withArgs(indexer1.address, allocationID1, expectedIndexingRewards) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const rewardsAssignedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((event) => event?.name === 'HorizonRewardsAssigned') + + expect(rewardsAssignedEvents.length).to.equal(1, 'HorizonRewardsAssigned event not found') + const event = rewardsAssignedEvents[0]! + expect(event.args[0]).to.equal(indexer1.address) + expect(event.args[1]).to.equal(allocationID1) + expectApproxEq(event.args[2], expectedIndexingRewards, 'rewards amount') }) it('should verify event structure differences between denial mechanisms', async function () { diff --git a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts index 773c37670..9825b5294 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts @@ -6,7 +6,7 @@ import { RewardsManager } from '@graphprotocol/contracts' import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toGRT } from '@graphprotocol/sdk' import type { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { expect } from 'chai' -import { constants, utils } from 'ethers' +import { BigNumber, constants, utils } from 'ethers' import hre from 'hardhat' import { NetworkFixture } from '../lib/fixtures' @@ -18,6 +18,18 @@ const INDEXER_INELIGIBLE = utils.id('INDEXER_INELIGIBLE') const SUBGRAPH_DENIED = utils.id('SUBGRAPH_DENIED') const CLOSE_ALLOCATION = utils.id('CLOSE_ALLOCATION') const NO_SIGNAL = utils.id('NO_SIGNAL') + +// Tolerance for fixed-point arithmetic rounding errors (matching Foundry tests) +const REWARDS_TOLERANCE = 20000 + +// Helper to check approximate equality for rewards (allows for rounding errors in fixed-point math) +function expectApproxEq(actual: BigNumber, expected: BigNumber, message: string) { + const diff = actual.sub(expected).abs() + expect( + diff.lte(REWARDS_TOLERANCE), + `${message}: difference ${diff.toString()} exceeds tolerance ${REWARDS_TOLERANCE}`, + ).to.be.true +} const NO_ALLOCATION = utils.id('NO_ALLOCATION') const BELOW_MINIMUM_SIGNAL = utils.id('BELOW_MINIMUM_SIGNAL') @@ -193,9 +205,9 @@ describe('Rewards - Reclaim Addresses', () => { // RewardsReclaimed emitted with address(0) for indexer/allocationID (subgraph-level reclaim) await expect(tx).emit(rewardsManager, 'RewardsReclaimed') - // Check reclaim wallet received the rewards (use gte due to timing variations) + // Check reclaim wallet received the rewards (allow for rounding errors) const balanceAfter = await grt.balanceOf(reclaimWallet.address) - expect(balanceAfter.sub(balanceBefore)).gte(expectedRewards) + expectApproxEq(balanceAfter.sub(balanceBefore), expectedRewards, 'reclaimed rewards') }) it('should reclaim pre-denial rewards via _deniedRewards when denied after allocation', async function () { @@ -219,16 +231,39 @@ describe('Rewards - Reclaim Addresses', () => { const balanceBefore = await grt.balanceOf(reclaimWallet.address) // Close allocation — pre-denial rewards flow through _deniedRewards → _reclaimRewards - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const parsedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((e) => e !== null) // RewardsDenied IS emitted (allocation-level denial for pre-denial rewards) - await expect(tx).emit(rewardsManager, 'RewardsDenied').withArgs(indexer1.address, allocationID1) - // RewardsReclaimed emitted with actual indexer/allocationID (allocation-level reclaim) - await expect(tx) - .emit(rewardsManager, 'RewardsReclaimed') - .withArgs(SUBGRAPH_DENIED, toGRT('1400'), indexer1.address, allocationID1, subgraphDeploymentID1) + const deniedEvents = parsedEvents.filter((e) => e!.name === 'RewardsDenied') + expect(deniedEvents.length).to.equal(1, 'RewardsDenied event not found') + expect(deniedEvents[0]!.args[0]).to.equal(indexer1.address) + expect(deniedEvents[0]!.args[1]).to.equal(allocationID1) - // Reclaim wallet received the pre-denial rewards + // RewardsReclaimed emitted with actual indexer/allocationID (allocation-level reclaim) + const reclaimEvents = parsedEvents.filter((e) => e!.name === 'RewardsReclaimed') + expect(reclaimEvents.length).to.be.gte(1, 'RewardsReclaimed event not found') + // Find the allocation-level reclaim (has non-zero indexer and allocationID) + const allocationReclaim = reclaimEvents.find((e) => e!.args[2] !== constants.AddressZero) + expect(allocationReclaim).to.not.be.undefined + expect(allocationReclaim!.args[0]).to.equal(SUBGRAPH_DENIED) + expectApproxEq(allocationReclaim!.args[1], toGRT('1400'), 'reclaimed amount') + expect(allocationReclaim!.args[2]).to.equal(indexer1.address) + expect(allocationReclaim!.args[3]).to.equal(allocationID1) + expect(allocationReclaim!.args[4]).to.equal(subgraphDeploymentID1) + + // Reclaim wallet received the pre-denial rewards (may receive additional rewards from subgraph-level reclaim) const balanceAfter = await grt.balanceOf(reclaimWallet.address) expect(balanceAfter.sub(balanceBefore)).gte(toGRT('1400')) }) @@ -286,17 +321,41 @@ describe('Rewards - Reclaim Addresses', () => { const balanceBefore = await grt.balanceOf(reclaimWallet.address) // Close allocation - should emit both denial and reclaim events - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDeniedDueToEligibility') - .withArgs(indexer1.address, allocationID1, expectedRewards) - await expect(tx) - .emit(rewardsManager, 'RewardsReclaimed') - .withArgs(INDEXER_INELIGIBLE, expectedRewards, indexer1.address, allocationID1, subgraphDeploymentID1) + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const parsedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((e) => e !== null) + + // Check RewardsDeniedDueToEligibility event + const denialEvents = parsedEvents.filter((e) => e!.name === 'RewardsDeniedDueToEligibility') + expect(denialEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + expect(denialEvents[0]!.args[0]).to.equal(indexer1.address) + expect(denialEvents[0]!.args[1]).to.equal(allocationID1) + expectApproxEq(denialEvents[0]!.args[2], expectedRewards, 'denied rewards amount') + + // Check RewardsReclaimed event exists and verify args + const reclaimEvents = parsedEvents.filter((e) => e!.name === 'RewardsReclaimed') + expect(reclaimEvents.length).to.be.gte(1, 'RewardsReclaimed event not found') + const reclaimEvent = reclaimEvents.find((e) => e!.args[0] === INDEXER_INELIGIBLE) + expect(reclaimEvent).to.not.be.undefined + expect(reclaimEvent!.args[0]).to.equal(INDEXER_INELIGIBLE) + expectApproxEq(reclaimEvent!.args[1], expectedRewards, 'reclaimed amount') + expect(reclaimEvent!.args[2]).to.equal(indexer1.address) + expect(reclaimEvent!.args[3]).to.equal(allocationID1) + expect(reclaimEvent!.args[4]).to.equal(subgraphDeploymentID1) // Check reclaim wallet received the rewards const balanceAfter = await grt.balanceOf(reclaimWallet.address) - expect(balanceAfter.sub(balanceBefore)).eq(expectedRewards) + expectApproxEq(balanceAfter.sub(balanceBefore), expectedRewards, 'wallet balance increase') }) it('should not mint to reclaim address when reclaim address not set', async function () { @@ -322,11 +381,30 @@ describe('Rewards - Reclaim Addresses', () => { const expectedRewards = toGRT('1400') // Close allocation - should only emit denial event, not reclaim - const tx = staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDeniedDueToEligibility') - .withArgs(indexer1.address, allocationID1, expectedRewards) - await expect(tx).to.not.emit(rewardsManager, 'RewardsReclaimed') + const tx = await staking.connect(indexer1).closeAllocation(allocationID1, randomHexBytes()) + const receipt = await tx.wait() + + // Parse RewardsManager events from the transaction receipt + const parsedEvents = receipt.logs + .map((log) => { + try { + return rewardsManager.interface.parseLog(log) + } catch { + return null + } + }) + .filter((e) => e !== null) + + // Check RewardsDeniedDueToEligibility event + const denialEvents = parsedEvents.filter((e) => e!.name === 'RewardsDeniedDueToEligibility') + expect(denialEvents.length).to.equal(1, 'RewardsDeniedDueToEligibility event not found') + expect(denialEvents[0]!.args[0]).to.equal(indexer1.address) + expect(denialEvents[0]!.args[1]).to.equal(allocationID1) + expectApproxEq(denialEvents[0]!.args[2], expectedRewards, 'denied rewards amount') + + // Check no RewardsReclaimed event + const reclaimEvents = parsedEvents.filter((e) => e!.name === 'RewardsReclaimed') + expect(reclaimEvents.length).to.equal(0, 'RewardsReclaimed event should not be emitted') }) }) @@ -375,11 +453,15 @@ describe('Rewards - Reclaim Addresses', () => { // RewardsReclaimed emitted (subgraph-level reclaim) await expect(tx).emit(rewardsManager, 'RewardsReclaimed') - // Only SUBGRAPH_DENIED wallet should receive rewards (use gte due to timing variations) + // Only SUBGRAPH_DENIED wallet should receive rewards (allow for rounding errors) const subgraphDeniedBalanceAfter = await grt.balanceOf(reclaimWallet.address) const indexerIneligibleBalanceAfter = await grt.balanceOf(otherWallet.address) - expect(subgraphDeniedBalanceAfter.sub(subgraphDeniedBalanceBefore)).gte(expectedRewards) + expectApproxEq( + subgraphDeniedBalanceAfter.sub(subgraphDeniedBalanceBefore), + expectedRewards, + 'SUBGRAPH_DENIED wallet balance', + ) expect(indexerIneligibleBalanceAfter.sub(indexerIneligibleBalanceBefore)).eq(0) }) @@ -472,7 +554,7 @@ describe('Rewards - Reclaim Addresses', () => { // INDEXER_INELIGIBLE wallet should receive rewards (fallback from SUBGRAPH_DENIED) const balanceAfter = await grt.balanceOf(reclaimWallet.address) - expect(balanceAfter.sub(balanceBefore)).gte(expectedRewards) + expectApproxEq(balanceAfter.sub(balanceBefore), expectedRewards, 'INDEXER_INELIGIBLE wallet balance') }) it('should drop rewards when both fail and neither address configured', async function () { diff --git a/packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts new file mode 100644 index 000000000..2971695a1 --- /dev/null +++ b/packages/contracts-test/tests/unit/rewards/rewards-signal-allocation-update.test.ts @@ -0,0 +1,560 @@ +import { Curation } from '@graphprotocol/contracts' +import { GraphToken } from '@graphprotocol/contracts' +import { IStaking } from '@graphprotocol/contracts' +import { RewardsManager } from '@graphprotocol/contracts' +import { deriveChannelKey, GraphNetworkContracts, helpers, randomHexBytes, toBN, 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' + +const { HashZero } = constants + +/** + * Test for the signal/allocation update accounting bug fix. + * + * The bug: When `onSubgraphSignalUpdate()` is called before `onSubgraphAllocationUpdate()` + * in the SAME BLOCK, the per-signal delta is zero but rewards tracked in `accRewardsForSubgraph` + * are never distributed to allocations. This causes rewards to be "bricked". + * + * The fix: Use the snapshot delta (accRewardsForSubgraph - accRewardsForSubgraphSnapshot) instead + * of only relying on the per-signal delta for calculating new rewards. + * + * IMPORTANT: These tests use evm_setAutomine to batch transactions into the same block, + * which is necessary to reproduce the bug condition where per-signal delta = 0. + */ +describe('Rewards: Signal and Allocation Update Accounting', () => { + const graph = hre.graph() + let governor: SignerWithAddress + let curator: SignerWithAddress + let indexer: SignerWithAddress + + let fixture: NetworkFixture + let contracts: GraphNetworkContracts + let grt: GraphToken + let curation: Curation + let staking: IStaking + let rewardsManager: RewardsManager + + const channelKey = deriveChannelKey() + const subgraphDeploymentID = randomHexBytes() + const allocationID = channelKey.address + const metadata = HashZero + + const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT every block + const tokensToSignal = toGRT('1000') + const tokensToStake = toGRT('100000') + const tokensToAllocate = toGRT('10000') + + before(async function () { + ;[curator, indexer] = await graph.getTestAccounts() + ;({ governor } = await graph.getNamedAccounts()) + + fixture = new NetworkFixture(graph.provider) + contracts = await fixture.load(governor) + grt = contracts.GraphToken as GraphToken + curation = contracts.Curation as Curation + staking = contracts.Staking as IStaking + rewardsManager = contracts.RewardsManager as RewardsManager + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + async function setupSubgraphWithAllocation() { + // Setup: curator signals on subgraph + await grt.connect(governor).mint(curator.address, tokensToSignal) + await grt.connect(curator).approve(curation.address, tokensToSignal) + await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0) + + // Setup: indexer stakes and allocates + await grt.connect(governor).mint(indexer.address, tokensToStake) + await grt.connect(indexer).approve(staking.address, tokensToStake) + await staking.connect(indexer).stake(tokensToStake) + await staking + .connect(indexer) + .allocateFrom( + indexer.address, + subgraphDeploymentID, + tokensToAllocate, + allocationID, + metadata, + await channelKey.generateProof(indexer.address), + ) + } + + describe('onSubgraphSignalUpdate followed by onSubgraphAllocationUpdate', function () { + it('should properly distribute rewards when signal update precedes allocation update (same block)', async function () { + await setupSubgraphWithAllocation() + + // Advance blocks to accumulate rewards + await helpers.mine(100) + + // Get expected rewards before any updates + const expectedRewardsForSubgraph = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(expectedRewardsForSubgraph).to.be.gt(0, 'Should have accumulated rewards') + + // Get initial state + const subgraphBefore = await rewardsManager.subgraphs(subgraphDeploymentID) + const accRewardsPerAllocatedTokenBefore = subgraphBefore.accRewardsPerAllocatedToken + + // Disable automine to batch transactions into the same block + await hre.network.provider.send('evm_setAutomine', [false]) + + try { + // First: call onSubgraphSignalUpdate (this zeros the per-signal delta) + // This simulates what happens when a curator mints/burns signal + const signalTx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // Second: call onSubgraphAllocationUpdate (in same block, per-signal delta is 0) + // This simulates what happens when an allocation is opened/closed + const allocTx = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Mine both transactions in the same block + await hre.network.provider.send('evm_mine') + + // Wait for both transactions to be mined + await signalTx.wait() + await allocTx.wait() + } finally { + // Re-enable automine + await hre.network.provider.send('evm_setAutomine', [true]) + } + + // Verify rewards were tracked at subgraph level + const subgraphAfterSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + expect(subgraphAfterSignal.accRewardsForSubgraph).to.be.gt( + 0, + 'accRewardsForSubgraph should be updated after signal update', + ) + + // Get final state + const subgraphAfterAllocation = await rewardsManager.subgraphs(subgraphDeploymentID) + + // THE FIX: accRewardsPerAllocatedToken should be updated even though per-signal delta was 0 + // With the bug, this would remain unchanged because newRewards=0 caused early return + expect(subgraphAfterAllocation.accRewardsPerAllocatedToken).to.be.gt( + accRewardsPerAllocatedTokenBefore, + 'accRewardsPerAllocatedToken should increase (BUG: was not updated when signal update preceded allocation update)', + ) + + // Verify snapshot consistency + expect(subgraphAfterAllocation.accRewardsForSubgraphSnapshot).to.equal( + subgraphAfterAllocation.accRewardsForSubgraph, + 'Snapshots should be in sync after updates', + ) + }) + + it('should not brick rewards when signal update zeros the per-signal delta (same block)', async function () { + await setupSubgraphWithAllocation() + + // Advance blocks + await helpers.mine(100) + + // Get the view function result (what rewards SHOULD be) before any updates + // Note: We call this to ensure the function works, but we verify via stored state below + await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID) + + // Disable automine to batch transactions into the same block + await hre.network.provider.send('evm_setAutomine', [false]) + + try { + // Call signal update first (zeros per-signal delta and accumulates rewards in accRewardsForSubgraph) + const signalTx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // Call allocation update (per-signal delta is now 0, but rewards are in accRewardsForSubgraph) + const allocTx = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Mine both transactions in the same block + await hre.network.provider.send('evm_mine') + + // Wait for both transactions to be mined + await signalTx.wait() + await allocTx.wait() + } finally { + // Re-enable automine + await hre.network.provider.send('evm_setAutomine', [true]) + } + + // Get the rewards accumulated in accRewardsForSubgraph + const afterSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + const rewardsAccumulated = afterSignal.accRewardsForSubgraph + + // These rewards should eventually be distributed to allocations + expect(rewardsAccumulated).to.be.gt(0, 'Rewards should be accumulated at subgraph level') + + // Get stored state + const subgraph = await rewardsManager.subgraphs(subgraphDeploymentID) + + // THE BUG: With the original buggy code, accRewardsPerAllocatedToken would remain at 0 + // because newRewards from per-signal delta is 0, causing early return. + // THE FIX: accRewardsPerAllocatedToken should be updated to reflect the accumulated rewards + expect(subgraph.accRewardsPerAllocatedToken).to.be.gt( + 0, + 'accRewardsPerAllocatedToken should be non-zero (BUG: rewards were bricked)', + ) + + // Verify view function and stored state are consistent + const [viewAccRewardsPerAllocatedToken] = + await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID) + + // The view should equal the stored value (since snapshots are synced) + expect(viewAccRewardsPerAllocatedToken).to.equal( + subgraph.accRewardsPerAllocatedToken, + 'View function should match stored state after updates', + ) + }) + + it('should handle multiple signal updates without losing rewards (same block allocation)', async function () { + await setupSubgraphWithAllocation() + + // Advance blocks + await helpers.mine(50) + + // First signal update + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const afterFirstSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance more blocks + await helpers.mine(50) + + // Disable automine to batch signal + allocation into same block + await hre.network.provider.send('evm_setAutomine', [false]) + + try { + // Second signal update (without allocation update in between) + const signalTx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // Allocation update in the same block (per-signal delta is 0) + const allocTx = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Mine both in the same block + await hre.network.provider.send('evm_mine') + + await signalTx.wait() + await allocTx.wait() + } finally { + await hre.network.provider.send('evm_setAutomine', [true]) + } + + const afterSecondSignal = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Rewards should have accumulated + expect(afterSecondSignal.accRewardsForSubgraph).to.be.gt( + afterFirstSignal.accRewardsForSubgraph, + 'Rewards should accumulate across signal updates', + ) + + const afterAllocation = await rewardsManager.subgraphs(subgraphDeploymentID) + + // All accumulated rewards should be distributed + expect(afterAllocation.accRewardsPerAllocatedToken).to.be.gt( + 0, + 'Rewards from multiple signal updates should be distributed', + ) + + // Snapshots should be in sync + expect(afterAllocation.accRewardsForSubgraphSnapshot).to.equal( + afterAllocation.accRewardsForSubgraph, + 'Snapshots should be in sync', + ) + }) + }) + + describe('snapshot consistency in reclaim paths', function () { + it('should update accRewardsForSubgraphSnapshot when rewards are reclaimed due to denial', async function () { + await setupSubgraphWithAllocation() + + // Deny the subgraph + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Advance blocks to accumulate rewards + await helpers.mine(100) + + // Get state before + const subgraphBefore = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Call allocation update - should reclaim (not distribute) rewards + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // Get state after + const subgraphAfter = await rewardsManager.subgraphs(subgraphDeploymentID) + + // accRewardsPerAllocatedToken should NOT increase (rewards reclaimed, not distributed) + expect(subgraphAfter.accRewardsPerAllocatedToken).to.equal( + subgraphBefore.accRewardsPerAllocatedToken, + 'accRewardsPerAllocatedToken should not increase when denied', + ) + + // THE FIX: accRewardsForSubgraphSnapshot should be updated to prevent re-reclaiming + expect(subgraphAfter.accRewardsForSubgraphSnapshot).to.be.gte( + subgraphBefore.accRewardsForSubgraphSnapshot, + 'accRewardsForSubgraphSnapshot should be updated in reclaim path', + ) + }) + + it('should not double-reclaim rewards after snapshot update', async function () { + await setupSubgraphWithAllocation() + + // Deny the subgraph + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Advance blocks + await helpers.mine(100) + + // First allocation update - reclaims rewards + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterFirstReclaim = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Second allocation update - each tx advances a block, so there's 1 more block of rewards + // The key invariant is that rewards are properly accounted for, not double-reclaimed + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + const afterSecondReclaim = await rewardsManager.subgraphs(subgraphDeploymentID) + + // The snapshot should have advanced by at most 1 block's worth of rewards + // (Each transaction creates a new block in Hardhat) + const maxOneBlockReward = ISSUANCE_PER_BLOCK.mul(tokensToSignal).div(await grt.balanceOf(curation.address)) + + const snapshotDiff = afterSecondReclaim.accRewardsForSubgraphSnapshot.sub( + afterFirstReclaim.accRewardsForSubgraphSnapshot, + ) + + // The difference should be at most one block's worth of rewards + expect(snapshotDiff).to.be.lte( + maxOneBlockReward.mul(2), // Allow for rounding and timing + 'Should only process one block worth of new rewards', + ) + + // Verify accRewardsPerAllocatedToken didn't increase (rewards still reclaimed, not distributed) + expect(afterSecondReclaim.accRewardsPerAllocatedToken).to.equal( + afterFirstReclaim.accRewardsPerAllocatedToken, + 'accRewardsPerAllocatedToken should not change during reclaim', + ) + }) + }) + + describe('onSubgraphSignalUpdate on denied subgraph', function () { + it('should reclaim rewards when onSubgraphSignalUpdate is called on denied subgraph', async function () { + await setupSubgraphWithAllocation() + + // Configure reclaim address for SUBGRAPH_DENIED + const SUBGRAPH_DENIED = hre.ethers.utils.id('SUBGRAPH_DENIED') + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, governor.address) + + // Verify reclaim address was set + const reclaimAddr = await rewardsManager.getReclaimAddress(SUBGRAPH_DENIED) + expect(reclaimAddr).to.equal(governor.address, 'Reclaim address should be set') + + // Deny the subgraph + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Record state after denial (setDenied calls onSubgraphAllocationUpdate internally) + const afterDenial = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance blocks - rewards should accumulate + await helpers.mine(100) + + // Call onSubgraphSignalUpdate (simulates curator action) + // With Option B fix: rewards should be reclaimed immediately + const tx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const receipt = await tx.wait() + const afterSignalUpdate = await rewardsManager.subgraphs(subgraphDeploymentID) + + // With Option B: accRewardsForSubgraph should NOT change for denied subgraphs + // (rewards are reclaimed directly, not stored) + expect(afterSignalUpdate.accRewardsForSubgraph).to.equal( + afterDenial.accRewardsForSubgraph, + 'accRewardsForSubgraph should not change for denied subgraphs (rewards reclaimed)', + ) + + // Verify reclaim event was emitted + const reclaimEvent = receipt.events?.find((e) => e.event === 'RewardsReclaimed') + expect(reclaimEvent).to.not.be.undefined + // Event args: (reason, rewards, indexer, allocationId, subgraphDeploymentId) + const rewards = reclaimEvent!.args![1] // rewards is second arg + expect(rewards).to.be.gt(0, 'Should have reclaimed rewards') + }) + + it('should accumulate rewards for claimable subgraphs in onSubgraphSignalUpdate', async function () { + await setupSubgraphWithAllocation() + + // Record initial state (subgraph is claimable by default) + const initialState = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance blocks - rewards should accumulate + await helpers.mine(100) + + // Call onSubgraphSignalUpdate + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const afterSignalUpdate = await rewardsManager.subgraphs(subgraphDeploymentID) + + // For claimable subgraphs: accRewardsForSubgraph SHOULD increase + expect(afterSignalUpdate.accRewardsForSubgraph).to.be.gt( + initialState.accRewardsForSubgraph, + 'accRewardsForSubgraph should increase for claimable subgraphs', + ) + }) + + it('view function getAccRewardsForSubgraph should not jump during denial', async function () { + await setupSubgraphWithAllocation() + + // Accumulate some rewards while claimable + await helpers.mine(50) + + // Deny the subgraph (setDenied distributes pre-denial rewards via onSubgraphAllocationUpdate) + await rewardsManager.connect(governor).setSubgraphAvailabilityOracle(governor.address) + await rewardsManager.connect(governor).setDenied(subgraphDeploymentID, true) + + // Record view value immediately after denial + const rewardsAtDenial = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsAtDenial).to.be.gt(0, 'Should have accumulated pre-denial rewards') + + // Advance blocks during denial + await helpers.mine(100) + + // View function should return SAME value (no jump up during denial) + const rewardsDuringDenial = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsDuringDenial).to.equal(rewardsAtDenial, 'View should not increase during denial') + + // Call signal update (with bug, this would NOT reclaim, causing view to jump on next allocation update) + // Configure reclaim address so rewards are reclaimed + const SUBGRAPH_DENIED = hre.ethers.utils.id('SUBGRAPH_DENIED') + await rewardsManager.connect(governor).setReclaimAddress(SUBGRAPH_DENIED, governor.address) + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // View function should STILL return same value (rewards reclaimed, not accumulated) + const rewardsAfterSignalUpdate = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsAfterSignalUpdate).to.equal(rewardsAtDenial, 'View should not jump after signal update') + + // Mine more blocks + await helpers.mine(50) + + // Call allocation update + await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + + // View should STILL be stable (rewards reclaimed, not accumulated) + const rewardsAfterAllocationUpdate = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(rewardsAfterAllocationUpdate).to.equal(rewardsAtDenial, 'View should not jump after allocation update') + }) + }) + + describe('onSubgraphSignalUpdate with no allocations', function () { + it('should reclaim as NO_ALLOCATION when signal exists but no allocations', async function () { + // Setup: only signal, no allocation + await grt.connect(governor).mint(curator.address, tokensToSignal) + await grt.connect(curator).approve(curation.address, tokensToSignal) + await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0) + + // Configure reclaim address for NO_ALLOCATION + const NO_ALLOCATION = hre.ethers.utils.id('NO_ALLOCATION') + await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATION, governor.address) + + // Record initial state + const initialState = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Advance blocks - rewards should accumulate + await helpers.mine(100) + + // Call onSubgraphSignalUpdate - should reclaim as NO_ALLOCATION + const tx = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const receipt = await tx.wait() + const afterSignalUpdate = await rewardsManager.subgraphs(subgraphDeploymentID) + + // accRewardsForSubgraph should NOT change (rewards reclaimed, not accumulated) + expect(afterSignalUpdate.accRewardsForSubgraph).to.equal( + initialState.accRewardsForSubgraph, + 'accRewardsForSubgraph should not change when no allocations (rewards reclaimed)', + ) + + // Verify reclaim event was emitted with NO_ALLOCATION reason + const reclaimEvent = receipt.events?.find((e) => e.event === 'RewardsReclaimed') + expect(reclaimEvent).to.not.be.undefined + expect(reclaimEvent!.args![0]).to.equal(NO_ALLOCATION, 'Should reclaim with NO_ALLOCATION reason') + expect(reclaimEvent!.args![1]).to.be.gt(0, 'Should have reclaimed rewards') + }) + + it('view function should not show phantom rewards when no allocations', async function () { + // Setup: only signal, no allocation + await grt.connect(governor).mint(curator.address, tokensToSignal) + await grt.connect(curator).approve(curation.address, tokensToSignal) + await curation.connect(curator).mint(subgraphDeploymentID, tokensToSignal, 0) + + // Record view immediately after signal + const viewAfterSignal = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + + // Advance blocks + await helpers.mine(100) + + // Configure reclaim and call signal update + const NO_ALLOCATION = hre.ethers.utils.id('NO_ALLOCATION') + await rewardsManager.connect(governor).setReclaimAddress(NO_ALLOCATION, governor.address) + await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + + // View should remain stable (rewards reclaimed) + const viewAfterReclaim = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID) + expect(viewAfterReclaim).to.equal(viewAfterSignal, 'View should not grow when no allocations') + }) + }) + + describe('invariant: no rewards lost or double-counted', function () { + it('should maintain accounting invariant across mixed updates (with same-block scenarios)', async function () { + await setupSubgraphWithAllocation() + + // Sequence of operations that could trigger the bug + await helpers.mine(25) + + // First: signal update followed by allocation update in SAME BLOCK + await hre.network.provider.send('evm_setAutomine', [false]) + try { + const signalTx1 = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const allocTx1 = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + await hre.network.provider.send('evm_mine') + await signalTx1.wait() + await allocTx1.wait() + } finally { + await hre.network.provider.send('evm_setAutomine', [true]) + } + + await helpers.mine(25) + + // Second: double signal update followed by allocation update in SAME BLOCK + await hre.network.provider.send('evm_setAutomine', [false]) + try { + const signalTx2 = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const signalTx3 = await rewardsManager.connect(governor).onSubgraphSignalUpdate(subgraphDeploymentID) + const allocTx2 = await rewardsManager.connect(governor).onSubgraphAllocationUpdate(subgraphDeploymentID) + await hre.network.provider.send('evm_mine') + await signalTx2.wait() + await signalTx3.wait() + await allocTx2.wait() + } finally { + await hre.network.provider.send('evm_setAutomine', [true]) + } + + // Final state check + const finalSubgraph = await rewardsManager.subgraphs(subgraphDeploymentID) + + // Key invariant: snapshots should be in sync + expect(finalSubgraph.accRewardsForSubgraphSnapshot).to.equal( + finalSubgraph.accRewardsForSubgraph, + 'INVARIANT VIOLATED: accRewardsForSubgraphSnapshot != accRewardsForSubgraph', + ) + + // Rewards should have been distributed + expect(finalSubgraph.accRewardsPerAllocatedToken).to.be.gt( + 0, + 'Rewards should have been distributed to allocations', + ) + }) + }) +}) 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 index a8c3b0c08..3ebe59d39 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-subgraph-service.test.ts @@ -380,12 +380,13 @@ describe('Rewards - SubgraphService', () => { // Mine blocks await helpers.mine(5) - // Get rewards - should return 0 when no allocations + // Get rewards - Option B: with no allocations, rewards are reclaimed (NO_ALLOCATION) + // so both accRewardsPerAllocatedToken and accRewardsForSubgraph remain 0 const [accRewardsPerAllocatedToken, accRewardsForSubgraph] = await rewardsManager.getAccRewardsPerAllocatedToken(subgraphDeploymentID1) expect(accRewardsPerAllocatedToken).to.equal(0) - expect(accRewardsForSubgraph).to.be.gt(0) // Subgraph still accrues, but no per-token rewards + expect(accRewardsForSubgraph).to.equal(0) // Option B: rewards reclaimed when no allocations }) }) diff --git a/packages/contracts-test/tests/unit/rewards/rewards.test.ts b/packages/contracts-test/tests/unit/rewards/rewards.test.ts index bdf13a83d..7702b3d4e 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards.test.ts @@ -338,39 +338,68 @@ describe('Rewards', () => { describe('getAccRewardsForSubgraph', function () { it('accrued for each subgraph', async function () { - // Curator1 - Update total signalled + // Setup: signal and allocations for two subgraphs const signalled1 = toGRT('1500') - await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) - const tracker1 = await RewardsTracker.create() - - // Curator2 - Update total signalled const signalled2 = toGRT('500') + const tokensToStake = toGRT('100000') + const tokensToAllocate = toGRT('10000') + + // Mint signal for both subgraphs + await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) await curation.connect(curator2).mint(subgraphDeploymentID2, signalled2, 0) - // Snapshot - const tracker2 = await RewardsTracker.create() - await tracker1.snapshot() + // Create allocations for both subgraphs so rewards are accumulated (not reclaimed as NO_ALLOCATION) + await grt.connect(governor).mint(indexer1.address, tokensToStake) + await grt.connect(indexer1).approve(staking.address, tokensToStake) + await staking.connect(indexer1).stake(tokensToStake) - // Jump + const channelKey1 = deriveChannelKey() + await staking + .connect(indexer1) + .allocate( + subgraphDeploymentID1, + tokensToAllocate, + channelKey1.address, + HashZero, + await channelKey1.generateProof(indexer1.address), + ) + + const channelKey2 = deriveChannelKey() + await staking + .connect(indexer1) + .allocate( + subgraphDeploymentID2, + tokensToAllocate, + channelKey2.address, + HashZero, + await channelKey2.generateProof(indexer1.address), + ) + + // Record starting point for both subgraphs + const startRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + const startRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) + + // Jump blocks to accrue rewards await helpers.mine(ISSUANCE_RATE_PERIODS) - // Snapshot - await tracker1.snapshot() - await tracker2.snapshot() + // Get final rewards + const endRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) + const endRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) - // Calculate rewards - const rewardsPerSignal1 = tracker1.accumulated - const rewardsPerSignal2 = tracker2.accumulated - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) - const expectedRewardsSG2 = rewardsPerSignal2.mul(signalled2).div(WeiPerEther) + // Calculate accrued rewards during the period + const accruedSG1 = endRewardsSG1.sub(startRewardsSG1) + const accruedSG2 = endRewardsSG2.sub(startRewardsSG2) - // Get rewards from contract - const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID1) - const contractRewardsSG2 = await rewardsManager.getAccRewardsForSubgraph(subgraphDeploymentID2) + // Verify proportional distribution: SG1 has 75% of signal (1500/2000), SG2 has 25% (500/2000) + // So SG1 should accrue 3x the rewards of SG2 + const totalAccrued = accruedSG1.add(accruedSG2) + expect(totalAccrued).to.be.gt(0, 'Should have accrued rewards') - // Check - expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) - expect(toRound(expectedRewardsSG2)).eq(toRound(contractRewardsSG2)) + // Check proportional distribution (allow small rounding error) + const sg1Share = accruedSG1.mul(100).div(totalAccrued) + const sg2Share = accruedSG2.mul(100).div(totalAccrued) + expect(sg1Share.toNumber()).to.be.closeTo(75, 1, 'SG1 should have ~75% of rewards') + expect(sg2Share.toNumber()).to.be.closeTo(25, 1, 'SG2 should have ~25% of rewards') }) it('should return zero rewards when subgraph signal is below minimum threshold', async function () { @@ -396,6 +425,24 @@ describe('Rewards', () => { // Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1).mint(subgraphDeploymentID1, signalled1, 0) + + // Create an allocation so rewards are accumulated (not reclaimed as NO_ALLOCATION) + const tokensToStake = toGRT('100000') + const tokensToAllocate = toGRT('10000') + await grt.connect(governor).mint(indexer1.address, tokensToStake) + await grt.connect(indexer1).approve(staking.address, tokensToStake) + await staking.connect(indexer1).stake(tokensToStake) + const channelKey = deriveChannelKey() + await staking + .connect(indexer1) + .allocate( + subgraphDeploymentID1, + tokensToAllocate, + channelKey.address, + HashZero, + await channelKey.generateProof(indexer1.address), + ) + // Snapshot const tracker1 = await RewardsTracker.create() @@ -479,8 +526,10 @@ describe('Rewards', () => { await helpers.mine(ISSUANCE_RATE_PERIODS) // Prepare expected results - // Note: rewards from signal to allocation (2 blocks) are reclaimed since no allocations exist yet - const expectedSubgraphRewards = toGRT('1000') // 5 blocks since allocation to when we do getAccRewardsForSubgraph + // With Option B model: accRewardsForSubgraph only tracks DISTRIBUTABLE rewards (not reclaimed) + // 7 blocks total: 2 blocks before allocation (reclaimed, NOT in accRewardsForSubgraph) + 5 blocks after allocation + const expectedSubgraphRewards = toGRT('1000') // only distributable rewards (5 blocks) + // accRewardsPerAllocatedToken reflects distributable rewards (5 blocks) const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens // Update diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 99a8e34f6..49a852abb 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -22,39 +22,12 @@ import { RewardsCondition } from "@graphprotocol/interfaces/contracts/contracts/ /** * @title Rewards Manager Contract * @author Edge & Node - * @notice Manages rewards distribution for indexers and delegators in the Graph Protocol + * @notice Manages indexing rewards distribution using a two-level accumulation model: + * signal → subgraph → allocation. See docs/RewardAccountingSafety.md for details. * - * @dev ## Token Accounting Model - * - * Rewards use a two-level accumulation model with snapshot-based safety: - * - * **Level 1 - Signal Distribution (cross-subgraph):** - * - `accRewardsPerSignal` accumulates rewards per signaled token globally - * - Each subgraph gets rewards proportional to its curation signal - * - `accRewardsForSubgraph` tracks total rewards allocated to each subgraph - * - * **Level 2 - Allocation Distribution (within-subgraph):** - * - `accRewardsPerAllocatedToken` scales subgraph rewards to indexer allocations - * - Each allocation tracks its starting snapshot to calculate its share - * - * Accumulation invariants: - * - Snapshots prevent double-counting: each allocation's reward = (current - snapshot) × tokens - * - Accumulator values never decrease - * - Tokens are minted at claim time - * - * @dev If an `issuanceAllocator` is set, it determines GRT issued per block. - * Otherwise, the `issuancePerBlock` storage value is used. This contract - * is a self-minting target responsible for directly minting allocated GRT. - * - * Note: - * The contract provides getter functions to query the state of accrued rewards: - * - getAccRewardsPerSignal - * - getAccRewardsForSubgraph - * - getAccRewardsPerAllocatedToken - * - getRewards - * These functions may overestimate the actual rewards due to changes in the total supply - * 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. + * @dev Issuance source: `issuanceAllocator` if set, otherwise `issuancePerBlock` storage. + * Getter functions (getAccRewardsPerSignal, getRewards, etc.) may overestimate until + * takeRewards is called due to pending state updates. */ contract RewardsManager is GraphUpgradeable, @@ -272,12 +245,10 @@ contract RewardsManager is } /** - * @notice Internal: Denies to claim rewards for a subgraph. - * @dev Idempotent: redundant calls (deny when already denied, undeny when already allowed) - * skip the denylist update and event emission (but still call `onSubgraphAllocationUpdate`). - * This preserves the original deny block number on repeated deny calls. + * @notice Sets the denied status for a subgraph. + * @dev Idempotent: redundant calls skip the update but still call `onSubgraphAllocationUpdate`. * @param subgraphDeploymentId Subgraph deployment ID - * @param deny Whether to set the subgraph as denied for claiming rewards or not + * @param deny True to deny rewards, false to allow */ function _setDenied(bytes32 subgraphDeploymentId, bool deny) private { onSubgraphAllocationUpdate(subgraphDeploymentId); @@ -342,30 +313,16 @@ contract RewardsManager is return rewardsEligibilityOracle; } - /** - * @inheritdoc IRewardsManager - * @dev Linear formula: `x = r * t` - * - * Notation: - * t: time steps are in blocks since last updated - * x: newly accrued rewards tokens for the period `t` - * - * @return claimablePerSignal accrued rewards per signal since last update, scaled by FIXED_POINT_SCALING_FACTOR - */ + /// @inheritdoc IRewardsManager function getNewRewardsPerSignal() public view override returns (uint256 claimablePerSignal) { (claimablePerSignal, ) = _getNewRewardsPerSignal(); } /** - * @notice Calculate new rewards per signal, split into claimable and unclaimable portions - * @dev Linear formula: `x = r * t` - * - * Notation: - * t: time steps are in blocks since last updated - * x: newly accrued rewards tokens for the period `t` - * - * @return claimablePerSignal Rewards per signal when signal exists, scaled by FIXED_POINT_SCALING_FACTOR - * @return unclaimableTokens Raw token amount that cannot be distributed due to zero signal + * @notice Calculate new rewards per signal since last update + * @dev Formula: `x = r * t` where t = blocks since last update. + * @return claimablePerSignal Rewards per signal (scaled by FIXED_POINT_SCALING_FACTOR) + * @return unclaimableTokens Tokens not distributed due to zero signal */ function _getNewRewardsPerSignal() private view returns (uint256 claimablePerSignal, uint256 unclaimableTokens) { // Calculate time steps @@ -397,7 +354,7 @@ contract RewardsManager is * @inheritdoc IRewardsManager * @dev Returns accumulated rewards for external callers. * New rewards are only included if the subgraph is claimable (neither denied nor below minimum signal). - * Reclaim for non-claimable subgraphs is handled in `onSubgraphAllocationUpdate()`. + * Reclaim for non-claimable subgraphs is handled in `onSubgraphSignalUpdate()` and `onSubgraphAllocationUpdate()`. */ function getAccRewardsForSubgraph(bytes32 _subgraphDeploymentID) public view override returns (uint256) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; @@ -405,33 +362,30 @@ contract RewardsManager is return subgraph.accRewardsForSubgraph.add(condition == RewardsCondition.NONE ? newRewards : 0); } - /// @inheritdoc IRewardsManager + /** + * @inheritdoc IRewardsManager + * @dev New rewards are only included via `getAccRewardsForSubgraph` when subgraph is claimable. + * Pre-existing stored rewards are always shown as distributable (preserved for when conditions clear). + * Does not check indexer eligibility - that can change and doesn't affect reward accrual. + */ function getAccRewardsPerAllocatedToken( bytes32 _subgraphDeploymentID ) public view override returns (uint256, uint256) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; + // getAccRewardsForSubgraph already handles claimability: excludes new rewards when not claimable uint256 accRewardsForSubgraph = getAccRewardsForSubgraph(_subgraphDeploymentID); uint256 newRewardsForSubgraph = MathUtils.diffOrZero( accRewardsForSubgraph, subgraph.accRewardsForSubgraphSnapshot ); - // There are two contributors to subgraph allocated tokens: - // - the legacy allocations on the legacy staking contract - // - the new allocations on the subgraph service - uint256 subgraphAllocatedTokens = 0; - address[2] memory rewardsIssuers = [address(staking()), address(subgraphService)]; - for (uint256 i = 0; i < rewardsIssuers.length; ++i) { - if (rewardsIssuers[i] != address(0)) { - subgraphAllocatedTokens += IRewardsIssuer(rewardsIssuers[i]).getSubgraphAllocatedTokens( - _subgraphDeploymentID - ); - } - } + // Get total allocated tokens across all issuers + uint256 subgraphAllocatedTokens = _getSubgraphAllocatedTokens(_subgraphDeploymentID); if (subgraphAllocatedTokens == 0) { - return (0, accRewardsForSubgraph); + // No allocations to distribute to, return stored value (no pending updates possible) + return (subgraph.accRewardsPerAllocatedToken, accRewardsForSubgraph); } uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph.mul(FIXED_POINT_SCALING_FACTOR).div( @@ -443,29 +397,32 @@ contract RewardsManager is // -- Internal Helpers -- /** - * @notice Calculate new rewards and claimability state for a subgraph - * @dev Returns the new rewards based on signal and the condition indicating why rewards - * may not be claimable (SUBGRAPH_DENIED, BELOW_MINIMUM_SIGNAL, or NONE if claimable). + * @notice Get subgraph rewards state including effective reclaim condition + * @dev Determines claimability with priority: SUBGRAPH_DENIED > BELOW_MINIMUM_SIGNAL > NO_ALLOCATION > NONE + * When multiple conditions apply, prefers conditions with configured reclaim addresses. * @param _subgraphDeploymentID Subgraph deployment - * @return newRewards The rewards that would accrue based on signal (may not be claimable) - * @return signalledTokens The subgraph's current signal - * @return condition The condition: NONE if claimable, otherwise the denial reason + * @return newRewards Rewards accumulated since last snapshot + * @return subgraphAllocatedTokens Total tokens allocated (0 if condition is not NONE) + * @return condition The effective condition for reclaim routing (NONE if claimable) */ function _getSubgraphRewardsState( bytes32 _subgraphDeploymentID - ) private view returns (uint256 newRewards, uint256 signalledTokens, bytes32 condition) { + ) private view returns (uint256 newRewards, uint256 subgraphAllocatedTokens, bytes32 condition) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; - signalledTokens = curation().getCurationPoolTokens(_subgraphDeploymentID); + uint256 signalledTokens = curation().getCurationPoolTokens(_subgraphDeploymentID); uint256 accRewardsPerSignalDelta = getAccRewardsPerSignal().sub(subgraph.accRewardsPerSignalSnapshot); newRewards = accRewardsPerSignalDelta.mul(signalledTokens).div(FIXED_POINT_SCALING_FACTOR); + subgraphAllocatedTokens = _getSubgraphAllocatedTokens(_subgraphDeploymentID); - if (isDenied(_subgraphDeploymentID)) { - condition = RewardsCondition.SUBGRAPH_DENIED; - } else if (signalledTokens < minimumSubgraphSignal) { - condition = RewardsCondition.BELOW_MINIMUM_SIGNAL; - } else { - condition = RewardsCondition.NONE; - } + condition = isDenied(_subgraphDeploymentID) ? RewardsCondition.SUBGRAPH_DENIED : RewardsCondition.NONE; + if ( + signalledTokens < minimumSubgraphSignal && + (condition == RewardsCondition.NONE || reclaimAddresses[condition] == address(0)) + ) condition = RewardsCondition.BELOW_MINIMUM_SIGNAL; + if ( + subgraphAllocatedTokens == 0 && + (condition == RewardsCondition.NONE || reclaimAddresses[condition] == address(0)) + ) condition = RewardsCondition.NO_ALLOCATION; } /** @@ -512,21 +469,93 @@ contract RewardsManager is return newAccRewardsPerSignal; } + /** + * @dev Internal function that updates subgraph reward accumulators. + * Shared logic for both signal and allocation update hooks. + * + * @param subgraph Storage pointer to the subgraph + * @param _subgraphDeploymentID The subgraph deployment ID + * @param accRewardsPerSignal Current global rewards per signal + * @param accRewardsForSubgraph Current subgraph accumulated rewards + * @param accRewardsPerAllocatedToken Current rewards per allocated token + * @return newAccRewardsForSubgraph Updated subgraph accumulated rewards + * @return newAccRewardsPerAllocatedToken Updated rewards per allocated token + */ + function _updateSubgraphRewards( + Subgraph storage subgraph, + bytes32 _subgraphDeploymentID, + uint256 accRewardsPerSignal, + uint256 accRewardsForSubgraph, + uint256 accRewardsPerAllocatedToken + ) internal returns (uint256 newAccRewardsForSubgraph, uint256 newAccRewardsPerAllocatedToken) { + ( + uint256 rewardsSinceSignalSnapshot, + uint256 subgraphAllocatedTokens, + bytes32 condition + ) = _getSubgraphRewardsState(_subgraphDeploymentID); + subgraph.accRewardsPerSignalSnapshot = accRewardsPerSignal; + + // Calculate undistributed: rewards accumulated but not yet distributed to allocations. + // Will be just rewards since last snapshot for subgraphs that have had onSubgraphSignalUpdate or + // onSubgraphAllocationUpdate called since upgrade; + // can include non-zero (original) accRewardsForSubgraph - accRewardsForSubgraphSnapshot for + // subgraphs that have not had either hook called since upgrade. + uint256 undistributedRewards = accRewardsForSubgraph.sub(subgraph.accRewardsForSubgraphSnapshot).add( + rewardsSinceSignalSnapshot + ); + + if (condition != RewardsCondition.NONE) { + _reclaimRewards(condition, undistributedRewards, address(0), address(0), _subgraphDeploymentID); + undistributedRewards = 0; + newAccRewardsForSubgraph = accRewardsForSubgraph; + } else { + newAccRewardsForSubgraph = accRewardsForSubgraph.add(rewardsSinceSignalSnapshot); + subgraph.accRewardsForSubgraph = newAccRewardsForSubgraph; + } + + subgraph.accRewardsForSubgraphSnapshot = newAccRewardsForSubgraph; + + newAccRewardsPerAllocatedToken = accRewardsPerAllocatedToken; + if (undistributedRewards != 0 && subgraphAllocatedTokens != 0) { + newAccRewardsPerAllocatedToken = accRewardsPerAllocatedToken.add( + undistributedRewards.mul(FIXED_POINT_SCALING_FACTOR).div(subgraphAllocatedTokens) + ); + subgraph.accRewardsPerAllocatedToken = newAccRewardsPerAllocatedToken; + } + } + /** * @inheritdoc IRewardsManager * @dev Must be called before `signalled GRT` on a subgraph changes. * Hook called from the Curation contract on mint() and burn() + * + * ## Claimability Behavior + * + * When a subgraph is not claimable (denied, below minimum signal, or no allocations): + * - Rewards are reclaimed immediately with the appropriate reason + * - `accRewardsForSubgraph` is NOT updated (rewards go to reclaim, not accumulator) + * + * When claimable (not denied, above minimum signal, has allocations): + * - Rewards are added to `accRewardsForSubgraph` for later distribution via `onSubgraphAllocationUpdate` */ - function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external override returns (uint256) { + function onSubgraphSignalUpdate( + bytes32 _subgraphDeploymentID + ) external override returns (uint256 accRewardsForSubgraph) { // Called since `total signalled GRT` will change - updateAccRewardsPerSignal(); + uint256 accRewardsPerSignal = updateAccRewardsPerSignal(); - // Updates the accumulated rewards for a subgraph Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; - uint256 accRewardsForSubgraph = getAccRewardsForSubgraph(_subgraphDeploymentID); - subgraph.accRewardsForSubgraph = accRewardsForSubgraph; - subgraph.accRewardsPerSignalSnapshot = accRewardsPerSignal; - return accRewardsForSubgraph; + accRewardsForSubgraph = subgraph.accRewardsForSubgraph; + + if (subgraph.accRewardsPerSignalSnapshot == accRewardsPerSignal) return accRewardsForSubgraph; + + (accRewardsForSubgraph, ) = _updateSubgraphRewards( + subgraph, + _subgraphDeploymentID, + accRewardsPerSignal, + accRewardsForSubgraph, + subgraph.accRewardsPerAllocatedToken + ); } /** @@ -535,59 +564,47 @@ contract RewardsManager is * * ## Claimability Behavior * - * When a subgraph is not claimable (denied or below minimum signal): - * - `accRewardsPerAllocatedToken` is NOT updated (frozen) - * - New rewards are reclaimed with the appropriate reason (SUBGRAPH_DENIED or BELOW_MINIMUM_SIGNAL) - * - `accRewardsPerSignalSnapshot` is updated to prevent double-reclaim + * When a subgraph is not claimable (denied, below minimum signal, or no allocations): + * - Rewards are reclaimed immediately with the appropriate reason + * - `accRewardsForSubgraph` is NOT updated (rewards go to reclaim, not accumulator) + * - `accRewardsPerAllocatedToken` does NOT increase * - * When claimable: - * - `accRewardsForSubgraph` and `accRewardsPerAllocatedToken` are updated normally - * - Allocations can claim their proportional share + * When claimable (not denied, above minimum signal, has allocations): + * - Rewards are added to `accRewardsForSubgraph` + * - `accRewardsPerAllocatedToken` increases (rewards distributable to allocations) * - * @return accRewardsPerAllocatedToken Current `accRewardsPerAllocatedToken` (frozen while subgraph is not claimable) + * @return accRewardsPerAllocatedToken Current `accRewardsPerAllocatedToken` */ function onSubgraphAllocationUpdate( bytes32 _subgraphDeploymentID ) public override returns (uint256 accRewardsPerAllocatedToken) { Subgraph storage subgraph = subgraphs[_subgraphDeploymentID]; - (uint256 newRewards, uint256 signalledTokens, bytes32 condition) = _getSubgraphRewardsState( - _subgraphDeploymentID - ); - subgraph.accRewardsPerSignalSnapshot = getAccRewardsPerSignal(); + uint256 accRewardsPerSignal = updateAccRewardsPerSignal(); + uint256 accRewardsForSubgraph = subgraph.accRewardsForSubgraph; accRewardsPerAllocatedToken = subgraph.accRewardsPerAllocatedToken; - if (newRewards == 0) return accRewardsPerAllocatedToken; - // Fallback: if denied but no reclaim address, try BELOW_MINIMUM_SIGNAL instead + // Return early to save gas if both snapshots are up-to-date if ( - condition == RewardsCondition.SUBGRAPH_DENIED && - reclaimAddresses[condition] == address(0) && - signalledTokens < minimumSubgraphSignal - ) { - condition = RewardsCondition.BELOW_MINIMUM_SIGNAL; - } - - if (condition != RewardsCondition.NONE) { - _reclaimRewards(condition, newRewards, address(0), address(0), _subgraphDeploymentID); - return accRewardsPerAllocatedToken; - } - - uint256 subgraphAllocatedTokens = _getSubgraphAllocatedTokens(_subgraphDeploymentID); - if (subgraphAllocatedTokens == 0) { - _reclaimRewards(RewardsCondition.NO_ALLOCATION, newRewards, address(0), address(0), _subgraphDeploymentID); - return accRewardsPerAllocatedToken; - } - - uint256 accRewardsForSubgraph = subgraph.accRewardsForSubgraph.add(newRewards); - accRewardsPerAllocatedToken = accRewardsPerAllocatedToken.add( - newRewards.mul(FIXED_POINT_SCALING_FACTOR).div(subgraphAllocatedTokens) + subgraph.accRewardsPerSignalSnapshot == accRewardsPerSignal && + subgraph.accRewardsForSubgraphSnapshot == accRewardsForSubgraph + ) return accRewardsPerAllocatedToken; + + (, accRewardsPerAllocatedToken) = _updateSubgraphRewards( + subgraph, + _subgraphDeploymentID, + accRewardsPerSignal, + accRewardsForSubgraph, + accRewardsPerAllocatedToken ); - subgraph.accRewardsForSubgraph = accRewardsForSubgraph; - subgraph.accRewardsPerAllocatedToken = accRewardsPerAllocatedToken; - subgraph.accRewardsForSubgraphSnapshot = accRewardsForSubgraph; } - /// @inheritdoc IRewardsManager + /** + * @inheritdoc IRewardsManager + * @dev Returns claimable rewards based on current accumulator state. + * Reflects deterministic exclusions (denied, below minimum signal, no allocations) but NOT indexer eligibility. + * Indexer eligibility is checked at claim time and can change independently of reward accrual. + */ function getRewards(address _rewardsIssuer, address _allocationID) external view override returns (uint256) { require( _rewardsIssuer == address(staking()) || _rewardsIssuer == address(subgraphService), diff --git a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol index 1cf817f2d..aa7d32eba 100644 --- a/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol +++ b/packages/interfaces/contracts/contracts/rewards/IRewardsManager.sol @@ -95,25 +95,12 @@ interface IRewardsManager { ); /** - * @dev Stores accumulated rewards and snapshots related to a particular SubgraphDeployment - * - * ## Snapshot Semantics - * - * Snapshots prevent double-counting. After each update, snapshot = current value. - * New rewards = current - snapshot (delta since last update). - * - * ## Claimability - * - * When a subgraph is not claimable (denied or below minimum signal): - * - `accRewardsForSubgraph` FREEZES (no new rewards credited) - * - `accRewardsPerAllocatedToken` FREEZES (allocation-level) - * - New rewards are reclaimed via `onSubgraphAllocationUpdate()` - * - `accRewardsPerSignalSnapshot` still updates to prevent double-counting - * - * @param accRewardsForSubgraph Accumulated rewards for the subgraph - * @param accRewardsForSubgraphSnapshot Snapshot of accumulated rewards for the subgraph - * @param accRewardsPerSignalSnapshot Snapshot of accumulated rewards per signal - * @param accRewardsPerAllocatedToken Accumulated rewards per allocated token + * @dev Accumulated rewards and snapshots for a SubgraphDeployment. + * See `onSubgraphAllocationUpdate()` for claimability behavior. + * @param accRewardsForSubgraph Total rewards allocated to this subgraph (always increases) + * @param accRewardsForSubgraphSnapshot Snapshot for calculating new rewards since last update + * @param accRewardsPerSignalSnapshot Snapshot of global accRewardsPerSignal at last update + * @param accRewardsPerAllocatedToken Per-token rewards for allocations (frozen when not claimable) */ struct Subgraph { uint256 accRewardsForSubgraph; @@ -324,12 +311,13 @@ interface IRewardsManager { * @dev Must be called before allocation on a subgraph changes. * Hook called from the Staking contract on allocate() and close() * - * ## Denial Behavior + * ## Non-Claimable Behavior * - * When the subgraph is denied: - * - Does NOT update `accRewardsPerAllocatedToken` (keeps it frozen) - * - Reclaims new rewards accrued since last snapshot (if reclaim address configured) - * - Always updates `accRewardsForSubgraphSnapshot` to prevent double-counting + * When the subgraph is not claimable (denied or below minimum signal): + * - `accRewardsForSubgraph` increases (rewards continue accruing to the subgraph) + * - `accRewardsPerAllocatedToken` does NOT increase (rewards not distributed to allocations) + * - Accrued rewards are reclaimed (if reclaim address configured) + * - All snapshots update to track the reclaimed amounts * * @param subgraphDeploymentID Subgraph deployment * @return Accumulated rewards per allocated token for a subgraph diff --git a/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol b/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol index cb53814fb..2a895c8ce 100644 --- a/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol +++ b/packages/interfaces/contracts/contracts/rewards/RewardsCondition.sol @@ -5,83 +5,50 @@ pragma solidity ^0.7.6 || ^0.8.0; /** * @title RewardsCondition * @author Edge & Node - * @notice Canonical definitions for reward condition reasons - * @dev Uses bytes32 identifiers (like OpenZeppelin roles) to allow decentralized extension. - * New reasons can be defined by any contract without modifying this library. - * These constants provide standard reasons used across The Graph Protocol. - * - * Note: bytes32(0) is reserved as NONE and cannot be used as a reclaim reason. This design prevents: - * 1. Accidental misconfiguration from setting a reclaim address for an invalid/uninitialized reason - * 2. Invalid reclaim operations when a condition identifier was not properly set - * The zero value serves as a sentinel to catch configuration errors at the protocol level. - * - * How condition reasons are used depends on the specific implementation. Different contracts - * may handle multiple applicable conditions differently. + * @notice Canonical condition identifiers for reward reclaim reasons. + * @dev bytes32(0) is reserved as NONE and cannot be used as a reclaim reason. + * See docs/RewardConditions.md for full handling details. */ library RewardsCondition { - /** - * @notice No condition - rewards can be claimed normally - * @dev Used as the default/initial state when no blocking condition applies - */ + /// @notice No condition - rewards claimable normally. Cannot be used as reclaim reason. bytes32 public constant NONE = bytes32(0); /** - * @notice Condition - indexer failed eligibility check - * @dev Indexer is not eligible to receive rewards according to eligibility oracle + * @notice Indexer failed eligibility check at claim time + * @dev Checked after SUBGRAPH_DENIED; skipped if subgraph denial already reclaimed */ bytes32 public constant INDEXER_INELIGIBLE = keccak256("INDEXER_INELIGIBLE"); /** - * @notice Condition - subgraph is on denylist - * @dev Subgraph deployment has been denied rewards by availability oracle + * @notice Subgraph is on denylist + * @dev Handled at both subgraph level (reclaim) and allocation level (defer) */ bytes32 public constant SUBGRAPH_DENIED = keccak256("SUBGRAPH_DENIED"); - /** - * @notice Condition - POI submitted too late - * @dev Proof of Indexing was submitted after the staleness deadline - */ + /// @notice POI submitted after staleness deadline bytes32 public constant STALE_POI = keccak256("STALE_POI"); - /** - * @notice Condition - allocation has no tokens - * @dev Altruistic allocation (zero tokens) is not eligible for rewards - */ + /// @notice Altruistic allocation (no curation signal) - not currently used in reclaim logic bytes32 public constant ALTRUISTIC_ALLOCATION = keccak256("ALTRUISTIC_ALLOCATION"); - /** - * @notice Condition - no POI provided - * @dev Allocation closed without providing a Proof of Indexing - */ + /// @notice POI is bytes32(0) bytes32 public constant ZERO_POI = keccak256("ZERO_POI"); - /** - * @notice Condition - allocation created in current epoch - * @dev Allocation must exist for at least one full epoch to earn rewards - */ + /// @notice Allocation created in current epoch (deferred, not reclaimed) bytes32 public constant ALLOCATION_TOO_YOUNG = keccak256("ALLOCATION_TOO_YOUNG"); - /** - * @notice Condition - allocation closed without POI - * @dev Allocation was closed without providing a Proof of Indexing - */ + /// @notice Allocation closed - uncollected rewards reclaimed bytes32 public constant CLOSE_ALLOCATION = keccak256("CLOSE_ALLOCATION"); /** - * @notice Condition - no curation signal exists - * @dev Total signalled tokens is zero, so rewards cannot be distributed + * @notice No curation signal exists (global level) + * @dev Triggered in updateAccRewardsPerSignal when total signalled tokens = 0 */ bytes32 public constant NO_SIGNAL = keccak256("NO_SIGNAL"); - /** - * @notice Condition - subgraph signal below minimum threshold - * @dev Subgraph has curation signal but it's below the minimumSubgraphSignal threshold - */ + /// @notice Subgraph signal below minimumSubgraphSignal threshold bytes32 public constant BELOW_MINIMUM_SIGNAL = keccak256("BELOW_MINIMUM_SIGNAL"); - /** - * @notice Condition - no allocations exist for subgraph - * @dev Subgraph has no indexer allocations, so rewards cannot be distributed for this subgraph - */ + /// @notice No allocations exist for subgraph bytes32 public constant NO_ALLOCATION = keccak256("NO_ALLOCATION"); } From 6d59cb8d5190836323d084983df036a9387541ec Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:55:23 +0000 Subject: [PATCH 41/43] refactor: replace bytes32(0) with RewardsCondition.NONE constant Use semantic constant consistent with rest of code. --- .../contracts-test/tests/unit/rewards/rewards-reclaim.test.ts | 4 ++-- packages/contracts/contracts/rewards/RewardsManager.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts index 9825b5294..4bce15917 100644 --- a/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts +++ b/packages/contracts-test/tests/unit/rewards/rewards-reclaim.test.ts @@ -125,9 +125,9 @@ describe('Rewards - Reclaim Addresses', () => { await expect(tx).revertedWith('Only Controller governor') }) - it('should reject setting reclaim address for bytes32(0)', async function () { + it('should reject setting reclaim address for NONE', async function () { const tx = rewardsManager.connect(governor).setReclaimAddress(HashZero, reclaimWallet.address) - await expect(tx).revertedWith('Cannot set reclaim address for (bytes32(0))') + await expect(tx).revertedWith('Cannot set reclaim address for NONE') }) it('should set eligibility reclaim address if governor', async function () { diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index 49a852abb..be14d359a 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -212,7 +212,7 @@ contract RewardsManager is */ function setReclaimAddress(bytes32 reason, address newAddress) external override onlyGovernor { // solhint-disable-next-line gas-small-strings - require(reason != bytes32(0), "Cannot set reclaim address for (bytes32(0))"); + require(reason != RewardsCondition.NONE, "Cannot set reclaim address for NONE"); address oldAddress = reclaimAddresses[reason]; From 74e09954faffa4ec34a73a4a315c93805a937ed7 Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:25:44 +0000 Subject: [PATCH 42/43] feat: update reward accumulators on deferred POI claims Call onSubgraphAllocationUpdate when _presentPoi exits early for ALLOCATION_TOO_YOUNG or SUBGRAPH_DENIED conditions. Keeps reward tracking and reclaim accumulation current even when rewards aren't collected, ensuring accumulation or reclaims in predictable period. Also cache currentEpoch to avoid duplicate external call. --- .../contracts/utilities/AllocationManager.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/subgraph-service/contracts/utilities/AllocationManager.sol b/packages/subgraph-service/contracts/utilities/AllocationManager.sol index b51b367ec..a332a57c8 100644 --- a/packages/subgraph-service/contracts/utilities/AllocationManager.sol +++ b/packages/subgraph-service/contracts/utilities/AllocationManager.sol @@ -191,6 +191,8 @@ abstract contract AllocationManager is IAllocation.State memory allocation = _allocations.get(_allocationId); require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId)); _allocations.presentPOI(_allocationId); // Always record POI presentation to prevent staleness + + uint256 currentEpoch = _graphEpochManager().currentEpoch(); // Scoped for stack management { // Determine rewards condition @@ -199,8 +201,7 @@ abstract contract AllocationManager is else if (_poi == bytes32(0)) condition = RewardsCondition.ZERO_POI; // solhint-disable-next-line gas-strict-inequalities - else if (_graphEpochManager().currentEpoch() <= allocation.createdAtEpoch) - condition = RewardsCondition.ALLOCATION_TOO_YOUNG; + else if (currentEpoch <= allocation.createdAtEpoch) condition = RewardsCondition.ALLOCATION_TOO_YOUNG; else if (_graphRewardsManager().isDenied(allocation.subgraphDeploymentId)) condition = RewardsCondition.SUBGRAPH_DENIED; @@ -214,8 +215,12 @@ abstract contract AllocationManager is ); // Early return skips the overallocation check intentionally to avoid loss of uncollected rewards - if (condition == RewardsCondition.ALLOCATION_TOO_YOUNG || condition == RewardsCondition.SUBGRAPH_DENIED) + if (condition == RewardsCondition.ALLOCATION_TOO_YOUNG || condition == RewardsCondition.SUBGRAPH_DENIED) { + // Keep reward and reclaim accumulation current even if rewards are not collected + _graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId); + return 0; + } bool rewardsReclaimable = condition == RewardsCondition.STALE_POI || condition == RewardsCondition.ZERO_POI; if (rewardsReclaimable) _graphRewardsManager().reclaimRewards(condition, _allocationId); @@ -246,7 +251,7 @@ abstract contract AllocationManager is tokensDelegationRewards, _poi, _poiMetadata, - _graphEpochManager().currentEpoch() + currentEpoch ); } From 16dbd7373a57e5964ebd6c6643a7d95bd688857d Mon Sep 17 00:00:00 2001 From: Rembrandt Kuipers <50174308+RembrandtK@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:12:33 +0000 Subject: [PATCH 43/43] fixup! fix: consistent reward reclaim for non-claimable subgraphs in both hooks --- packages/contracts/contracts/rewards/RewardsManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts/contracts/rewards/RewardsManager.sol b/packages/contracts/contracts/rewards/RewardsManager.sol index be14d359a..e914e2fdb 100644 --- a/packages/contracts/contracts/rewards/RewardsManager.sol +++ b/packages/contracts/contracts/rewards/RewardsManager.sol @@ -470,7 +470,7 @@ contract RewardsManager is } /** - * @dev Internal function that updates subgraph reward accumulators. + * @notice Internal function that updates subgraph reward accumulators. * Shared logic for both signal and allocation update hooks. * * @param subgraph Storage pointer to the subgraph