Skip to content

Add defence-in-depth confirmation that protocol-managed cancellations took effect #638

Description

@MoonBoi9001

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.

  1. Confirm the cancel took effect. After sending a protocol-managed cancel, wait for the receipt, then read RecurringCollector.getAgreement(agreementId).state and require CanceledByPayer before marking the agreement cancelled. If not, leave it live for the next sweep to retry.
  2. 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.
  3. 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).versionHash and 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions