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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

cliffvesting.gno

4.01 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/nym-moultest123/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) }