1// Package vesting computes a gno.land account's vesting curve, as a PURE2// calculator: no state, no balances, no transfers.3//4// It is not a vesting scheme of its own. It is a faithful reimplementation of5// the one the CHAIN enforces, tm2's std.VestingSchedule, so that a realm can6// answer "how much of this balance can actually move right now" with the same7// arithmetic the ante handler uses. Divergence here is worse than useless, so8// 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's11// clock. A schedule already over when the chain starts is valid and vests12// 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 a16// 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/cliffvesting20//21// That package is the employee-grant shape (start, cliff, end) and multiplies22// 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 GNOT24// 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, so26// [Schedule.Vested] goes through a 128-bit intermediate instead.27//28// # What this package cannot do29//30// Find out an address's schedule. Realm code cannot read one: the VM's whole31// view of an account is banker.GetCoins, which returns the TOTAL balance with32// the locked part included, and no native exposes std.VestingSchedule. The33// schedule has to come from the caller. See gno.land/r/moul/vesting for what34// that means for a page that wants to show real numbers.35package vesting3637import (38 "errors"39 "math/bits"40)4142// Type selects the curve.43type Type uint84445const (46 // Continuous vests linearly from Start to End. The tm2 default.47 Continuous Type = iota48 // Delayed is a cliff: nothing vests until End, then all of it.49 Delayed50)5152func (t Type) String() string {53 if t == Delayed {54 return "delayed"55 }56 return "continuous"57}5859var (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)6566// Schedule is one account's vesting plan, as the chain stores it.67type Schedule struct {68 Original int64 // the granted amount, in the smallest unit69 Start int64 // unix seconds; ignored by Delayed70 End int64 // unix seconds71 Type Type72}7374// 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 in77// int64 for its subtractions: with 0 <= Start < End, neither End-Start nor78// 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{}, ErrNegativeOriginal83 }84 if s.IsZero() {85 return s, nil // no schedule; the other fields do not matter86 }87 if end <= 0 {88 return Schedule{}, ErrEndNotPositive89 }90 if typ != Delayed {91 if start < 0 {92 return Schedule{}, ErrNegativeStart93 }94 if start >= end {95 return Schedule{}, ErrStartAfterEnd96 }97 }98 return s, nil99}100101// IsZero reports whether there is no schedule at all, which locks nothing.102func (s Schedule) IsZero() bool { return s.Original == 0 }103104// 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 0108 }109 if now >= s.End {110 return s.Original111 }112 if s.Type == Delayed {113 return 0 // a cliff vests nothing until End114 }115 if now <= s.Start {116 return 0117 }118 return mulDiv(s.Original, now-s.Start, s.End-s.Start)119}120121// 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) }124125// 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. The128// locked part is capped at the balance: an account that has already spent down129// to less than it still owes to the schedule has nothing spendable, not a130// negative amount. tm2 reaches the same answer by subtracting locked coins131// 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 0135 }136 locked := s.Locked(now)137 if locked >= balance {138 return 0139 }140 return balance - locked141}142143// 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 vested149 }150 return mulDiv(s.Vested(now), 1000, s.Original)151}152153// RemainingSeconds returns how long until the schedule completes, zero once it154// 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 0158 }159 return s.End - now160}161162// mulDiv computes floor(a*b/den) through a 128-bit intermediate, so a product163// that leaves int64 does not wrap.164//165// Every caller here passes 0 <= b <= den with den > 0, which is what makes the166// bits.Div64 precondition hold: the quotient is then at most a, so it fits in167// 64 bits, which is exactly the hi < den that Div64 requires and panics168// 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 0172 }173 hi, lo := bits.Mul64(uint64(a), uint64(b))174 q, _ := bits.Div64(hi, lo, uint64(den))175 return int64(q)176}177Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.