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

v0 source pure

Package cliffvesting computes cliff-then-linear vesting schedules as a PURE calculator — no state, no balances, no tr...

Readme View source

gno.land/p/moul/x/daily/cliffvesting/v0

Cliff-then-linear vesting calculatorNew, NewLinear, Vested, Unvested, Claimable, PercentVested, CliffAmount, IsFullyVested, Duration, HasCliff.

1import "gno.land/p/moul/x/daily/cliffvesting/v0"
2
3s, _ := cliffvesting.New(1200, 0, 3, 12) // total, start, cliff, end
4s.Vested(2)          // 0   — before the cliff
5s.Vested(3)          // 300 — the cliff releases the elapsed portion at once
6s.Vested(12)         // 1200
7s.Claimable(6, 300)  // 300 — vested minus already claimed

Pure. No state, no balances, no transfers. The arithmetic is the part that is easy to get subtly wrong and easy to test; custody belongs to the realm holding the coins.

The cliff is a step, not a ramp: nothing vests before it, then the whole elapsed portion unlocks at once, and the rest accrues linearly.

All integer arithmetic — no float ever reaches consensus state:

  • total*elapsed/duration, multiplying first. The reverse computes a per-tick rate that truncates to zero whenever total < duration, so nothing would ever vest. That is the classic vesting bug; it has its own test (7 tokens over 1000 ticks).
  • Rounding is down, so a beneficiary is never paid more than they earned, and the final instalment collects the remainder: 1000 over 3 periods is 333 + 333 + 334, and Vested(end) is exactly total, never total-1.
  • Claimable never returns negative, even if the caller's bookkeeping says more was claimed than has vested.

Times are int64, so the caller may use block heights or unix seconds — the unit only has to be consistent.

Live demo: r/moul/x/daily/cliffvestingdemo · render it at /r/moul/x/daily/cliffvestingdemo/v0.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package cliffvesting computes cliff-then-linear vesting schedules as a PURE calculator — no state, no balances, no transfers.

The shape is the standard employee/token grant: nothing vests until the cliff, the cliff releases the whole elapsed portion at once, and the rest accrues linearly until the end of the term. Keeping it pure is deliberate: the arithmetic is the part that is easy to get subtly wrong and easy to test, while custody belongs to the realm holding the coins.

Everything is integer arithmetic. Vested is computed as total*elapsed/duration with the multiplication FIRST, so the usual rounding bug — dividing before multiplying and truncating the rate to zero — cannot happen. Rounding is always DOWN, which means the beneficiary never receives more than they have earned and the final instalment collects the remainder; at t >= end the result is exactly total, never total-1.

Times are int64 so the caller can use block heights or unix seconds. The unit only has to be consistent.

A live demo of this package is at r/moul/x/daily/cliffvestingdemo(/r/moul/x/daily/cliffvestingdemo/v0).

Variables 1

var ErrBadTotal, ErrBadDuration, ErrCliffAfter, ErrCliffBefore

1var (
2	ErrBadTotal    = errors.New("cliffvesting: total must be positive")
3	ErrBadDuration = errors.New("cliffvesting: duration must be positive")
4	ErrCliffAfter  = errors.New("cliffvesting: cliff must not fall after the end")
5	ErrCliffBefore = errors.New("cliffvesting: cliff must not fall before the start")
6)
source

Functions 2

func New

1func New(total, start, cliff, end int64) (Schedule, error)
source

New validates and returns a Schedule. cliff must lie within [start, end]. Passing cliff == start means "no cliff".

func NewLinear

1func NewLinear(total, start, end int64) (Schedule, error)
source

NewLinear is New with no cliff.

Types 1

type Schedule

struct
1type Schedule struct {
2	Total int64 // total amount to vest
3	Start int64 // vesting begins
4	Cliff int64 // nothing is claimable before this
5	End   int64 // fully vested at or after this
6}
source

Schedule is a cliff-then-linear vesting plan. Construct with New so the invariants are checked once.

Methods on Schedule

func Claimable

method on Schedule
1func (s Schedule) Claimable(t, claimed int64) int64
source

Claimable returns what can be withdrawn at time t given how much has already been claimed. Never negative, even if claimed somehow exceeds vested.

func CliffAmount

method on Schedule
1func (s Schedule) CliffAmount() int64
source

CliffAmount returns the lump sum released the instant the cliff is reached.

func Duration

method on Schedule
1func (s Schedule) Duration() int64
source

Duration returns the length of the vesting term.

func HasCliff

method on Schedule
1func (s Schedule) HasCliff() bool
source

HasCliff reports whether the schedule has a non-trivial cliff.

func IsFullyVested

method on Schedule
1func (s Schedule) IsFullyVested(t int64) bool
source

IsFullyVested reports whether the term has completed at time t.

func PercentVested

method on Schedule
1func (s Schedule) PercentVested(t int64) int64
source

PercentVested returns the vested share at t as an integer percentage, rounded down. Integer-only: no floats reach consensus state.

func Unvested

method on Schedule
1func (s Schedule) Unvested(t int64) int64
source

Unvested returns the remainder still locked at time t.

func Vested

method on Schedule
1func (s Schedule) Vested(t int64) int64
source

Vested returns how much has vested at time t. Zero before the cliff, exactly Total at or after End, and floor(total*elapsed/duration) in between.

Imports 1

  • errors stdlib

Source Files 3