vesting.gno
10.15 Kb · 315 lines
1// Realm vesting answers one question for a gno.land account: of the coins it
2// holds, how many can actually move right now.
3//
4// # Read this before you trust a number here
5//
6// A realm CANNOT read an account's vesting schedule. The VM's whole view of an
7// account is banker.GetCoins, which returns the TOTAL balance with the locked
8// part included, and no native exposes std.VestingSchedule. So this realm
9// reads two things from the chain and is told the third:
10//
11// balance read from the chain, banker.GetCoins always true
12// now read from the chain, block time always true
13// schedule supplied, see below only as good as its source
14//
15// Every rendered figure says which of the two it rests on. Nothing here is
16// presented as verified when it is not.
17//
18// # Where a schedule comes from
19//
20// Either the query string, for a one-off calculation that stores nothing, or
21// the registry. [Declare] writes a schedule for the CALLER'S OWN address and
22// no other, which is the whole trust model: an address can only misdescribe
23// itself, and a wrong entry misleads nobody but its author.
24//
25// Anyone can read their real schedule in one command and declare it:
26//
27// gnokey query auth/accounts/g1youraddress -remote https://rpc.gno.land:443
28//
29// # Why the schedule can be cached at all
30//
31// Because it can never change. std.SetVesting has exactly one caller in the
32// monorepo, gno.land/pkg/gnoland/app.go, during genesis balance loading, and
33// no message type creates or modifies a schedule. A schedule declared once is
34// correct forever, which is what makes a registry honest rather than stale.
35package vesting
36
37import (
38 "strconv"
39 "strings"
40 "time"
41
42 "chain"
43 "chain/banker"
44 "chain/runtime"
45 "chain/runtime/unsafe"
46
47 "gno.land/p/nt/avl/v0"
48 "gno.land/p/nt/ufmt/v0"
49
50 "gno.land/p/nym-moultest123/vesting/v0"
51)
52
53const (
54 // Path is this realm's package path.
55 Path = "gno.land/r/nym-moultest123/vesting/v0"
56 // Link is Path as a gnoweb route.
57 Link = "/r/nym-moultest123/vesting/v0"
58 // Denom is the only coin this realm reports on.
59 Denom = "ugnot"
60)
61
62// seeded are the schedules this realm ships with, restored by init() after
63// every redeploy.
64//
65// They are source constants and not registry rows on purpose: the realm is
66// private = true, so a redeploy wipes realm state, and the one schedule the
67// page exists to show should not need a transaction to come back. Each is
68// copied from the genesis allocation sheet that built mainnet, which is the
69// public, pinned input named in misc/deployments/mainnet.gno.land/gen-genesis.sh.
70//
71// An address is seeded here only when its LABEL is public too. The addresses
72// are all on chain, but naming one is a claim about who holds it, and this
73// realm is public and permanent. gen-genesis.sh names the investors-vesting
74// multisig outright, so that one ships; the treasuries and the other buckets
75// are identified nowhere public, so they do not, and the URL form serves
76// anyone who wants to check them anyway.
77var seeded = []struct {
78 addr address
79 label string
80 original int64
81 start int64
82 end int64
83 delayed bool
84}{
85 {
86 addr: "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
87 label: "moul",
88 original: 106560000000,
89 start: 1789225200,
90 end: 1852383600,
91 },
92 {
93 // "including the investors-vesting multisig
94 // g1x7tm26g9wj84cmg3cs74uwf3g9lqj4mjp6gax3, 150,000,000 GNOT total,
95 // 144,000,000 of it vesting continuously" -- gen-genesis.sh, and the
96 // sheet row it describes:
97 // g1x7tm26…=150000000000000ugnot;vesting=144000000000000ugnot,1789225200,1852383600
98 addr: "g1x7tm26g9wj84cmg3cs74uwf3g9lqj4mjp6gax3",
99 label: "investors-vesting multisig",
100 original: 144000000000000,
101 start: 1789225200,
102 end: 1852383600,
103 },
104}
105
106// declared maps an address to the schedule that address declared for itself.
107// Seeded schedules are NOT in here: they are read from the seeded slice, which
108// is what lets the index render them in full while paging everything else.
109// Anyone may Declare, so this tree is unbounded and the page that shows it
110// cannot be.
111var declared avl.Tree
112
113// seedIndex resolves a seeded address without walking the slice twice.
114var seedIndex avl.Tree
115
116// entry is one row of the registry.
117type entry struct {
118 schedule vesting.Schedule
119 label string // non-empty only for a seeded row
120 seeded bool
121 at int64 // block height the row was written at; 0 for a seeded row
122}
123
124func init() { reset() }
125
126// reset restores the registry to exactly what the source declares. init() is
127// its only caller on chain; a test calls it to render a page that does not
128// depend on what the test before it wrote, since an Example runs after every
129// Test in the package and sees the state they left.
130func reset() {
131 declared = avl.Tree{}
132 seedIndex = avl.Tree{}
133 for _, s := range seeded {
134 typ := vesting.Continuous
135 if s.delayed {
136 typ = vesting.Delayed
137 }
138 sch, err := vesting.New(s.original, s.start, s.end, typ)
139 if err != nil {
140 panic("vesting: seeded schedule for " + s.label + ": " + err.Error())
141 }
142 seedIndex.Set(s.addr.String(), &entry{schedule: sch, label: s.label, seeded: true})
143 }
144}
145
146// Declare records the caller's own vesting schedule, replacing any previous
147// one. It cannot write a row for anybody else, which is what keeps the
148// registry honest without the realm being able to verify a thing.
149//
150// Pass original = 0 to declare that the address has no schedule at all.
151func Declare(cur realm, original, start, end int64, delayed bool) {
152 typ := vesting.Continuous
153 if delayed {
154 typ = vesting.Delayed
155 }
156 sch, err := vesting.New(original, start, end, typ)
157 if err != nil {
158 panic(err.Error())
159 }
160 who := unsafe.PreviousRealm().Address()
161 if e, ok := lookup(who); ok && e.seeded {
162 panic("vesting: " + who.String() + " is seeded in the source; edit the realm instead")
163 }
164 declared.Set(who.String(), &entry{schedule: sch, at: chainHeight()})
165 chain.Emit("declared", "addr", who.String(),
166 "original", strconv.FormatInt(original, 10),
167 "start", strconv.FormatInt(start, 10),
168 "end", strconv.FormatInt(end, 10),
169 "type", typ.String())
170}
171
172// Forget removes the caller's own declaration.
173func Forget(cur realm) {
174 who := unsafe.PreviousRealm().Address()
175 e, ok := lookup(who)
176 if !ok {
177 panic("vesting: nothing declared for " + who.String())
178 }
179 if e.seeded {
180 panic("vesting: " + who.String() + " is seeded in the source; edit the realm instead")
181 }
182 declared.Remove(who.String())
183 chain.Emit("forgot", "addr", who.String())
184}
185
186// Count returns how many schedules addresses have declared for themselves.
187// Seeded schedules are not included; SeededCount reports those.
188func Count() int { return declared.Size() }
189
190// SeededCount returns how many schedules this realm ships in its source.
191func SeededCount() int { return seedIndex.Size() }
192
193// ScheduleOf returns the declared schedule for addr, and whether there is one.
194func ScheduleOf(addr address) (original, start, end int64, delayed, ok bool) {
195 e, found := lookup(addr)
196 if !found {
197 return 0, 0, 0, false, false
198 }
199 s := e.schedule
200 return s.Original, s.Start, s.End, s.Type == vesting.Delayed, true
201}
202
203// SpendableOf returns what addr can move right now, and whether the answer
204// rests on a declared schedule. With ok false the figure is the whole balance,
205// which is correct only if the address really has no schedule.
206func SpendableOf(addr address) (spendable int64, ok bool) {
207 bal := balanceOf(addr)
208 e, found := lookup(addr)
209 if !found {
210 return bal, false
211 }
212 return e.schedule.Spendable(bal, now()), true
213}
214
215// LockedOf returns what addr cannot move right now, and whether the answer
216// rests on a declared schedule.
217func LockedOf(addr address) (locked int64, ok bool) {
218 e, found := lookup(addr)
219 if !found {
220 return 0, false
221 }
222 bal := balanceOf(addr)
223 l := e.schedule.Locked(now())
224 if l > bal {
225 l = bal // cannot lock more than is there
226 }
227 return l, true
228}
229
230// lookup reads a row, seeded first so a seeded address can never be shadowed.
231// avl's Get returns ONE value and nil for a miss, unlike Remove which returns
232// two; that asymmetry is a documented gno-vs-Go trap.
233func lookup(addr address) (*entry, bool) {
234 if v := seedIndex.Get(addr.String()); v != nil {
235 return v.(*entry), true
236 }
237 v := declared.Get(addr.String())
238 if v == nil {
239 return nil, false
240 }
241 return v.(*entry), true
242}
243
244// balanceOf reads the address's TOTAL ugnot, locked included. That is what
245// banker.GetCoins reports and the only balance figure a realm can obtain.
246// A readonly banker takes no realm, so this works from inside Render.
247func balanceOf(addr address) int64 {
248 return banker.NewReadonlyBanker().GetCoins(addr).AmountOf(Denom)
249}
250
251// gnot renders a ugnot amount as GNOT with six decimals and no float.
252func gnot(u int64) string {
253 neg := u < 0
254 if neg {
255 u = -u
256 }
257 whole, frac := u/1_000_000, u%1_000_000
258 s := ufmt.Sprintf("%d.%s", whole, pad6(frac))
259 if neg {
260 s = "-" + s
261 }
262 return s + " GNOT"
263}
264
265// pad6 left-pads to six digits. ufmt has no width flags, so this is by hand.
266func pad6(n int64) string {
267 s := strconv.FormatInt(n, 10)
268 for len(s) < 6 {
269 s = "0" + s
270 }
271 return s
272}
273
274// permille renders tenths of a percent as a percentage with one decimal.
275func permille(p int64) string {
276 return ufmt.Sprintf("%d.%d%%", p/10, p%10)
277}
278
279// duration renders a span of seconds as days and hours, which is the only
280// resolution worth reading on a two-year schedule.
281func duration(sec int64) string {
282 if sec <= 0 {
283 return "complete"
284 }
285 d, h := sec/86400, (sec%86400)/3600
286 var b strings.Builder
287 if d > 0 {
288 b.WriteString(strconv.FormatInt(d, 10))
289 b.WriteString("d ")
290 }
291 b.WriteString(strconv.FormatInt(h, 10))
292 b.WriteString("h")
293 return b.String()
294}
295
296// isAddr is a cheap shape check, so a typo renders as "not an address" rather
297// than as an account with a zero balance.
298func isAddr(s string) bool {
299 if len(s) != 40 || !strings.HasPrefix(s, "g1") {
300 return false
301 }
302 for _, r := range s {
303 if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') {
304 return false
305 }
306 }
307 return true
308}
309
310// now is the chain's own clock, in unix seconds. time.Now() in a realm returns
311// the BLOCK time, not a wall clock, which is exactly the value the ante
312// handler compares a vesting schedule against.
313func now() int64 { return time.Now().Unix() }
314
315func chainHeight() int64 { return runtime.ChainHeight() }