Skip to content

Indexing payments management audit fix hybrid reduced scope#1312

Draft
RembrandtK wants to merge 21 commits intoindexing-payments-management-auditfrom
indexing-payments-management-audit-fix-reduced
Draft

Indexing payments management audit fix hybrid reduced scope#1312
RembrandtK wants to merge 21 commits intoindexing-payments-management-auditfrom
indexing-payments-management-audit-fix-reduced

Conversation

@RembrandtK
Copy link
Copy Markdown
Contributor

@RembrandtK RembrandtK commented Apr 1, 2026

Filter for files in audit scope:

git diff indexing-payments-management-audit --name-status --diff-filter=d -- \
    '*.sol' ':!*/test*' ':!*.t.sol' ':!*Helper.sol' ':!*/mock*' ':!*/toolshed/*'`

Audit findings are individually tracked in packages/issuance/audits/PR1301/. Responses (after the --- separator) are added in relevant commits.

Trust Model: callback hardening & explicit eligibility

  • Primary files: RecurringCollector.sol, IAgreementCollector.sol (new), IAgreementOwner.sol
  • TRST-H-1, H-2, H-4: Payer callbacks are now gas bounded and cannot block or revert the collect path. RAM no longer needs an approval callback because RC has advance approval.
  • TRST-L-1, SR-4: Eligibility checking promoted to an agreement term; both parties know and agree up front, no surprise gating after acceptance.
  • TRST-L-2, L-5: Offers committed on-chain before acceptance; terms verified against stored hashes, alongside the existing RCA framework.
  • Overflow-prone token/duration terms rejected at acceptance.

Escrow Management: threshold/margin model, JIT removed

  • Primary file: RecurringAgreementManager.sol
  • TRST-M-2, M-3: More robust threshold and margin based degradation.
  • TRST-H-3: re-read escrow balance before acting (stale snapshot fix).
  • TRST-M-1: Minimum thaw fraction prevents dust-thaw griefing.

Operational Hardening: pause & emergency controls

  • Touches: RecurringCollector.sol, RecurringAgreementManager.sol, IEmergencyRoleControl.sol (new), Authorizable.sol
  • TRST-L-3: Pause mechanism on RecurringCollector (upgradeable).
  • Emergency role control + eligibility oracle escape hatch on RAM.

Acknowledged without code changes (TRST-CR-1/3, L-4, R-1, SR-1/2/3)

Incidental

  • RAM storage restructure (RecurringAgreementManager.sol): Storage moves from flat to per-collector keying since agreement IDs cannot be relied on to be globally unique. Mixed with switching to RC-reported max-next-claim for tracking lifetime.
  • SubgraphService (SubgraphService.sol, AllocationHandler.sol, IndexingAgreement.sol, SubgraphServiceStorage.sol): Resize-to-zero instead of force-close for allocations with active agreements; optionally (configuration controlled) block closing allocations with active agreements.
  • RewardsManager: Option to revert collection for ineligible indexers rather than have rewards be immediately lost.

Add packages/testing with real PaymentsEscrow, RecurringCollector,
IssuanceAllocator, and RecurringAgreementManager deployed together.
Gas results confirm 21-27x headroom on callback budgets.

Improve test coverage across all packages:
- horizon: RecurringCollector 86%→100% lines, 76%→100% functions
- subgraph-service: 92.86%→94.76% branches
- issuance: restore 100% coverage, remove dead getPageBytes16

Fix coverage scripts to print summary tables to stdout.
Trust Security audit (2026-03-03 to 2026-03-19) findings extracted from
Graph_PR1301_v01.pdf into PR1301/ directory with README index.
Includes auditor-supplied staleSnap PoC test.
Replace tempJit/fullReserveMargin with two configurable parameters:
minOnDemandBasisThreshold (default 128) and minFullBasisMargin
(default 16). Effective basis limited based on spare balance relative to
sumMaxNextClaimAll using strict <.

Delete staleSnap.t.sol (PoC scenario no longer applicable after
threshold-based degradation replaces tempJit).
…ST-M-1)

Added configurable `minThawFraction` (uint8, proportion of 256, default
16 = 6.25%) that skips thaws when the excess above max is below
`sumMaxNextClaim * fraction / 256` for the (collector, provider) pair.
Add revertOnIneligible flag to RewardsManager. When true, ineligible
indexers cause takeRewards to revert (blocking POI presentation and
preserving rewards for future collection). When false (default),
ineligible indexers have rewards reclaimed but takeRewards succeeds.
Over-allocated and stale allocations are now resized to zero tokens
instead of being force-closed. This keeps the allocation open so that
any bound indexing agreement remains active. The allocation stays as
a stakeless (altruistic) allocation rather than being deleted.

Changes:
- AllocationHandler.presentPOI: call _resizeAllocation(0) instead of
  _closeAllocation when over-allocated
- SubgraphService.closeStaleAllocation: resize to zero instead of
  _onCloseAllocation + _closeAllocation
- SubgraphService._collectIndexingRewards: remove _onCloseAllocation
  call after over-allocation (allocation stays open)
- Extract _resizeAllocation internal helper from resizeAllocation
Add a governor-controlled guard that prevents closing an allocation
when it has an active indexing agreement. When enabled, stopService
reverts with SubgraphServiceAllocationHasActiveAgreement instead of
auto-canceling the agreement.

- Add SubgraphServiceV2Storage with blockClosingAllocationWithActiveAgreement
- Add setter/getter and BlockClosingAllocationWithActiveAgreementSet event
- IndexingAgreement.onCloseAllocation: revert if active when guard enabled,
  otherwise cancel as ServiceProvider (forceClosed param removed since
  closeStaleAllocation now resizes instead of closing)
…terms

Add checked-arithmetic overflow guard on accept and update paths to
prevent agreements whose maxOngoingTokensPerSecond × maxSecondsPerCollection
product would overflow during collection, with 1024× headroom margin.
…ancel (TRST-L-2, L-5)

Payers pre-store RCA/RCAU offers on-chain; acceptance verifies the
stored hash instead of calling back to the payer contract. This
eliminates the callback-based approval flow and its associated
trust assumptions.

Escrow reservation uses max(active, pending) instead of additive
current + pending, so only the larger of the two term sets is
reserved (L-2). The claim formula caps the collection window at
min(remaining time, maxSecondsPerCollection), eliminating overestimates
for agreements near their deadline (L-5).

Scoped getMaxNextClaim lets callers query active terms, pending
offers, or both. Payer-initiated cancel targets specific terms by
hash and scope (ACTIVE/PENDING), with a guard preventing deletion
of offers whose terms are already active.
…TRST-H-1, H-2, H-4, L-1, SR-4)

Payer callbacks (beforeCollection, afterCollection, isEligible) now use
gas-capped low-level call/staticcall instead of try/catch, preventing
gas siphoning via the 63/64 rule (H-1) and caller-side ABI decode
reverts from malformed returndata (H-2). A gasleft() guard before each
callback reverts when insufficient gas remains (L-1). Failed callbacks
emit PayerCallbackFailed for off-chain monitoring (SR-4).

Eligibility checking is now opt-in via a conditions bitmask on RCA/RCAU.
CONDITION_ELIGIBILITY_CHECK gates the IProviderEligibility staticcall at
collection time. At acceptance, _requireEligibilityCapability validates
ERC-165 support — an EOA without code cannot set this flag, closing the
EIP-7702 attack vector where an EOA later acquires code to block
collection (H-4).
Replace getCollectors, getCollectorProviders, getProviderAgreements
(and their paginated overloads), getCollectorProviderCount,
getProviderAgreementCount, getPairAgreementCount, and
getTotalAgreementCount with indexed accessors: getCollectorAt,
getProviderCount/At, getAgreementCount/At, and getEscrowSnap.

Remove EnumerableSetUtil import and using statements. The getPage and
getPageBytes16 helpers inline substantial library code at each call
site — six call sites pushed RAM over the 24576-byte Spurious Dragon
deployable bytecode limit.

Also switches view param types from IRecurringCollector to
IAgreementCollector and renames getSumMaxNextClaimAll to
getSumMaxNextClaim to match the updated IRecurringAgreements interface.

Does not compile: new accessors reference future hierarchical storage
($.collectors[c].providers[p], $.collectorSet) introduced in a later
commit.
…atch

Add IEmergencyRoleControl implementation allowing pause guardians to
revoke any non-governor role as a fast-response emergency measure.
Governor role is excluded to prevent locking out governance.

Also adds emergencyClearEligibilityOracle for fail-open when the oracle
is broken, extracts _setProviderEligibilityOracle private helper to
share between the governor setter and emergency clear, and removes the
afterAgreementStateChange stub (obsolete for thin pass-through).
…ntCollector pass-throughs

offerAgreement now calls collector.offer() with opaque offer data instead
of constructing RCA hashes and storage directly. cancelAgreement calls
collector.cancel() with a version hash and options bitmask instead of
routing through the data service.

Both forward to the collector then reconcile locally — the collector owns
agreement state, RAM just caches maxNextClaim for escrow accounting.

Does not compile: new function bodies reference _reconcileAgreement with
a collector parameter that is introduced in a later commit.
…d revokeOffer

These functions are subsumed by the IAgreementCollector offer/cancel
interface: updates are offered via offerAgreement with an update offer
type, and revocations use cancelAgreement targeting the relevant terms
hash. The collector now owns offer/update lifecycle state.

Also add forceRemoveAgreement.
Replace flat storage mappings with hierarchical CollectorData and
CollectorProviderData structs. Agreements are now namespaced under their
collector ($.collectors[c].agreements[id]) instead of a global mapping,
preventing cross-collector ID collisions.

Add _getAgreementProvider for lazy discovery: on first encounter, validates
collector role, reads agreement details, checks payer/dataService, and
registers in tracking sets. Subsequent accesses use the cached provider.

Rewrite internal functions to use the hierarchical storage:
- _reconcileAgreement: discover-then-reconcile flow
- _removeAgreement: replaces _deleteAgreement
- _reconcileProvider: replaces _reconcilePairTracking
- _setAgreementMaxNextClaim: takes CollectorProviderData, reads old value
- _escrowMinMax: takes sumMaxNextClaim value parameter
- _providerEscrowDeficit: takes CollectorProviderData
- _reconcileProviderEscrow: replaces _updateEscrow, uses cpd
- _setEscrowSnap: takes CollectorProviderData

Add forceRemoveAgreement operator escape hatch. Update beforeCollection,
afterCollection, reconcileAgreement, and reconcileProvider to use the
new collector-parameterised internal functions.

Reduces AgreementInfo to {provider, maxNextClaim}.
@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 1, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedglob@​11.0.3998510050100
Addedlodash@​4.17.2175828792100
Addedjson5@​2.2.310010010082100
Addedgraphql-tag@​2.12.610010010083100
Addedyargs@​17.7.29910010087100
Addedp-queue@​6.6.29910010088100
Addedtypescript@​5.9.31001009010090
Addedprettier@​3.8.1901009790100
Addedgraphql@​16.11.09710010097100

View full report

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 1, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn Critical
Critical CVE: npm fast-xml-parser has an entity encoding bypass via regex injection in DOCTYPE entity names

CVE: GHSA-m7jm-9gc2-mpf2 fast-xml-parser has an entity encoding bypass via regex injection in DOCTYPE entity names (CRITICAL)

Affected versions: >= 5.0.0 < 5.3.5; >= 4.1.3 < 4.5.4

Patched version: 5.3.5

From: pnpm-lock.yamlnpm/fast-xml-parser@5.2.5

ℹ Read more on: This package | This alert | What is a critical CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known critical CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/fast-xml-parser@5.2.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm entities is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: pnpm-lock.yamlnpm/entities@4.5.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/entities@4.5.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Protestware or unwanted behavior: npm es5-ext

Note: The script attempts to run a local post-install script, which could potentially contain malicious code. The error handling suggests that it is designed to fail silently, which is a common tactic in malicious scripts.

From: pnpm-lock.yamlnpm/es5-ext@0.10.64

ℹ Read more on: This package | This alert | What is protestware?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Consider that consuming this package may come along with functionality unrelated to its primary purpose.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/es5-ext@0.10.64. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm markdown-it is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: pnpm-lock.yamlnpm/markdown-it@14.1.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/markdown-it@14.1.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Convert RecurringCollector to TransparentUpgradeableProxy pattern with
ERC-7201 namespaced storage. Authorizable storage also converted to
ERC-7201 for proxy compatibility.
Add pause guardian pattern gating accept, update, collect, cancel, and
offer behind whenNotPaused. This provides a middle layer between the
RAM-level pause (agreement lifecycle only) and the Controller-level
nuclear pause (all escrow operations protocol-wide). The previous
approveAgreement pause-bypass vector no longer exists since callback-
based approval was replaced by stored-hash authorization (L-3).
@RembrandtK RembrandtK force-pushed the indexing-payments-management-audit-fix-reduced branch from 5ec30ff to bbe0195 Compare April 1, 2026 22:35
@openzeppelin-code
Copy link
Copy Markdown

openzeppelin-code bot commented Apr 1, 2026

Indexing payments management audit fix hybrid reduced scope

Generated at commit: 0bbb476f37f85d042927e84d8764fa58eb020ccf

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
3
5
0
15
39
62
Dependencies Critical
High
Medium
Low
Note
Total
0
0
0
0
0
0

For more details view the full report in OpenZeppelin Code Inspector

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 98.15951% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.86%. Comparing base (7405c9d) to head (0bbb476).

Files with missing lines Patch % Lines
...ntracts/payments/collectors/RecurringCollector.sol 96.09% 4 Missing and 4 partials ⚠️
.../contracts/agreement/RecurringAgreementManager.sol 99.41% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@                          Coverage Diff                           @@
##           indexing-payments-management-audit    #1312      +/-   ##
======================================================================
+ Coverage                               90.29%   90.86%   +0.56%     
======================================================================
  Files                                      81       81              
  Lines                                    5130     5274     +144     
  Branches                                 1084      949     -135     
======================================================================
+ Hits                                     4632     4792     +160     
+ Misses                                    465      453      -12     
+ Partials                                   33       29       -4     
Flag Coverage Δ
unittests 90.86% <98.15%> (+0.56%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@RembrandtK RembrandtK changed the base branch from main to indexing-payments-management-audit April 2, 2026 13:19
…celIndexingAgreement

cancelIndexingAgreement (introduced in a7fb875) required a valid
provision check (onlyValidProvision, later refactored to enforceService
with VALID_PROVISION | REGISTERED). Cancel should remain available
regardless of provision state. Combined with
blockClosingAllocationWithActiveAgreement (b124656), an indexer whose
provision drops below minimum is stuck in a catch-22: VALID_PROVISION
blocks cancel, and the active agreement blocks closing the allocation.

REGISTERED is not relevant to cancelling on accepted agreement either.
Changed to enforceService(indexer, DEFAULT). IndexingAgreement.cancel()
validates the caller and pause check remains.

Tests added (not all related to this issue):
- Cross-package integration lifecycle
- Cancel with below-minimum provision
- Horizon coverage gaps
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant