Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

cliffvesting.gno

4.00 Kb · 110 lines
  1// Package cliffvesting computes cliff-then-linear vesting schedules as a PURE
  2// calculator — no state, no balances, no transfers.
  3//
  4// The shape is the standard employee/token grant: nothing vests until the
  5// cliff, the cliff releases the whole elapsed portion at once, and the rest
  6// accrues linearly until the end of the term. Keeping it pure is deliberate:
  7// the arithmetic is the part that is easy to get subtly wrong and easy to test,
  8// while custody belongs to the realm holding the coins.
  9//
 10// Everything is integer arithmetic. Vested is computed as
 11// total*elapsed/duration with the multiplication FIRST, so the usual rounding
 12// bug — dividing before multiplying and truncating the rate to zero — cannot
 13// happen. Rounding is always DOWN, which means the beneficiary never receives
 14// more than they have earned and the final instalment collects the remainder;
 15// at t >= end the result is exactly total, never total-1.
 16//
 17// Times are int64 so the caller can use block heights or unix seconds. The unit
 18// only has to be consistent.
 19//
 20// A live demo of this package is at
 21// [r/moul/x/daily/cliffvestingdemo](/r/moul/x/daily/cliffvestingdemo/v0).
 22package cliffvesting
 23
 24import "errors"
 25
 26var (
 27	ErrBadTotal    = errors.New("cliffvesting: total must be positive")
 28	ErrBadDuration = errors.New("cliffvesting: duration must be positive")
 29	ErrCliffAfter  = errors.New("cliffvesting: cliff must not fall after the end")
 30	ErrCliffBefore = errors.New("cliffvesting: cliff must not fall before the start")
 31)
 32
 33// Schedule is a cliff-then-linear vesting plan. Construct with New so the
 34// invariants are checked once.
 35type Schedule struct {
 36	Total int64 // total amount to vest
 37	Start int64 // vesting begins
 38	Cliff int64 // nothing is claimable before this
 39	End   int64 // fully vested at or after this
 40}
 41
 42// New validates and returns a Schedule. cliff must lie within [start, end].
 43// Passing cliff == start means "no cliff".
 44func New(total, start, cliff, end int64) (Schedule, error) {
 45	s := Schedule{Total: total, Start: start, Cliff: cliff, End: end}
 46	if total <= 0 {
 47		return Schedule{}, ErrBadTotal
 48	}
 49	if end <= start {
 50		return Schedule{}, ErrBadDuration
 51	}
 52	if cliff > end {
 53		return Schedule{}, ErrCliffAfter
 54	}
 55	if cliff < start {
 56		return Schedule{}, ErrCliffBefore
 57	}
 58	return s, nil
 59}
 60
 61// NewLinear is New with no cliff.
 62func NewLinear(total, start, end int64) (Schedule, error) {
 63	return New(total, start, start, end)
 64}
 65
 66// Duration returns the length of the vesting term.
 67func (s Schedule) Duration() int64 { return s.End - s.Start }
 68
 69// HasCliff reports whether the schedule has a non-trivial cliff.
 70func (s Schedule) HasCliff() bool { return s.Cliff > s.Start }
 71
 72// Vested returns how much has vested at time t. Zero before the cliff, exactly
 73// Total at or after End, and floor(total*elapsed/duration) in between.
 74func (s Schedule) Vested(t int64) int64 {
 75	if t < s.Cliff || t < s.Start {
 76		return 0
 77	}
 78	if t >= s.End {
 79		return s.Total
 80	}
 81	elapsed := t - s.Start
 82	// Multiply BEFORE dividing: the reverse truncates the per-tick rate to
 83	// zero whenever total < duration, which is the classic vesting bug.
 84	return s.Total * elapsed / s.Duration()
 85}
 86
 87// Unvested returns the remainder still locked at time t.
 88func (s Schedule) Unvested(t int64) int64 { return s.Total - s.Vested(t) }
 89
 90// Claimable returns what can be withdrawn at time t given how much has already
 91// been claimed. Never negative, even if claimed somehow exceeds vested.
 92func (s Schedule) Claimable(t, claimed int64) int64 {
 93	v := s.Vested(t) - claimed
 94	if v < 0 {
 95		return 0
 96	}
 97	return v
 98}
 99
100// IsFullyVested reports whether the term has completed at time t.
101func (s Schedule) IsFullyVested(t int64) bool { return t >= s.End }
102
103// PercentVested returns the vested share at t as an integer percentage,
104// rounded down. Integer-only: no floats reach consensus state.
105func (s Schedule) PercentVested(t int64) int64 {
106	return s.Vested(t) * 100 / s.Total
107}
108
109// CliffAmount returns the lump sum released the instant the cliff is reached.
110func (s Schedule) CliffAmount() int64 { return s.Vested(s.Cliff) }