Skip to content

fix(recurring): bind batch schedule key to signed terms - #1754

Closed
LeoSlrRf wants to merge 1 commit into
feat/req-429-pull-assertionsfrom
feat/req-429-schedule-key-replay
Closed

fix(recurring): bind batch schedule key to signed terms#1754
LeoSlrRf wants to merge 1 commit into
feat/req-429-pull-assertionsfrom
feat/req-429-schedule-key-replay

Conversation

@LeoSlrRf

@LeoSlrRf LeoSlrRf commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Decouple schedule key from EIP-712 digest to prevent replay via re-signing

The bitmap tracking which payment indices have been triggered was previously keyed on the EIP-712 digest, which includes nonce and deadline. This meant a subscriber could re-sign the same economic terms with a fresh nonce or deadline and reset the paid-index tracking, allowing already-settled payments to be triggered again.

The schedule key is now derived exclusively from the fields that define the economic terms of the schedule (subscriber, token, recipient, amounts, period, etc.), deliberately excluding nonce and deadline. This ensures that re-signing with updated metadata does not produce a new key and cannot unlock previously paid indices.

Changes

  • Introduces _scheduleKeyFromPermit and _scheduleKeyFromBatch (with public wrappers) to compute a stable, content-addressed key for each schedule.
  • triggeredPaymentsBitmap and lastPaymentIndex are now keyed on the schedule key rather than the EIP-712 digest.
  • Adds ERC20RecurringPaymentProxy__ZeroScheduleId error, reverted when a batch permit carries a zero scheduleId, which would make distinct schedules collide.
  • Extracts _assertUnpaid, _assertOrder, and _markPaid helpers to centralise bitmap and ordering logic.
  • Moves _markPaid to after the transfer calls so a failed transfer never marks an index as paid.
  • Adds an explicit rejection of index 0 before the existing upper-bound check.
  • Updates tests to use scheduleKeyFromPermit / scheduleKeyFromBatch and adds a dedicated Schedule key replay suite covering re-sign stability, zero-index rejection, key sensitivity to term changes, and zero scheduleId rejection.

@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes recurring-payment replay state independent of signature nonce and deadline, preventing re-signing from resetting paid indices.

  • Derives stable schedule keys from signed payment terms.
  • Uses the stable key for order checks, replay checks, and paid-state updates.
  • Rejects payment index zero and zero batch schedule identifiers.
  • Adds tests for failed-transfer state rollback and stable schedule-key behavior.

Confidence Score: 5/5

The PR appears safe to merge with no concrete changed-code failure identified.

The executed recurring-payment path consistently derives one nonce-independent key for order checking, replay checking, and state updates, while reverted transfers leave payment state unmarked.

Important Files Changed

Filename Overview
packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol Introduces stable term-derived schedule keys and consistently applies the single-permit key to replay and ordering state.
packages/smart-contracts/test/contracts/ERC20RecurringPaymentProxy.test.ts Updates state assertions to use schedule keys and adds coverage for re-signing, index zero, batch key stability, term changes, and zero schedule identifiers.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Signed schedule permit] --> B[Verify EIP-712 signature]
  A --> C[Derive stable schedule key]
  C --> D[Check order and paid bitmap]
  D --> E[Execute token transfers]
  E --> F[Mark index paid under schedule key]
  G[Re-sign with new nonce or deadline] --> C
Loading

Reviews (1): Last reviewed commit: "fix(recurring): bind batch schedule key ..." | Re-trigger Greptile

@LeoSlrRf LeoSlrRf closed this Aug 24, 2026
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