Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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() }