Skip to content

ADR-003: Derived Stream Status

Stream accounts need an observable state to control which operations are permitted at any point. The three logical states are:

  1. Active — vesting in progress, can withdraw and cancel
  2. Completed — fully vested and fully claimed, account closed
  3. Cancelled — creator terminated the stream, account closed

The initial architecture documentation proposed storing a VestingStatus enum (Active | Completed | Cancelled) as an explicit field. This pattern is common in Solana programs where state machines are encoded as enums.

Derive stream status from data fields instead of storing it as an explicit enum.

The StreamAccount stores only cancelled: bool. Completion is derived from amount_withdrawn == amount. Status is computed at read time rather than stored and maintained by instruction code.

A stored enum creates a dual source of truth problem. Every withdraw instruction would need to update both amount_withdrawn and potentially the status field. If either update is missed or fails after the other, the account enters an inconsistent state:

  • amount_withdrawn == amount but status == Active — can’t cancel, locked forever
  • status == Completed but amount_withdrawn < amount — tokens trapped in vault

By deriving status, consistency is guaranteed by the data itself. Completion is a mathematical fact (amount_withdrawn == amount), not a separate field to synchronize.

For milestone streams, the same principle applies: cancelled is stored, milestone_reached is a boolean gate, and completion is amount_withdrawn == amount. The state machine is encoded in the combination of these independent fields rather than a unified enum.

ApproachProsCons
Stored VestingStatus enumSingle match readability; explicit in account dataDual source of truth risk; every mutation must update two fields; requires additional byte
Derived from data (chosen)Single source of truth; impossible to desyncClients compute status instead of reading it; status not visible in account raw bytes
  • Impossible to desync: Status is always consistent with recorded amounts because it is the amounts.
  • Smaller account: One bool instead of an enum + discriminant byte.
  • Simpler instruction logic: No status transition validation needed on write — it’s self-evident from the math.
  • Client-side computation: Status is not directly readable from raw account bytes. Frontends and indexers must compute it. The SDK provides getStatus() and getMilestoneStatus() helpers for this.
  • No explicit state transitions: The state machine is implicit in the combination of cancelled, amount_withdrawn == amount, and for milestones, milestone_reached. This makes program code less visually self-documenting than a match on an enum.