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}