Skip to content

fix(audit): block recurring rollover into post-expiry periods (MULT-7)#33

Merged
dev-jodee merged 8 commits into
audit/ai-scanner/03-lock-plan-v1from
audit/ai-scanner/04-recurring-rollover-guard
Apr 28, 2026
Merged

fix(audit): block recurring rollover into post-expiry periods (MULT-7)#33
dev-jodee merged 8 commits into
audit/ai-scanner/03-lock-plan-v1from
audit/ai-scanner/04-recurring-rollover-guard

Conversation

@dev-jodee
Copy link
Copy Markdown
Collaborator

Audit finding: MULT-7

validate_recurring_transfer allowed rollover into a post-expiry period when current_ts was within the drift window, granting a fresh full amount_per_period allowance in a terminal period that should not exist. With short periods, the drift window enabled repeated post-expiry pulls.

Fix

After computing the candidate next period start, only roll over if it stays strictly before expiry_ts. Pulls inside the drift window remain valid against whatever allowance is left in the final authorized period; no fresh allowance is granted past expiry.

Affects

validate_recurring_transfer is the shared helper called by both:

  • recurring delegation transfers
  • subscription transfers

Both paths inherit the fix.

Test plan

  • cargo build -p subscriptions
  • cargo test -p subscriptions --lib (211/211 pass; one new regression test)
  • New test_recurring_rollover_blocked_at_expiry_boundary reproduces the scanner PoC and asserts the second post-expiry pull now fails with AmountExceedsPeriodLimit
  • Existing test_recurring_transfer_within_drift_window and test_recurring_transfer_past_drift_window still pass

Stack

Stacked on top of #32 (MULT-16).

Skip rollover when candidate_start >= expiry_ts. Pulls within drift
window remain valid in the final authorized period; no fresh allowance
is granted in terminal periods.

Affects validate_recurring_transfer (used by recurring delegations and
subscription transfers).
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Compute Unit Report

Instruction Samples Min CUs Max CUs Avg CUs Est Cost (Low) [SOL] Est Cost (Med) [SOL] Est Cost (High) [SOL]
cancel_subscription 11 1774 2098 1982 0.000005000 0.000005079 0.000005991
close_subscription_authority 7 1866 1901 1871 0.000005000 0.000005074 0.000005935
create_fixed_delegation 36 3561 11066 4889 0.000005001 0.000005195 0.000007444
create_plan 84 3525 11038 5099 0.000005001 0.000005203 0.000007549
create_recurring_delegation 25 3592 8121 4974 0.000005001 0.000005198 0.000007487
delete_plan 8 401 401 401 0.000005000 0.000005016 0.000005200
init_subscription_authority 136 5805 16753 9310 0.000005002 0.000005372 0.000009655
revoke_delegation 19 303 570 405 0.000005000 0.000005016 0.000005202
subscribe 21 6639 18663 9292 0.000005002 0.000005371 0.000009646
transfer_fixed 6 8478 8481 8480 0.000005002 0.000005339 0.000009240
transfer_recurring 17 8566 8651 8594 0.000005002 0.000005343 0.000009297
transfer_subscription 10 8862 8985 8901 0.000005002 0.000005356 0.000009450
update_plan 21 409 488 461 0.000005000 0.000005018 0.000005230

Generated: 2026-04-28

@dev-jodee dev-jodee marked this pull request as ready for review April 28, 2026 16:24
Extract is_effectively_expired helper. Sponsor revocation now waits the
same TIME_DRIFT_ALLOWED_SECS past expiry that transfers tolerate, so
sponsor cannot close a delegation while the delegatee can still pull.
Extend SubscribeData with expected_mint/amount/period_hours/created_at.
Program rejects with PlanTermsMismatch if the live plan disagrees with
what the subscriber signed. Stale-signed subscribe transactions can no
longer enroll into a recreated plan with different terms.

SDK and webapp callers fetch plan data and pass the snapshot.
Plan::check_destination and Plan::can_pull now filter out zero-padded
slots before membership tests. A plan with fewer than four configured
destinations no longer authorizes a zero-owned receiver, and a plan
with fewer than four pullers no longer authorizes a zero-pubkey caller.
…-10)

Webapp exit flows now pass the on-chain payer as receiver when it
differs from the connected signer, so sponsor-funded delegations and
SubscriptionAuthority accounts can actually be closed.

Also migrates revokeSubscription and cancelAndRevokeSubscription from
buildRevokeDelegation to buildRevokeSubscription with planPda + receiver,
fixing subscription revoke for both sponsor and non-sponsor cases.
…(MULT-9)

Stale-delegation cleanup no longer appends a close on the current
SubscriptionAuthority. Revoking stale delegations is now scoped to the
supplied delegation accounts; the SA stays open and current grants
remain valid.
@dev-jodee dev-jodee merged commit fa2a3da into audit/ai-scanner/03-lock-plan-v1 Apr 28, 2026
4 checks passed
@dev-jodee dev-jodee deleted the audit/ai-scanner/04-recurring-rollover-guard branch April 28, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant