Skip to content

Payment streaming: time-vested splits that unlock funds continuously #346

Description

@grantfox-oss

Difficulty: Expert. Scope: multi-week epic. Contract + SDK + dashboard.

Problem

Every payment path today is instantaneous: pay transfers now, deposit then distribute transfers now. There is no notion of time. Payroll, vesting, grants, and subscriptions all need "release X per unit time to these recipients until the funding runs out," and Tributary cannot express it.

Proposal

Add a stream primitive layered on the split routing table: a funder locks an amount against a split with a start time, end time (or rate), and the contract makes vested = f(now) withdrawable. Anyone can trigger withdraw_vested(stream_id, token), which pays out the newly-vested portion through the split's existing recipient/share routing (so streams compose with nested splits for free).

Why this is hard

  • Introduces the first time-dependent state machine in the contract, with env.ledger().timestamp() as the clock. Vesting math must be monotonic and never over-release, even across many partial withdrawals.
  • Precision: linear vesting with i128 and 7-decimal assets needs careful rounding so the sum of withdrawals never exceeds the locked amount and the final withdrawal cleans up dust (mirror the existing dust-to-last rule).
  • Cancellation and top-ups: define what happens to unvested funds on cancel (refund to funder), who may cancel (funder? controller?), and how top-ups extend a live stream.
  • Storage TTL: a long-running stream must not archive mid-flight (coordinate with Extend TTL on escrow balance entries so parked deposits do not archive #250/Extend TTL on the per-creator split index so splits_of() does not archive #251 TTL work).
  • Reentrancy/ordering around the token transfer and the vested-bookkeeping update.

Deliverables

  • Design RFC (vesting curve, cancel/top-up semantics, authorization model).
  • Contract: create_stream, withdraw_vested, cancel_stream, top_up, vested_of (view), plus events.
  • SDK helpers and a dashboard stream view showing live vested/withdrawn/remaining.
  • Tests including fake-time progression and property tests (never over-release; conservation).

Acceptance criteria

  1. Maintainer-approved RFC before implementation.
  2. withdrawn_total <= locked_total holds under any interleaving of withdrawals, proven by property test.
  3. Vested amount is exact at boundaries (0 at start, full at end) and dust is handled at the final withdrawal.
  4. Cancellation refunds exactly the unvested remainder; authorization is enforced and tested.
  5. Streams route through nested Recipient::Split correctly.
  6. TTL is extended so a live stream cannot archive; documented.

Related

Distinct from recurring pull payments (separate hard issue): streaming is continuous release of pre-locked funds; recurring is scheduled pulls from an external balance.

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26enhancementNew feature or requesthelp wantedExtra attention is neededrustPull requests that update rust code

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions