Skip to content

Indexing payments management audit fixed rebased#1331

Merged
RembrandtK merged 128 commits intomainfrom
indexing-payments-management-audit-fixed-rebased
May 9, 2026
Merged

Indexing payments management audit fixed rebased#1331
RembrandtK merged 128 commits intomainfrom
indexing-payments-management-audit-fixed-rebased

Conversation

@RembrandtK
Copy link
Copy Markdown
Contributor

@RembrandtK RembrandtK commented May 5, 2026

Rebase of #1301 (indexing-payments-management-audit) and #1325 (indexing-payments-management-audit-fix-2-light) onto current main.

For subsequent fixes see: #1334

Commits being compared

ORIG=indexing-payments-management-audit-pre-rebase   # pre-rebase tag
REB=2292e6ae8                                        # the rebase result
OLD_BASE=ddee12b11                                   # where ORIG branched off main
NEW_BASE=52b5356d3                                   # where REB branched off main

Drift introduced by main since the original merge base

23 files. Zero file-level overlap with the audit-branch surface, no behavioural interaction with anything under audit. Single substantive production-contract change:

git diff "$ORIG" "$REB" --name-status -- '*.sol'
# Output: M       packages/token-distribution/contracts/GraphTokenLockWallet.sol

This contract change was separately audited and is not in scope of this audit.

For reference, audit scope files (with exclusion added for token-distribution):

git diff $NEW_BASE $REB --name-status --diff-filter=d -- \
    '*.sol' ':!*/test*' ':!*.t.sol' ':!*Helper.sol' ':!*/mock*' ':!*/toolshed/*' \
    ':!packages/token-distribution'

Equivalence checks

# Sanity: the rebase moved ORIG from OLD_BASE to NEW_BASE.
git merge-base "$ORIG" "$NEW_BASE"                     # = OLD_BASE
git merge-base --is-ancestor "$OLD_BASE" "$NEW_BASE"   # exits 0
# The drift between ORIG and REB is exactly main's drift since OLD_BASE, nothing else changed.
diff <(git diff "$ORIG" "$REB") <(git diff "$OLD_BASE" "$NEW_BASE")
# expected: empty

matiasedgeandnode and others added 30 commits June 11, 2025 14:08
Fixes TRST-M-1 audit finding:

Wrong TYPEHASH string is used for agreement updates, limiting functionality.

* Fixed EIP712_RCAU_TYPEHASH to use correct uint64 types for deadline and endsAt fields (was incorrectly using uint256)
* This prevents signature verification failures for RecurringCollectionAgreementUpdate
Fixes TRST-M-2 audit finding:

Collection for an elapsed or canceled agreement could be wrong due to
temporal calculation inconsistencies between IndexingAgreement and
RecurringCollector layers.

* Replace isCollectable() with getCollectionInfo() that returns both collectability and duration
* Make RecurringCollector the single source of truth for temporal logic
* Update IndexingAgreement to call getCollectionInfo() once and pass duration to _tokensToCollect()
Fixes signature replay attack vulnerability where old signed RecurringCollectionAgreementUpdate
messages could be replayed to revert agreements to previous terms.

 ## Changes

- Add `nonce` field to RecurringCollectionAgreementUpdate struct (uint32)
- Add `updateNonce` field to AgreementData struct to track current nonce
- Add nonce validation in RecurringCollector.update() to ensure sequential updates
- Update EIP712_RCAU_TYPEHASH to include nonce field
- Add comprehensive tests for nonce validation and replay attack prevention
- Add RecurringCollectorInvalidUpdateNonce error for invalid nonce attempts

 ## Implementation Details

- Nonces start at 0 when agreement is accepted
- Each update must use current nonce + 1
- Nonce is incremented after successful update
- Uses uint32 for gas optimization (supports 4B+ updates per agreement)
- Single source of truth: nonce stored in AgreementData struct
Implements slippage protection mechanism to prevent silent token loss
during rate-limited collections in RecurringCollector agreements.

The implementation uses type(uint256).max convention to disable slippage
checks, providing users full control over acceptable token loss during
rate limiting.

Resolves audit finding TRST-L-5: "RecurringCollector silently reduces
collected tokens without user consent"
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
Signed-off-by: Tomás Migone <tomas@edgeandnode.com>
RembrandtK added 4 commits May 5, 2026 16:39
…GNED (TRST-L-7)

The v03 mitigation review on TRST-L-7 asks for it to be clarified that
when a payer is represented by a separate signer (Authorizable), the
signer — not the payer — must call cancel() with SCOPE_SIGNED for the
revocation to take effect against subsequent accept/update.

Updated the IAgreementCollector NatSpec on cancel() to spell out the
per-scope caller requirement, including that combining SCOPE_SIGNED
with SCOPE_PENDING / SCOPE_ACTIVE only makes sense when msg.sender is
both payer and signer. Mirrored the clarification in the RecurringCollector
implementation comment so the dev-doc and inline comment agree.
… safety margin (TRST-L-8)

The v03 mitigation review on TRST-L-8 asks for tests that ensure the
defined CALLBACK_GAS_OVERHEAD constant stays sufficient as the code
evolves. Existing tests cover the lower bound (precheck reverts under
tight gas) but do not assert the upper bound — that when the precheck
passes, the EIP-150 cap still forwards at least MAX_PAYER_CALLBACK_GAS
to the callee.

Added recordedBeforeCollectionGasleft / recordedAfterCollectionGasleft
to MockAgreementOwner, sampled at the first opcode of each callback,
and a regression test asserting both stay within 500 gas of MAX. The
current overhead constant lands the recorded value ~300 below MAX
(function dispatch overhead), leaving ~200 gas of headroom against the
500 tolerance — so an edit that adds pre-CALL Solidity overhead trips
the alarm before CALLBACK_GAS_OVERHEAD becomes outright insufficient.
…GIBILITY_CHECK (TRST-L-9)

The v03 mitigation review on TRST-L-9 asks for it to be documented that
turning CONDITION_ELIGIBILITY_CHECK on against an EOA payer is
considered fully trusting that payer, since EIP-7702 lets the EOA
attach a contract that returns false from isEligible() to block
collections at any time. The existing comment treated the flag as a
plain opt-in feature without naming the EOA-via-7702 caveat.

Expanded the NatSpec on the constant declaration so the trust boundary
is visible at the same site readers consult when deciding whether to
opt into the flag for a given payer.
…Details (TRST-R-14)

The v03 report's TRST-R-14 flags _getAgreementProvider passing 0 as the
index argument to IAgreementCollector.getAgreementDetails: that 0 is no
longer a placeholder, it now selects VERSION_CURRENT at the collector,
and the named constant communicates intent and survives any future
remapping of version indices.

Imports VERSION_CURRENT from the interface and substitutes it at the
single call site.
@RembrandtK RembrandtK self-assigned this May 5, 2026
@openzeppelin-code
Copy link
Copy Markdown

openzeppelin-code Bot commented May 5, 2026

Indexing payments management audit fixed rebased

Generated at commit: 675d39716add4192e0e65ecbd92c5a27f050d26a

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
3
5
0
14
38
60
Dependencies Critical
High
Medium
Low
Note
Total
0
0
0
0
0
0

For more details view the full report in OpenZeppelin Code Inspector

- subgraph-service: ScopedCancelActive integration test leaked the payer
  prank into post-cancel view-reads via resetPrank (startPrank). When
  fuzz picked rca.payer == SubgraphService's auto-deployed proxy admin
  (0x8816d8...), the verification reads on subgraphService reverted with
  ProxyDeniedAdminAccess. Switched to single-shot vm.prank for the cancel
  call and added a deterministic regression test using the failing seed.
