ADR-004: Timestamp-Based Vesting Curve
Context
Section titled “Context”Vesting schedules come in three common shapes:
- Pure linear — tokens vest continuously from start to end
- Cliff-then-linear — no tokens until cliff, then linear from cliff to end
- Pure cliff — 100% of tokens unlock at a single moment
The initial architecture documentation proposed a VestingType enum with two variants (Cliff and Linear) to distinguish between curve types. This is the common approach in Solana vesting programs.
Decision
Section titled “Decision”Use three timestamps (start_time, cliff_time, end_time) to implicitly define the vesting curve, without an explicit VestingType enum.
All three real-world vesting curves are naturally expressed by three timestamps:
cliff_time == 0→ pure linear fromstart_timetoend_timecliff_time > start_time,end_time > cliff_time→ cliff-then-linearcliff_time > start_time,end_time == cliff_time→ pure cliff (100% at a single point)
A single formula handles all three cases without branching on a type tag:
if clock < cliff_time: vested = 0else: elapsed = clock - start_time duration = end_time - start_time vested = min(amount * elapsed / duration, amount)Rationale
Section titled “Rationale”This is smaller, simpler, and cannot desync — the curve is the timestamps. A stored enum adds a byte, requires validation that the enum matches the timestamps, and creates a desync risk if the curve type is set incorrectly at creation time. For the pure cliff case, setting end_time == cliff_time makes elapsed / duration == 1 exactly when clock >= cliff_time, vesting 100% without special-casing.
Alternatives Considered
Section titled “Alternatives Considered”| Approach | Pros | Cons |
|---|---|---|
VestingType enum | Explicit curve type in account data; easy frontend display | Desync risk (enum != timestamps); extra byte; branches in instruction logic |
| Timestamps only (chosen) | Single formula; no desync possible; supports all three curves | Curve type implicit; frontends must derive display label |
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- One formula for all curves: No branching on curve type in the program. The same
elapsed / durationmath works for linear, cliff-then-linear, and pure cliff. - Smaller account: Saves 1+ byte per stream (no enum discriminant).
- Impossible to misconfigure: You can’t accidentally set
VestingType::Linearwithcliff_time > start_timebecause there is no type field to desync.
Negative
Section titled “Negative”- Implicit curve type: The curve shape is not directly visible in account data. Frontends must derive the display label from timestamps (e.g., “Cliff + Linear” when
cliff_time > start_time). The SDK’sgetVestedPercent()handles this transparently. - Pure cliff edge case: Setting
end_time == cliff_timefor a pure cliff is not immediately intuitive. Documentation must explicitly call out this pattern.