Indexing payments management audit fixed rebased#1331
Conversation
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>
…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.
Indexing payments management audit fixed rebased
🚨 Report Summary
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.
|
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.
|
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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Sign-off commit bbec75e. v04 addressed recommendations only; no new findings or status changes versus v03.
| address graphTokenGateway, | ||
| address graphProxyAdmin, | ||
| address graphCuration | ||
| address graphProxyAdmin |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
I believe this is no longer being used so we could delete. Maybe leave for a post-post-horizon cleanup 😅
There was a problem hiding this comment.
LIkely. I am not sure if it was ever used. @matiasedgeandnode? (From commit a7fb875.)
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
This function doesn't exist for IAgreementOwner
There was a problem hiding this comment.
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.
…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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Implementation doesn't check that tokens > 0, we can change this to
not NULL && closedAtEpoch == 0
Rebase of #1301 (
indexing-payments-management-audit) and #1325 (indexing-payments-management-audit-fix-2-light) onto currentmain.For subsequent fixes see: #1334
Commits being compared
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:
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):
Equivalence checks