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

vesting.gno

6.17 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/moul/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}