ADR-003: Derived Stream Status
Context
Section titled “Context”Stream accounts need an observable state to control which operations are permitted at any point. The three logical states are:
- Active — vesting in progress, can withdraw and cancel
- Completed — fully vested and fully claimed, account closed
- 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.
Decision
Section titled “Decision”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.
Rationale
Section titled “Rationale”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 == amountbutstatus == Active— can’t cancel, locked foreverstatus == Completedbutamount_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.
Alternatives Considered
Section titled “Alternatives Considered”| Approach | Pros | Cons |
|---|---|---|
Stored VestingStatus enum | Single match readability; explicit in account data | Dual source of truth risk; every mutation must update two fields; requires additional byte |
| Derived from data (chosen) | Single source of truth; impossible to desync | Clients compute status instead of reading it; status not visible in account raw bytes |
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Impossible to desync: Status is always consistent with recorded amounts because it is the amounts.
- Smaller account: One
boolinstead of an enum + discriminant byte. - Simpler instruction logic: No status transition validation needed on write — it’s self-evident from the math.
Negative
Section titled “Negative”- Client-side computation: Status is not directly readable from raw account bytes. Frontends and indexers must compute it. The SDK provides
getStatus()andgetMilestoneStatus()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 amatchon an enum.