graphprotocol / graphprotocol/dipper
Add defence-in-depth confirmation that protocol-managed cancellations took effect
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 0
- Forks
- 1
- Avg merge
- 37m
- Merged PRs (30d)
- 3
Description
Context
In protocol-managed mode, dipper cancels an indexing agreement by calling the manager contract's cancelAgreement, passing a 32-byte terms hash that identifies which version to cancel; the contract acts only if that hash matches the one it stored at acceptance. This mode currently ships off by default behind a switch; a planned follow-up removes the self-payer path and makes it the only way agreements are funded.
The gap
Verified against the contract source: when the supplied hash is wrong or stale, the contract does not reject the call — it silently does nothing and the transaction still succeeds, emitting no event that distinguishes a no-op from a real cancel. So a cancellation that quietly did nothing would be recorded by dipper as successful: it marks the agreement cancelled while it stays live on-chain and keeps paying.
Why this is defence-in-depth, not a primary fix
The primary protections are already in place: dipper fetches the EIP-712 domain from the contract at startup (so it can't drift), the agreement struct is pinned by a drift test, and a misconfigured collector address would fail loudly at offer time before any agreement exists to cancel. A silent no-op therefore only triggers in narrow residual cases — e.g. the contract changing how it derives the hash, or the terms changing between when dipper stored them and when it cancels.
When it matters
Dormant while the mode stays off behind its switch. Once the contract path becomes the only one and is deployed against real funds, there is no fallback and this confirmation is the only detection for a silently-failed cancel. Recommended to land before that deployment — not a blocker for the code that introduces the path.
Proposed work
All three build on one missing capability — reading agreement state back from the collector.
- Confirm the cancel took effect. After sending a protocol-managed cancel, wait for the receipt, then read
RecurringCollector.getAgreement(agreementId).stateand requireCanceledByPayerbefore marking the agreement cancelled. If not, leave it live for the next sweep to retry. - Surface the failure. When the read shows the cancel didn't take, log a warning/error with the agreement id so operators see a stuck cancellation instead of silence.
- Recover a lost terms hash. When an agreement is missing its stored hash (e.g. pre-migration rows), read the authoritative value from
getAgreementDetails(agreementId).versionHashand backfill it.
Minor cleanups (deferred)
- The comment on the terms-hash computation says the value is "the hash dipper signs over," but in protocol-managed mode nothing is signed — the hash is only a cancel identifier. Reword, and note a mismatch fails silently.
- Some test fakes return an "already cancelled" result the real contract never produces; give them a real state field so they model the contract's actual no-op behaviour.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the protocol-managed cancellation path that calls the manager contract's cancelAgreement. Read the receipt, then query RecurringCollector.getAgreement(agreementId).state and getAgreementDetails(agreementId).versionHash to cover cancellation confirmation and missing-hash recovery. Done means failed cancellations remain live with an agreement-id warning, successful ones reach CanceledByPayer, and missing hashes can be backfilled.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- blockchain, payments
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100