Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/vesting-cliff-boundary-correctness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Vesting cliff boundary correctness

This document specifies how `RevoraVesting` interprets **cliff duration** and ledger timestamps so integrators and auditors can rely on deterministic, reviewable behavior (issue #171).

## Definitions

- `start_time`: schedule start (seconds).
- `cliff_duration_secs`: non-negative offset; **cliff end** is `cliff_time = start_time + cliff_duration_secs`.
- `duration_secs`: total schedule length from `start_time`; **vesting end** is `end_time = start_time + duration_secs`.
- Validation: `cliff_duration_secs <= duration_secs` (strict `>` is rejected).

## Boundary semantics

| Condition | Vested amount |
|-----------|----------------|
| `now < cliff_time` | `0` |
| `now == cliff_time` | `0` (first instant of the linear segment; elapsed = 0) |
| `cliff_time < now < end_time` | `floor(total_amount × (now − cliff_time) / (end_time − cliff_time))`, capped at `total_amount` |
| `now >= end_time` | `total_amount` |
Comment on lines +14 to +19
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown table uses || prefixes, which won’t render as a standard GitHub-flavored markdown table. Replace with single-pipe table rows (e.g., | Condition | Vested amount |) so the spec renders correctly.

Copilot uses AI. Check for mistakes.

So the **cliff holds zero vesting** for every second strictly before `cliff_time`. The **linear window** is `[cliff_time, end_time)` in continuous terms; at `end_time` the position is fully vested.

## Degenerate case: `cliff_duration_secs == duration_secs`

Then `cliff_time == end_time`. There is no open linear interval: nothing vests until `now >= end_time`, at which point **100%** vests in one step (pure cliff / big-bang unlock).

## Security and abuse notes

- **Timestamp source**: Uses Soroban ledger timestamp; callers must not assume wall-clock alignment across chains.
- **Cancelled schedules**: `vested_amount` is always `0` after cancel, regardless of time.
- **Rounding**: Integer division truncates toward zero; sum of holder allocations off-chain should be reconciled with this rounding mode where relevant.

## Tests

See `src/vesting_test.rs`:

- `claimable_at_exact_cliff_timestamp_is_zero`
- `claimable_one_second_after_cliff_matches_linear_slice`
- `claimable_last_second_before_end_one_step_below_full`
- `cliff_equals_duration_unlocks_full_amount_only_at_end`
Loading
Loading