vesting.gno
8.52 Kb · 280 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/moul/vesting/v0"
51)
52
53const (
54 // Path is this realm's package path.
55 Path = "gno.land/r/moul/vesting/v0"
56 // Link is Path as a gnoweb route.
57 Link = "/r/moul/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.
70var seeded = []struct {
71 addr address
72 label string
73 original int64
74 start int64
75 end int64
76 delayed bool
77}{
78 {
79 addr: "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
80 label: "moul",
81 original: 106560000000,
82 start: 1789225200,
83 end: 1852383600,
84 },
85}
86
87// declared maps an address to the schedule that address declared for itself.
88var declared avl.Tree
89
90// entry is one row of the registry.
91type entry struct {
92 schedule vesting.Schedule
93 label string // non-empty only for a seeded row
94 seeded bool
95 at int64 // block height the row was written at; 0 for a seeded row
96}
97
98func init() { reset() }
99
100// reset restores the registry to exactly what the source declares. init() is
101// its only caller on chain; a test calls it to render a page that does not
102// depend on what the test before it wrote, since an Example runs after every
103// Test in the package and sees the state they left.
104func reset() {
105 declared = avl.Tree{}
106 for _, s := range seeded {
107 typ := vesting.Continuous
108 if s.delayed {
109 typ = vesting.Delayed
110 }
111 sch, err := vesting.New(s.original, s.start, s.end, typ)
112 if err != nil {
113 panic("vesting: seeded schedule for " + s.label + ": " + err.Error())
114 }
115 declared.Set(s.addr.String(), &entry{schedule: sch, label: s.label, seeded: true})
116 }
117}
118
119// Declare records the caller's own vesting schedule, replacing any previous
120// one. It cannot write a row for anybody else, which is what keeps the
121// registry honest without the realm being able to verify a thing.
122//
123// Pass original = 0 to declare that the address has no schedule at all.
124func Declare(cur realm, original, start, end int64, delayed bool) {
125 typ := vesting.Continuous
126 if delayed {
127 typ = vesting.Delayed
128 }
129 sch, err := vesting.New(original, start, end, typ)
130 if err != nil {
131 panic(err.Error())
132 }
133 who := unsafe.PreviousRealm().Address()
134 if e, ok := lookup(who); ok && e.seeded {
135 panic("vesting: " + who.String() + " is seeded in the source; edit the realm instead")
136 }
137 declared.Set(who.String(), &entry{schedule: sch, at: chainHeight()})
138 chain.Emit("declared", "addr", who.String(),
139 "original", strconv.FormatInt(original, 10),
140 "start", strconv.FormatInt(start, 10),
141 "end", strconv.FormatInt(end, 10),
142 "type", typ.String())
143}
144
145// Forget removes the caller's own declaration.
146func Forget(cur realm) {
147 who := unsafe.PreviousRealm().Address()
148 e, ok := lookup(who)
149 if !ok {
150 panic("vesting: nothing declared for " + who.String())
151 }
152 if e.seeded {
153 panic("vesting: " + who.String() + " is seeded in the source; edit the realm instead")
154 }
155 declared.Remove(who.String())
156 chain.Emit("forgot", "addr", who.String())
157}
158
159// Count returns how many schedules the registry holds.
160func Count() int { return declared.Size() }
161
162// ScheduleOf returns the declared schedule for addr, and whether there is one.
163func ScheduleOf(addr address) (original, start, end int64, delayed, ok bool) {
164 e, found := lookup(addr)
165 if !found {
166 return 0, 0, 0, false, false
167 }
168 s := e.schedule
169 return s.Original, s.Start, s.End, s.Type == vesting.Delayed, true
170}
171
172// SpendableOf returns what addr can move right now, and whether the answer
173// rests on a declared schedule. With ok false the figure is the whole balance,
174// which is correct only if the address really has no schedule.
175func SpendableOf(addr address) (spendable int64, ok bool) {
176 bal := balanceOf(addr)
177 e, found := lookup(addr)
178 if !found {
179 return bal, false
180 }
181 return e.schedule.Spendable(bal, now()), true
182}
183
184// LockedOf returns what addr cannot move right now, and whether the answer
185// rests on a declared schedule.
186func LockedOf(addr address) (locked int64, ok bool) {
187 e, found := lookup(addr)
188 if !found {
189 return 0, false
190 }
191 bal := balanceOf(addr)
192 l := e.schedule.Locked(now())
193 if l > bal {
194 l = bal // cannot lock more than is there
195 }
196 return l, true
197}
198
199// lookup reads a row. avl's Get returns ONE value and nil for a miss, unlike
200// Remove which returns two; that asymmetry is a documented gno-vs-Go trap.
201func lookup(addr address) (*entry, bool) {
202 v := declared.Get(addr.String())
203 if v == nil {
204 return nil, false
205 }
206 return v.(*entry), true
207}
208
209// balanceOf reads the address's TOTAL ugnot, locked included. That is what
210// banker.GetCoins reports and the only balance figure a realm can obtain.
211// A readonly banker takes no realm, so this works from inside Render.
212func balanceOf(addr address) int64 {
213 return banker.NewReadonlyBanker().GetCoins(addr).AmountOf(Denom)
214}
215
216// gnot renders a ugnot amount as GNOT with six decimals and no float.
217func gnot(u int64) string {
218 neg := u < 0
219 if neg {
220 u = -u
221 }
222 whole, frac := u/1_000_000, u%1_000_000
223 s := ufmt.Sprintf("%d.%s", whole, pad6(frac))
224 if neg {
225 s = "-" + s
226 }
227 return s + " GNOT"
228}
229
230// pad6 left-pads to six digits. ufmt has no width flags, so this is by hand.
231func pad6(n int64) string {
232 s := strconv.FormatInt(n, 10)
233 for len(s) < 6 {
234 s = "0" + s
235 }
236 return s
237}
238
239// permille renders tenths of a percent as a percentage with one decimal.
240func permille(p int64) string {
241 return ufmt.Sprintf("%d.%d%%", p/10, p%10)
242}
243
244// duration renders a span of seconds as days and hours, which is the only
245// resolution worth reading on a two-year schedule.
246func duration(sec int64) string {
247 if sec <= 0 {
248 return "complete"
249 }
250 d, h := sec/86400, (sec%86400)/3600
251 var b strings.Builder
252 if d > 0 {
253 b.WriteString(strconv.FormatInt(d, 10))
254 b.WriteString("d ")
255 }
256 b.WriteString(strconv.FormatInt(h, 10))
257 b.WriteString("h")
258 return b.String()
259}
260
261// isAddr is a cheap shape check, so a typo renders as "not an address" rather
262// than as an account with a zero balance.
263func isAddr(s string) bool {
264 if len(s) != 40 || !strings.HasPrefix(s, "g1") {
265 return false
266 }
267 for _, r := range s {
268 if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') {
269 return false
270 }
271 }
272 return true
273}
274
275// now is the chain's own clock, in unix seconds. time.Now() in a realm returns
276// the BLOCK time, not a wall clock, which is exactly the value the ante
277// handler compares a vesting schedule against.
278func now() int64 { return time.Now().Unix() }
279
280func chainHeight() int64 { return runtime.ChainHeight() }