Search Apps Documentation Source Content File Folder Download Copy Actions Download 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() }