Skip to content

Commit 8b2777b

Browse files
authored
feat(payments): subscription billing rearchitecture + bindings v0.16.0 (#132)
Closes ten prior audit findings on the subscription billing path with one cohesive redesign. Substantive contract behavior changes — downstream consumers (Rust bindings v0.16.0, indexer, dapp) must update. ## What changed **Bill amount** - TWAP-fair from activation: `nominalRate × cumDeltaPeriod / (baseline × interval)`, capped at `nominalRate`. Operators ramping stake mid-period cannot inflate a customer's bill; ramp-down still reduces it. - Baseline pinned at activation (not first bill). Removes the stake-ramp attack window that the previous lazy-init path opened. - Manager QoS hook `computeBillAdjustmentBps(serviceId, periodStart, periodEnd)` can discount the bill (clamped `[0, 10_000]`); called via gas-capped staticcall. `queryDeveloperPaymentAddress` is now gas-capped too. Malicious managers cannot grief keepers. - Per-operator TWAP cursors are mutated only AFTER the escrow-balance check passes — a failed try-bill no longer leaves cursors advanced (closes the free-period exploit on the batch path). - Dust bills (rounding to less than 1 wei per recipient) skip cleanly via `minBillAmount` rather than reverting deep in `_distributeBill`. - Overflow in `twapBillAmount` now reverts with `BillingArithmeticOverflow` instead of silently returning nominal. **Operator payout** - Same `cumDelta × exposureBps` weights drive bill amount AND payout share — customer-fairness and operator-fairness coupled. An operator who ramped stake earns a larger slice of the SAME (capped) pool. - Zero active operators: bill is skipped, cursor advances, escrow untouched, `SubscriptionBillSkippedNoOperators` emitted. No livelock when a Dynamic service loses all its operators. - Staker pool refund-to-escrow when the fee distributor is unset or reverts (was: silent capture by treasury). - `claimRewardsAll` semantics unchanged for the user; the keeper rebate uses the same pull-pattern mapping. **Payment split** - `PaymentSplit` gained a fifth field `keeperBps`. `setPaymentSplit` requires the five-field sum equal 10_000. `paymentSplit()` returns a 5-tuple. - Default split unchanged at 20/20/40/20/0 — admins opt in to a non-zero keeper rebate via `setPaymentSplit` per network policy. - Non-subscription distributions (PayOnce, RFQ, per-job) fold `keeperBps` into the operator pool so the total still sums to 10_000. **EventDriven funding** - Upfront `paymentAmount > 0` at request is rejected with `UpfrontPaymentNotAllowedForEventDriven`. Customer funds are never collected on a path that cannot pay them out properly. Quote-create flow has the matching guard. **Activation paths** - `_handleInitialPayments` (request flow) and `_activateQuoteService` (quote flow) both seed per-op TWAP cursors and pin `subscriptionBaselineStake` at activation. A bill against an unbaselined service reverts loudly with `SubscriptionBaselineNotInitialized` instead of silently re-seeding. **Per-service operator ceiling** - `ProtocolConfig.MAX_OPERATORS_PER_SERVICE = 64`. `maxOperators = 0` on a blueprint config is interpreted as "use the protocol ceiling" — never "unlimited". Bounds every per-operator loop in the bill / distribute / terminate paths. Hard fix for the storage-audit Critical that let a blueprint creator unbound the bill loop. **Events** - New: `SubscriptionBillSkippedNoOperators`, `SubscriptionBillAdjustedByManager`, `KeeperRebateAccrued`, `StakerShareRefundedToEscrow`, `SubscriptionBaselineInitialized`. - `PaymentSplitUpdated` extended with `keeperBps`. **Errors** - New: `BillingArithmeticOverflow`, `SubscriptionBaselineNotInitialized`, `UpfrontPaymentNotAllowedForEventDriven`. **Dead code removed** - `PaymentLib.calculateOperatorPayments`, `validatePaymentAmount`, `bpsShareRoundUp`, `divUp` — superseded by inline per-weight distribution and `minBillAmount`. Orphaned test file deleted. - `_seedTwapCursorForOp`, `_aggregateOperatorStakeAtNow` — unreachable helpers stripped. ## Audits Four independent reviewers ran against this branch: - Security: 0 Critical / 1 High (cursor-mutation persistence — fixed in this commit) / 1 Medium (quote-path EventDriven bypass — fixed) plus Lows - Architectural correctness: all 10 prior audit findings verified fixed - Hardening + gas + code quality: dead-code cleanup, gas captures, naming - Timing intervals + storage / O(1) audits: 3 timing Highs and 2 storage Criticals filed; storage C-1 (`maxOperators = 0` unbounded) fixed inline, remaining items captured as follow-up issues. ## Tests - New `test/security/SubscriptionBilling.t.sol` (renamed from F5TWAPBilling): 15 tests — 7 unit + 6 fuzz (× 1000 runs each), covering bill cap, zero-op skip, keeper rebate, QoS waiver, EventDriven rejection, try-bill cursor invariant, and overflow revert. - Updated all PaymentSplit literals + destructures across `test/` and `script/` for the 5-tuple. - Fixed two pre-existing test failures rooted in the same via-IR optimizer quirk on `vm.warp` + `block.timestamp`: `test_GovernorSelfUpgrade` / `test_TokenUpgradeViaGovernance` (Governance), and `testFuzz_DisputeWindow_Enforcement` (SlashingFuzz, 1000 runs). - Full regression: 1445 tests pass across 94 suites. ## Bindings + docs - `bindings v0.16.0`: regenerated, CHANGELOG written. - `docs/PRICING.md` rewritten end-to-end against v0.16.0 source. - `README.md` updated for the 5-tuple split + new payment-model summary. - `docs/DEPLOYMENT_RUNBOOK.md` + `docs/full-deploy.md` path corrections. - Stale AUDIT-*.md / pursue-launch-readiness.md / COMMIT_MSG.md removed. ## Follow-up (NOT in this PR) Captured for future PRs: - Storage C-2: O(1) operator-asset stake aggregate (perf, not correctness). - Storage H-1: ValidatorPodManager unbounded delegator array on slash. - Storage H-3: `claimRewardsAll` try/catch on per-token transfer. - Timing H-1: Job RFQ quote `requester` binding not verified. - Timing H-2: Service-creation quote freshness gap. - Timing H-3: `extendServiceFromQuotes` cumulative TTL cap. - Contract-size: TanglePaymentsFacet + TangleServicesFacet exceed EIP-170 (deploys on anvil require `--disable-code-size-limit`); needs facet split before mainnet.
1 parent 241b09d commit 8b2777b

57 files changed

Lines changed: 2929 additions & 2660 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AUDIT-PAYMENTS.md

Lines changed: 0 additions & 272 deletions
This file was deleted.

0 commit comments

Comments
 (0)