Commit 8b2777b
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
File tree
- bindings
- abi
- src/bindings
- docs
- fixtures
- script
- src
- config
- core
- facets/tangle
- interfaces
- libraries
- test
- fuzz
- payments
- rewards
- security
- tangle
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
This file was deleted.
0 commit comments