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.

vesting.gno

6.18 Kb · 176 lines
  1// Package vesting computes a gno.land account's vesting curve, as a PURE
  2// calculator: no state, no balances, no transfers.
  3//
  4// It is not a vesting scheme of its own. It is a faithful reimplementation of
  5// the one the CHAIN enforces, tm2's std.VestingSchedule, so that a realm can
  6// answer "how much of this balance can actually move right now" with the same
  7// arithmetic the ante handler uses. Divergence here is worse than useless, so
  8// every rule below is copied from tm2/pkg/std/vesting.go rather than designed:
  9//
 10//   - Times are unix seconds, and nothing compares them against the chain's
 11//     clock. A schedule already over when the chain starts is valid and vests
 12//     everything at once.
 13//   - [Continuous] vests linearly between Start and End. [Delayed] is a cliff:
 14//     nothing before End, everything at or after it, and Start is ignored.
 15//   - Rounding is DOWN, always, so the account never counts as spendable a
 16//     ugnot the chain would refuse to move.
 17//   - A zero Original is "no schedule", which locks nothing.
 18//
 19// # Why this is not p/moul/x/daily/cliffvesting
 20//
 21// That package is the employee-grant shape (start, cliff, end) and multiplies
 22// in plain int64, which silently wraps for the amounts a chain actually holds:
 23// over the 63,158,400 second mainnet term, any grant above 146,036 GNOT
 24// overflows, and a 318,720,000 GNOT grant reports a NEGATIVE vested amount.
 25// tm2 reaches for math/big at exactly this point. gno has no math/big, so
 26// [Schedule.Vested] goes through a 128-bit intermediate instead.
 27//
 28// # What this package cannot do
 29//
 30// Find out an address's schedule. Realm code cannot read one: the VM's whole
 31// view of an account is banker.GetCoins, which returns the TOTAL balance with
 32// the locked part included, and no native exposes std.VestingSchedule. The
 33// schedule has to come from the caller. See gno.land/r/nym-moultest123/vesting for what
 34// that means for a page that wants to show real numbers.
 35package vesting
 36
 37import (
 38	"errors"
 39	"math/bits"
 40)
 41
 42// Type selects the curve.
 43type Type uint8
 44
 45const (
 46	// Continuous vests linearly from Start to End. The tm2 default.
 47	Continuous Type = iota
 48	// Delayed is a cliff: nothing vests until End, then all of it.
 49	Delayed
 50)
 51
 52func (t Type) String() string {
 53	if t == Delayed {
 54		return "delayed"
 55	}
 56	return "continuous"
 57}
 58
 59var (
 60	ErrNegativeOriginal = errors.New("vesting: original amount cannot be negative")
 61	ErrEndNotPositive   = errors.New("vesting: end time must be positive")
 62	ErrNegativeStart    = errors.New("vesting: start time cannot be negative")
 63	ErrStartAfterEnd    = errors.New("vesting: start time must be before end time")
 64)
 65
 66// Schedule is one account's vesting plan, as the chain stores it.
 67type Schedule struct {
 68	Original int64 // the granted amount, in the smallest unit
 69	Start    int64 // unix seconds; ignored by Delayed
 70	End      int64 // unix seconds
 71	Type     Type
 72}
 73
 74// New validates a schedule, applying tm2's own rules in tm2's own order.
 75//
 76// The Start >= 0 check is not cosmetic and is the reason Vested can stay in
 77// int64 for its subtractions: with 0 <= Start < End, neither End-Start nor
 78// now-Start can overflow. tm2 rejects a negative start for exactly this.
 79func New(original, start, end int64, typ Type) (Schedule, error) {
 80	s := Schedule{Original: original, Start: start, End: end, Type: typ}
 81	if original < 0 {
 82		return Schedule{}, ErrNegativeOriginal
 83	}
 84	if s.IsZero() {
 85		return s, nil // no schedule; the other fields do not matter
 86	}
 87	if end <= 0 {
 88		return Schedule{}, ErrEndNotPositive
 89	}
 90	if typ != Delayed {
 91		if start < 0 {
 92			return Schedule{}, ErrNegativeStart
 93		}
 94		if start >= end {
 95			return Schedule{}, ErrStartAfterEnd
 96		}
 97	}
 98	return s, nil
 99}
100
101// IsZero reports whether there is no schedule at all, which locks nothing.
102func (s Schedule) IsZero() bool { return s.Original == 0 }
103
104// Vested returns how much of Original has vested at unix time now.
105func (s Schedule) Vested(now int64) int64 {
106	if s.IsZero() {
107		return 0
108	}
109	if now >= s.End {
110		return s.Original
111	}
112	if s.Type == Delayed {
113		return 0 // a cliff vests nothing until End
114	}
115	if now <= s.Start {
116		return 0
117	}
118	return mulDiv(s.Original, now-s.Start, s.End-s.Start)
119}
120
121// Locked returns the part of Original that has not vested at unix time now.
122// This is what the chain refuses to let leave the account.
123func (s Schedule) Locked(now int64) int64 { return s.Original - s.Vested(now) }
124
125// Spendable returns how much of balance can actually move at unix time now.
126//
127// balance is the account's TOTAL, which is what banker.GetCoins reports. The
128// locked part is capped at the balance: an account that has already spent down
129// to less than it still owes to the schedule has nothing spendable, not a
130// negative amount. tm2 reaches the same answer by subtracting locked coins
131// from the balance and refusing the transfer if the result does not cover it.
132func (s Schedule) Spendable(balance, now int64) int64 {
133	if balance <= 0 {
134		return 0
135	}
136	locked := s.Locked(now)
137	if locked >= balance {
138		return 0
139	}
140	return balance - locked
141}
142
143// PermilleVested returns the vested share at now in tenths of a percent,
144// rounded down, so a page can show one decimal without a float. Integer only:
145// no float ever reaches consensus state.
146func (s Schedule) PermilleVested(now int64) int64 {
147	if s.IsZero() {
148		return 1000 // nothing to vest is fully vested
149	}
150	return mulDiv(s.Vested(now), 1000, s.Original)
151}
152
153// RemainingSeconds returns how long until the schedule completes, zero once it
154// has. Reported rather than formatted: the caller owns how a duration reads.
155func (s Schedule) RemainingSeconds(now int64) int64 {
156	if s.IsZero() || now >= s.End {
157		return 0
158	}
159	return s.End - now
160}
161
162// mulDiv computes floor(a*b/den) through a 128-bit intermediate, so a product
163// that leaves int64 does not wrap.
164//
165// Every caller here passes 0 <= b <= den with den > 0, which is what makes the
166// bits.Div64 precondition hold: the quotient is then at most a, so it fits in
167// 64 bits, which is exactly the hi < den that Div64 requires and panics
168// without. Keep that invariant at the call site, not by checking it here.
169func mulDiv(a, b, den int64) int64 {
170	if a == 0 || b == 0 {
171		return 0
172	}
173	hi, lo := bits.Mul64(uint64(a), uint64(b))
174	q, _ := bits.Div64(hi, lo, uint64(den))
175	return int64(q)
176}