fix(contracts,indexer): close remaining audit findings + subscription… - #100
Conversation
… retry backoff Buantum audit (2026-07-10) remediation. Eight of the twelve findings were already fixed in the tree; this closes the remaining four and the off-chain gaps the audit's scope (A/05) excluded. Contracts (StrimzSubscriptions is immutable — requires redeploy): - M-01: burn chargeAttemptId immediately before _settleCharge instead of on entry. Only Charged and TransferFailed consume an id now; NotDue, InsufficientFunds, RevokedApproval and MerchantInactive stay retryable on the same deterministic id. Previously eight failed sweeps (~2h) exhausted every id for a period and stranded billing for the rest of the grace window. - M-02: reject a past startAt in both enrolment paths, placed before permit() so a rejected enrolment does not burn the payer's token nonce. - I-01: cancelling an already-cancelled subscription reverts. - I-02: remove the dead JobStatus.Approved variant (pre-launch, no rows). - Declare activate() on IStrimzAgentRegistry — implemented but unreachable through the interface (L-04 gap). Off-chain alignment: - Regenerate the escrow ABI. JobResolved/JobReclaimed/JobRefunded were absent from the indexer's topic filter so those logs were never fetched, leaving disputed and timed-out jobs parked in a non-terminal status. JobDisputed had also gained an indexed `by` param, changing its topic0 — disputes were never indexed either. - AgentJobStatus gains funded/resolved/reclaimed. `accepted` stays: it is off-chain only and guards approveJob against double-funding. - Split SubscriptionChargeOutcome per contract outcome; six outcomes previously collapsed into "skipped", hiding retryable vs terminal. Retry backoff (scheduler + indexer): - Subscription gains retryCount and nextRetryAt; the sweeper honours nextRetryAt. Escalating schedule 15m/1h/3h/6h/12h/24h lives in the SQL that increments the counter so the two stay atomic. ~6 attempts per 48h grace window instead of ~192. Incidental fixes found in the path of the above: - A successful charge never reset Subscription.status, so a recovered payer stayed at_risk and was flipped to lapsed by the grace cron. - The monthly commerce spend cap counted timeout-reclaimed (fully refunded) jobs against the merchant. - at_risk fired on any skip outcome, downgrading healthy payers on a NotDue. - subscription.charge_failed webhooked merchants on non-failures. forge test: 126 passed (was 118, +8 regression tests). go build/test clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxLmrcAgiH9TVVcZjEZ46J
`AgentJobStatus` gained funded/resolved/reclaimed so the escrow's dispute-resolution and timeout-reclaim exits are representable. The job detail page keys a Record on that union, so `next build` failed on the three missing keys. resolved and reclaimed are terminal but not clean successes — an arbitrated split and an escrow return — so both read neutral rather than positive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxLmrcAgiH9TVVcZjEZ46J
|
Review: audit remediation looks solid I checked this against the full Buantum report. All twelve findings are closed in the branch. The four High findings are genuinely fixed, not patched over. H-01 and H-03 are done well. Each binds every fund-routing parameter into a dedicated EIP-712 intent. The intent is verified before the token call. So a relayer cannot redirect funds or rewrite subscription terms. The M-01 fix is the careful one. It is correct. The attempt id is now burned only when money is about to move. Early and skipped sweeps stay retryable. The anti-double-charge guarantee still holds. The off-chain alignment is clean. The enum change is safe. The indexer keys off named events, not numeric enum values. I ran the tests locally. 126 passed, zero failed. One thing to fix before merge. Look at Right now a live subscription's real payment failure gets tagged as test. It never reaches live merchant endpoints. That is the exact webhook this PR is trying to make reliable. Please add a live-mode assertion to the charge-failed webhook test too. Two smaller notes. Fine to leave as is. The spend cap excludes disputed jobs while escrow is still held. A merchant could briefly queue extra spend during a dispute. It re-counts on resolution. The retry backoff depends on the indexer projecting the skip before the next tick. The on-chain idempotency and charge lock keep that bounded. A rollout reminder, not a code issue. The Payments and Subscriptions fixes live in immutable contracts. They need a fresh redeploy. Re-point every consumer after. If the escrow proxy is upgraded in place, the removed enum variant shifts the later indices. That would misread any in-flight job status. Prefer a clean deploy. Or drain in-flight jobs first. Nice work overall. |
… real mode The charge-failed outbox emit hardcoded mode to test. The webhook dispatcher filters merchantWebhookEndpoint on ev.mode, so a live subscription's payment failure matched zero live endpoints and was dropped with no error, no retry, and no dead letter — the merchant never learned the charge failed. Pass the already read from the Subscription lookup. Also close two gaps in the e2e coverage that let this through: - The lifecycle test skipped with IsPaymentFailure unset, so the webhook branch was never exercised. It now sets the flag and asserts the emitted event carries mode=live. - Add TestE2E_SubscriptionChargeSkip_NonPaymentFailureEmitsNoWebhook for the other half of the M-01 gate: a not_due skip writes the audit row but emits no webhook and leaves the payer out of at_risk.
… retry backoff
Buantum audit (2026-07-10) remediation. Eight of the twelve findings were already fixed in the tree; this closes the remaining four and the off-chain gaps the audit's scope (A/05) excluded.
Contracts (StrimzSubscriptions is immutable — requires redeploy):
Off-chain alignment:
byparam, changing its topic0 — disputes were never indexed either.acceptedstays: it is off-chain only and guards approveJob against double-funding.Retry backoff (scheduler + indexer):
Incidental fixes found in the path of the above:
forge test: 126 passed (was 118, +8 regression tests). go build/test clean.