Skip to content

stellar_send subscriptions have no expiry or max_executions cap, risking indefinite forgotten recurring charges #23

Description

@abayomicornelius

Problem (spike / open-ended)

execute_subscription (stellar_send/src/subscription.rs lines 125-167) advances next_execution_time by exactly one interval_seconds per call (line 150-153: sub.next_execution_time.checked_add(sub.interval_seconds)), regardless of how far now actually is past the previous next_execution_time. The module doc comment explicitly frames this as deliberate: "so a late keeper call doesn't silently drift the cadence forward." But the flip side is: if a keeper has been offline/unavailable for a long stretch (or a subscription simply wasn't executed for many intervals for any reason), the schedule is now arbitrarily "behind," and because execute_subscription has no restriction on how many times it can be called within a short real-world time span (only that now >= next_execution_time for that call), a keeper — or anyone, since execution is permissionless — can call it back-to-back-to-back in rapid succession (limited only by however many intervals have been missed) to "catch up," charging the payer many periods' worth of payments in a single burst.

Why this is worth investigating

This is a genuine design tension, not a simple bug: the "advance by one interval, not to now + interval" choice correctly prevents cadence drift (a real problem this design intentionally solves), but as a side effect it permits an unbounded catch-up burst if a subscription goes unexecuted for a while, which could be surprising to a payer who expected a steady drip and instead sees N payments land within the same block/short time window once a keeper resumes. Whether that's acceptable depends on product expectations (a "financial subscription" user likely wants steady cadence; a background token-purchase DCA-style user might not mind catch-up bursts as harmless).

Open questions

  • Should there be a max_catch_up_executions (or simply "at most one execution per call, but no other explicit cap on how many times per real-world hour the function can be called by different/same callers") — the current code already limits to one execution per call, but nothing stops a keeper from calling it 50 times in the same transaction batch or same block to catch up 50 missed intervals instantly.
  • Should catch-up be disallowed entirely (a missed interval is simply skipped, next_execution_time reset to now + interval_seconds on the next successful call) — trading "payer sometimes get skipped payments" for "payer never gets surprise multi-period bursts"?
  • Interacts with the max_executions/expiry open question in the related issue in this batch — a bounded subscription makes an unbounded catch-up burst less concerning (there's a hard ceiling on total possible damage) than an unbounded one.

Investigation steps

Model both semantics (strict catch-up vs. skip-ahead) against a few realistic keeper-downtime scenarios and evaluate which better matches the payer's reasonable expectations set at create_subscription time; consider whether the choice should even be a per-subscription flag (catch_up: bool) rather than a single global policy.

Testing strategy

Whichever direction is chosen: test_execute_subscription_rapid_catch_up_multiple_calls documenting exactly how many rapid calls succeed given a known backlog, and (if a cap or skip-ahead policy is adopted) a test confirming the new bound is enforced.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26contractsSmart contract logicenhancementNew feature or requestvery hardVery difficult / senior-level bounty issue

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