- horizon: TRST-L-8's MAX_PAYER_CALLBACK_GAS margin test asserts the
  production gas overhead, but `forge coverage` disables the optimizer,
  legitimately growing the dispatch δ past tolerance. Skip under
  FOUNDRY_COVERAGE=1 (set by the package's coverage scripts); direct
  `forge coverage` invocations still fail loudly.
- foundry.toml (all 4 packages): exclude block-timestamp lint — pure
  noise across this codebase.
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 6, 2026

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 6, 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: Babel vulnerable to arbitrary code execution when compiling specifically crafted malicious code in npm babel-traverse

CVE: GHSA-67hx-6x53-jw92 Babel vulnerable to arbitrary code execution when compiling specifically crafted malicious code (CRITICAL)

Affected versions: >= 0

Patched version: No patched versions

From: pnpm-lock.yamlnpm/babel-traverse@6.26.0

ℹ 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/babel-traverse@6.26.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
Obfuscated code: npm @react-native/debugger-frontend is 96.0% likely obfuscated

Confidence: 0.96

Location: Package overview

From: pnpm-lock.yamlnpm/@react-native/debugger-frontend@0.81.4

ℹ 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/@react-native/debugger-frontend@0.81.4. 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 buffer is 96.0% likely obfuscated

Confidence: 0.96

Location: Package overview

From: pnpm-lock.yamlnpm/@openzeppelin/hardhat-upgrades@1.28.0npm/@openzeppelin/foundry-upgrades@0.4.0npm/buffer@4.9.2

ℹ 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/buffer@4.9.2. 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

After the audit branch was rebased onto current main as
indexing-payments-management-audit-fixed-rebased, the linear chain was
re-signed and the SHAs cited throughout the report no longer resolve.
Anchor the pre-rebase tip at tag indexing-payments-management-audit-pre-rebase
and provide the mapping plus a one-line equivalence check.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 99.06890% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.16%. Comparing base (52b5356) to head (675d397).

Files with missing lines Patch % Lines
...ntracts/payments/collectors/RecurringCollector.sol 99.20% 3 Missing ⚠️
...ges/contracts/contracts/rewards/RewardsManager.sol 96.42% 1 Missing ⚠️
...n/contracts/data-service/libraries/StakeClaims.sol 96.15% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1331      +/-   ##
==========================================
+ Coverage   88.62%   91.16%   +2.54%     
==========================================
  Files          75       81       +6     
  Lines        4615     5342     +727     
  Branches      823      975     +152     
==========================================
+ Hits         4090     4870     +780     
+ Misses        504      449      -55     
- Partials       21       23       +2     
Flag Coverage Δ
unittests 91.16% <99.06%> (+2.54%) ⬆️

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 marked this pull request as ready for review May 6, 2026 12:02
Sign-off commit bbec75e. v04 addressed recommendations only; no new
findings or status changes versus v03.
address graphTokenGateway,
address graphProxyAdmin,
address graphCuration
address graphProxyAdmin
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a breaking change for the event, I don't think it's being used by the network subgraph but maybe we'll want to update the abi eventually so it's not broken.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tmigone do you want to answer? (From commit 392047c I think.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely not used in the network subgraph or by anyone, i think it's safe

* @param allocationId The id of the allocation
* @param subgraphDeploymentId The id of the subgraph deployment
*/
event LegacyAllocationMigrated(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is no longer being used so we could delete. Maybe leave for a post-post-horizon cleanup 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIkely. I am not sure if it was ever used. @matiasedgeandnode? (From commit a7fb875.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was never used indeed. added with horizon to migrate legacy allo ids from staking to subgraph service but we never executed on it and have since decided to remove with clean up.
its likely it got re-added with matias' commit but we don't need it.

* - 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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security+contracts@ thegraph.com (remove space)

why can't we have the proper email without the space? Also, shouldn't this be:

@custom:security-contact

?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A work-around due to limitation of pragma version compiler; 0.76 does not support that natspec form (introduced in 0.8.2).

BTW not introduced by this branch, already on main?

* @notice Accept a Recurring Collection Agreement.
* @dev Caller must be the data service the RCA was issued to.
* If `signature` is non-empty: checks `rca.deadline >= block.timestamp` and verifies the ECDSA signature.
* If `signature` is empty: the payer must be a contract implementing {IAgreementOwner.approveAgreement}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't exist for IAgreementOwner

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. :) It was changed, and even then the comment was out of date. Did a sweep of a few related stale doc issues, committed them in #1334.

RembrandtK added a commit that referenced this pull request May 8, 2026
…eClaimsClaimNotFound revert

Closes the two real coverage gaps from PR #1331:
- RewardsManager.supportsInterface(IProviderEligibilityManagement.interfaceId)
  was the only OR-clause never asserted true.
- StakeClaims.processStakeClaim's claim-not-found require was never
  exercised; reachable only via library misuse, so test directly through
  a thin harness with empty mappings.
* States:
* - Null = indexer == address(0)
* - Active = not Null && tokens > 0
* - Closed = Active && closedAtEpoch != 0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this looks wrong. Maybe the intention was to replace not Null && tokens > 0 with Active referencing the previous definition for active but the result feels strange to me.

* @dev Possible states a legacy allocation can be.
* States:
* - Null = indexer == address(0)
* - Active = not Null && tokens > 0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation doesn't check that tokens > 0, we can change this to

not NULL && closedAtEpoch == 0

@RembrandtK RembrandtK merged commit 675d397 into main May 9, 2026
7 checks passed
@RembrandtK RembrandtK deleted the indexing-payments-management-audit-fixed-rebased branch May 9, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants