Skip to content

feat: ERC20RecurringPaymentProxy batch schedule v0.2.0 - #1763

Open
LeoSlrRf wants to merge 17 commits into
masterfrom
feat/req-429-recurring-batch
Open

feat: ERC20RecurringPaymentProxy batch schedule v0.2.0#1763
LeoSlrRf wants to merge 17 commits into
masterfrom
feat/req-429-recurring-batch

Conversation

@LeoSlrRf

@LeoSlrRf LeoSlrRf commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

ERC20RecurringPaymentProxy v0.2.0 — Batch schedule permit with multi-leg payments

What changed

Batch permit model replaces the single-recipient permit

SchedulePermit is replaced by SchedulePermitBatch, which carries two typed arrays of Leg structs (initialLegs and recurringLegs). Each Leg holds a recipient, amount, and an 8-byte payment reference. Index 1 settles initialLegs when present; all subsequent indices settle recurringLegs. Up to 8 legs per array (MAX_LEGS = 8) are allowed. Payment references must be unique within each array.

Explicit due-time array instead of firstPayment + periodSeconds

The permit now carries a uint32[] dueTimes array (one entry per cycle) that must be strictly increasing. This replaces the previous firstPayment/periodSeconds arithmetic and allows irregular schedules.

scheduleId anchors replay state independently of nonce/deadline

A bytes32 scheduleId field is committed into both the schedule key and the EIP-712 digest. nonce and deadline are in the digest but excluded from the key, so re-signing with a new nonce or deadline does not change the schedule key and bitmap/cancellation state persist across re-signs. A zero scheduleId is rejected. Changing any other signed term produces a new key with a virgin bitmap and cancelled flag.

Ownable removed; all admin actions gated by DEFAULT_ADMIN_ROLE

pause, unpause, setFeeProxy, grantRelayer, and revokeRelayer now require DEFAULT_ADMIN_ROLE instead of Ownable. grantRelayer/revokeRelayer replace the single-slot setRelayer. Multiple relayers may hold RELAYER_ROLE simultaneously and compete for the relayerFee.

Subscriber self-trigger via admitCycles / revokeCycles

Relayers can grant (admitCycles) or clear (revokeCycles) a bitmask of cycle indices on a schedule key. A subscriber may call triggerRecurringPaymentBatch directly only for admitted cycles. Relayers bypass the admission check entirely.

Hardened token-pull path

_pullExact verifies the contract's balance increased by exactly the requested amount, rejecting fee-on-transfer tokens and silent-fail transferFrom implementations. _approveFeeProxy and _payRelayer also check return values. A post-settlement balance assertion (UnexpectedBalance) ensures nothing leaks.

cancelScheduleBatch lets subscribers halt a schedule

Subscribers can mark a schedule key as cancelled; subsequent trigger attempts revert with Cancelled.

rescueTokens for stuck balances

Admins can recover any ERC-20 token accidentally left in the contract.

EIP-1271 smart-account signatures

ECDSA.recover is replaced by SignatureChecker.isValidSignatureNow, enabling contract-wallet subscribers.

FeeProxyUpdated event on setFeeProxy

The old and new proxy addresses are emitted when the fee proxy is updated.

New test contracts

  • ERC20SilentFail — returns false on a failed transferFrom instead of reverting.
  • ERC20FeeOnTransfer — under-delivers by 1 token on every transferFrom.
  • ERC20FailTransfertransfer() always returns false.
  • ERC20BlockRecipient — reverts transfers to a configurable blocked address.
  • MockERC1271 — minimal ERC-1271 wallet for smart-account signature tests.

ABI artifact

src/lib/artifacts/ERC20RecurringPaymentProxy/0.2.0.json is added with the full v0.2.0 ABI.

LeoSlrRf commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR introduces ERC20RecurringPaymentProxy v0.2.0 with signed batch schedules, explicit due times, multi-leg settlement, persistent schedule state, subscriber admission and cancellation, hardened token handling, and ERC-1271 signatures.

  • Replaces single-recipient permits with initial and recurring payment-leg arrays.
  • Adds role-based relayer and administrative management.
  • Adds exact token-pull checks, cancellation, token rescue, and schedule admission controls.
  • Registers the 0.2.0 ABI for explicit version selection while retaining deployed 0.1.0 as the default.
  • Adds contract, artifact, signature-wallet, and adverse-token behavior tests.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol Reworks recurring payments around signed multi-leg schedules, persistent schedule state, role-based authorization, exact token accounting, cancellation, and ERC-1271 verification.
packages/smart-contracts/src/lib/artifacts/ERC20RecurringPaymentProxy/0.2.0.json Adds the complete ABI artifact for the new batch-schedule contract interface.
packages/smart-contracts/src/lib/artifacts/ERC20RecurringPaymentProxy/index.ts Registers the 0.2.0 ABI for explicit lookup and deliberately keeps deployed 0.1.0 as the default.
packages/smart-contracts/test/contracts/ERC20RecurringPaymentProxy.test.ts Adds broad coverage for permit hashing, scheduling, authorization, settlement, replay state, cancellation, and adverse ERC-20 behavior.
packages/smart-contracts/test/lib/artifact.test.ts Verifies explicit 0.2.0 ABI lookup, the retained 0.1.0 default, and absence of 0.2.0 deployment metadata.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Signed batch permit] --> B[Validate signature and schedule]
  B --> C{Caller authorized?}
  C -->|Relayer role| D[Check due, cancellation, order, replay]
  C -->|Admitted subscriber| D
  D --> E[Pull exact payer total]
  E --> F[Settle initial or recurring legs]
  F --> G[Pay relayer fee]
  G --> H[Assert baseline balance restored]
  H --> I[Emit PaymentTriggered]
Loading

Reviews (5): Last reviewed commit: "fix(recurring): unique refs, hash-once, ..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown

❌ Echidna Fuzzing Results

Mode: ( test sequences)
Status: Property Violations Found

Property Test Results

Status Count
✅ Passed 0
❌ Failed 0
Total 0
Pass Rate 0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@github-actions

Copy link
Copy Markdown

⚠️ Slither Security Analysis

Status: Issues Found

Findings Summary

Severity Count Status
✅ High 0 Pass
✅ Medium 0 Pass
🔵 Low 0 Info
ℹ️ Informational 0 Info

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

@LeoSlrRf LeoSlrRf changed the title fix(recurring): emit 8-byte payment references feat: ERC20RecurringPaymentProxy batch schedule v0.2.0 Aug 24, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Slither Security Analysis

Status: Issues Found

Findings Summary

Severity Count Status
✅ High 0 Pass
✅ Medium 0 Pass
🔵 Low 0 Info
ℹ️ Informational 0 Info

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

@github-actions

Copy link
Copy Markdown

❌ Echidna Fuzzing Results

Mode: ( test sequences)
Status: Property Violations Found

Property Test Results

Status Count
✅ Passed 0
❌ Failed 0
Total 0
Pass Rate 0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@LeoSlrRf
LeoSlrRf marked this pull request as ready for review August 25, 2026 08:58
@LeoSlrRf
LeoSlrRf requested review from a team, aimen74 and rodrigopavezi and removed request for a team August 25, 2026 08:59
@github-actions

Copy link
Copy Markdown

❌ Echidna Fuzzing Results

Mode: ( test sequences)
Status: Property Violations Found

Property Test Results

Status Count
✅ Passed 0
❌ Failed 0
Total 0
Pass Rate 0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@github-actions

Copy link
Copy Markdown

⚠️ Slither Security Analysis

Status: Issues Found

Findings Summary

Severity Count Status
✅ High 0 Pass
✅ Medium 0 Pass
🔵 Low 0 Info
ℹ️ Informational 0 Info

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

@github-actions

Copy link
Copy Markdown

❌ Echidna Fuzzing Results

Mode: ( test sequences)
Status: Property Violations Found

Property Test Results

Status Count
✅ Passed 0
❌ Failed 0
Total 0
Pass Rate 0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@github-actions

Copy link
Copy Markdown

⚠️ Slither Security Analysis

Status: Issues Found

Findings Summary

Severity Count Status
✅ High 0 Pass
✅ Medium 0 Pass
🔵 Low 0 Info
ℹ️ Informational 0 Info

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

@rodrigopavezi rodrigopavezi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job!!

Let's only deploy to mainnet chains we support after we test everything same chain direct recurring payment on testnet. we never know how it will behave with the rest of the stack and the UX.

Cheers

@rodrigopavezi rodrigopavezi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strong PR. _pullExact is the right shape — a balanceOf delta rather than just a require, so it catches silent-fail and fee-on-transfer under-delivery, which a return-value check alone would miss. The post-settlement UnexpectedBalance assertion is a good addition, and the test "cannot settle an unfunded subscriber from a residual proxy balance" closes a vector that is reachable once the subscriber can self-trigger. Digest parity against ethers _TypedDataEncoder covers the nested-array hashing, which was the most likely thing to go quietly wrong.

Four concrete defects inline. Two other things worth raising here rather than on a line:

1. The single-fee entry point is gone — is that intended?

triggerRecurringPayment and the SchedulePermit struct are removed, so 0.2.0 is a new deployment alongside 0.1.0's seven live ones. The shaped decision was explicitly additive: keep the single entry point beside the batch one, because existing schedules are bound to the old address by their signed permits and cannot be migrated. Dropping it means 0.1.0 monthly schedules keep draining on the old address for up to 21 years while detection, the reconciler and the dashboard handle two addresses and two ABIs.

That may be the right call if few enough 0.1.0 schedules are live — but it is a decision worth making out loud rather than absorbing.

2. paymentReference uniqueness makes a subgraph fix blocking.

Nothing enforces distinct references across legs, so two legs sharing one emit two identical-reference TransferWithReferenceAndFee events in a single transaction. The subgraph builds entity ids without a log index, so those collide. Either enforce uniqueness in _assertLegs, or land the subgraph log-index fix before this deploys — it moves from latent to blocking with this change.

Nit on the description: it says scheduleId is "excluded from the EIP-712 digest", but it is in the digest (see inline). Worth correcting, since auditors read the description.

Worth writing down somewhere: the schedule key changes when any signed term changes, which the tests confirm is intentional. The consequence is that an amended schedule gets a virgin bitmap and a virgin cancelled flag — so cancelling schedule A does not cancel an amended variant B, and re-signing with a changed amount resets paid cycles. Fine if the engine always treats amend as cancel-plus-new-scheduleId, but that assumption should be explicit rather than inferred.

Comment thread packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol Outdated
Comment thread packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol Outdated
Comment thread packages/smart-contracts/src/contracts/ERC20RecurringPaymentProxy.sol Outdated
@github-actions

Copy link
Copy Markdown

❌ Echidna Fuzzing Results

Mode: ( test sequences)
Status: Property Violations Found

Property Test Results

Status Count
✅ Passed 0
❌ Failed 0
Total 0
Pass Rate 0%

📄 Full report and corpus available in workflow artifacts.

ℹ️ About Echidna Fuzzing

Echidna is a property-based fuzzer that generates random sequences of transactions
to test invariants (properties that should always hold true).

Properties tested:

  • Fee calculation bounds
  • Access control enforcement
  • Amount constraints
  • No duplicate payments
  • Zero address validation
  • Integer overflow protection

@LeoSlrRf

Copy link
Copy Markdown
Contributor Author

Thanks for the review — replies are on the inline threads as well.

R1 (testnet before mainnet): Agreed. We will only deploy to supported mainnet chains after same-chain direct recurring payment is tested on testnet.

R2a (single-fee entry point removed): Not a code change. Discussed and agreed — 0.2.0 is a new deployment. Existing 0.1.0 schedules stay on the old address and remain callable via the old ABI.

R2b (unique paymentReference): Fixed — see the inline reply on _assertLegs.

R2c (scheduleId in the digest): Fixed — PR description corrected; see the inline reply.

R2d (amend = new key / virgin cancelled): Fixed. Documented on the schedules mapping: changing any signed term except nonce/deadline yields a new key with a virgin bitmap and cancelled flag.

@github-actions

Copy link
Copy Markdown

⚠️ Slither Security Analysis

Status: Issues Found

Findings Summary

Severity Count Status
✅ High 0 Pass
✅ Medium 0 Pass
🔵 Low 0 Info
ℹ️ Informational 0 Info

📄 Full report available in workflow artifacts.
🔍 View detailed findings in the Security tab.

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.

2 participants