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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

grc20votes.gno

23.83 Kb · 649 lines
  1// Package grc20votes is a GRC20-shaped ledger that remembers what every
  2// holder's voting power used to be.
  3//
  4// An analog of OpenZeppelin's ERC20Votes, not a transliteration. Where the
  5// EVM's shape exists only because the EVM is what it is — opt-in delegation, a
  6// storage model that charges for every extra word forever — gno gets to do the
  7// obvious thing instead: delegation defaults to self, and the checkpoint rides
  8// in the account record a transfer was going to write anyway.
  9//
 10// # What a consumer does
 11//
 12// A realm allocates one Ledger and keeps it in an unexported package variable:
 13//
 14//	var ledger = grc20votes.NewLedger("Kourt Governance", "COURT", 6, 720)
 15//
 16//	func Transfer(cur realm, to address, amount int64) {
 17//		if !cur.IsCurrent() {
 18//			panic("stale realm")
 19//		}
 20//		ledger.Transfer(cur.Previous().Address(), to, amount)
 21//	}
 22//
 23// Nothing here takes a `cur realm`, because a /p/ package cannot declare a
 24// crossing function. That is not a limitation worked around — it is the right
 25// split. Authentication belongs to the realm that has a caller to authenticate;
 26// this package is told WHICH address is acting and does the bookkeeping.
 27//
 28// # Why it is safe to keep this in /p/
 29//
 30// Every field is unexported and no method hands back an interior pointer, so a
 31// consumer holds a *Ledger and nothing else. That matters more here than it
 32// looks: /p/-declared types can be named by other /p/ packages, so an exported
 33// *account or a method returning one would let a stranger declare a mutator
 34// over it — and gno's storage-realm borrow would run that mutator under the
 35// CONSUMING realm's authority. See gno-security-guide.md §3(B) and §4; this
 36// follows the encapsulation pattern p/demo/tokens/grc20 sets, with the
 37// authentication moved out to the realm rather than carried on a teller.
 38//
 39// The one exception is deliberate: Ledger has no method taking a callback, so
 40// there is nothing to launder through. Adding one later would need the
 41// parameter type declared in the consuming realm, not here.
 42package grc20votes
 43
 44import (
 45	"chain"
 46	"chain/runtime"
 47	"math/overflow"
 48	"strconv"
 49	"time"
 50
 51	checkpoint "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/checkpoint/v0"
 52	bptree "gno.land/p/nt/bptree/v0"
 53)
 54
 55// Bps is the basis-point scale, named once because it is load-bearing in three
 56// places at once here and in any governor reading this ledger: the supply
 57// ceiling, a rendered percentage, and a quorum. A scale written out separately
 58// in each is a number that can come to disagree with itself.
 59const Bps = int64(10000)
 60
 61// MaxSupply keeps a tally's arithmetic honest for anything weighing this
 62// ledger. A threshold comparison is yes*Bps >= (yes+no)*threshold, and yes*Bps
 63// overflows int64 once the supply passes MaxInt64/Bps — a wrapped tally goes
 64// negative rather than failing loudly, so a won vote reports as lost.
 65//
 66// Not a remote bound: six decimals and a billion units in issue is 1e15 base
 67// units, already past it. Capped at mint, so a tally needs no overflow checks —
 68// roughly 922 million whole tokens at six decimals.
 69const MaxSupply = int64(9223372036854775807) / Bps
 70
 71// Event names are grc20's exactly (p/demo/tokens/grc20: types.gno
 72// TransferEvent, token.gno chain.Emit), so an indexer following
 73// Transfer/Approval sees the same stream a stock token produces.
 74//
 75// What this does NOT do is implement grc20.Teller. The SECURITY note on
 76// types.gno's Teller tells consumers to reject anything failing
 77// IsCanonicalTeller, so satisfying it would be worthless to a careful caller
 78// and misleading to a careless one.
 79const (
 80	mintEvent     = "Mint"
 81	burnEvent     = "Burn"
 82	transferEvent = "Transfer"
 83	approvalEvent = "Approval"
 84
 85	// delegateChangedEvent has no counterpart in grc20 because grc20 has no
 86	// delegation. The name is OpenZeppelin's, since anybody indexing a
 87	// governance token already knows it.
 88	delegateChangedEvent = "DelegateChanged"
 89)
 90
 91// sep joins the two halves of an allowance key. The checkpoint package's
 92// separator rather than a byte that happens to match it: two constants with the
 93// same value in different files eventually disagree.
 94const sep = checkpoint.Sep
 95
 96// supplyKey checkpoints the supply like any holder, under a key no address can
 97// take. "~" is not in the bech32 alphabet and an address always begins with
 98// "g". Not the separator itself, which the checkpoint package refuses as a key:
 99// one containing the separator shares a page range with its neighbours.
100const supplyKey = "~supply"
101
102// account is one holder: what they have, who votes it, and what it was.
103//
104// The votes series is a value field, not a pointer, so changing it dirties this
105// record and nothing else — which is why checkpointing is affordable here.
106//
107// The record outlives a zero balance. The stock ledger removes a holder at zero
108// (grc20 token.gno, led.balances.Remove); doing that here would delete the
109// voting history, and an account oscillating through zero would churn keys.
110type account struct {
111	balance  int64
112	delegate address // "" means self — see Delegate
113	votes    checkpoint.Series
114}
115
116// Ledger is one token's balances and the history of their voting power.
117//
118// Allocated by the consuming realm, which is what makes this work at all: the
119// trees inside carry that realm's storage stamp, so a method here borrows the
120// consumer's authority for the write and the storage is billed to them. A /p/
121// package's own state is frozen after init and could hold none of this.
122// Clock is where this ledger's sense of height comes from. A realm supplies
123// one so its OWN clock governs epochs; nil means the chain's, which is what an
124// ordinary deployment wants.
125//
126// It exists because a realm that can fast-forward its clock for testing cannot
127// fast-forward this package's: a pure package may not import a realm, and a
128// SETTABLE package global would be a capability any caller could take. Bound at
129// construction by the realm that owns the ledger, it is neither.
130type Clock interface {
131	// Height is the block height this ledger should quantise epochs by.
132	Height() int64
133
134	// Now is the same block's wall-clock time, unix seconds. The ledger itself
135	// quantises by HEIGHT and always will — epoch quantisation is what makes the
136	// anti-flash-loan property structural — but it passes this through to the
137	// governor on the Electorate interface, where a voting DEADLINE lives.
138	Now() int64
139}
140
141type Ledger struct {
142	// clock is nil on an ordinary deployment; see Clock.
143	clock    Clock
144	name     string
145	symbol   string
146	decimals int
147	id       string
148
149	// epochBlocks quantises the clock. Every change inside one epoch coalesces
150	// into a single update of a record already being written, which is what
151	// bounds archive growth by wall-clock activity rather than by block
152	// production — and it makes the anti-flash-loan property an invariant
153	// rather than a discipline, since a transaction cannot outlive its block
154	// and a block cannot outlive its epoch.
155	epochBlocks int64
156
157	accounts   *bptree.BPTree // address -> *account
158	allowances *bptree.BPTree // owner + sep + spender -> int64
159
160	// THIS ARCHIVE IS NEVER TRIMMED, and that is a property of the design rather
161	// than an omission. checkpoint.Archive has a Trim, the claim's hourly stake
162	// series uses it, and one line here would look like an ordinary storage
163	// saving.
164	//
165	// Trim's own contract is "ValueAt exact AT AND ABOVE keepFrom". Every question
166	// in the realm above is anchored at a PAST epoch and reads two values from
167	// here — PastVotes for each voter's ceiling and PastTotal for the bar those
168	// votes are judged against — so trimming past a live question's epoch makes
169	// both inexact, independently of each other. That is the numerator and the
170	// denominator drifting to different instants, which is the defect that
171	// produced turnout at 200-400% of its own bar and was reverted.
172	//
173	// What would have to be true to relax it: keepFrom below the oldest epoch any
174	// open question is anchored at, which means knowing every open question — a
175	// fact this package deliberately does not have. check-epoch-coherence arm 12
176	// pins the call sites meanwhile.
177	archive *checkpoint.Archive
178
179	supply checkpoint.Series
180	total  int64
181}
182
183// NewLedger creates an empty ledger. The identity is fixed here rather than
184// read from a constant, so one realm can run several and a court can name its
185// own coin.
186//
187// epochBlocks is the quantisation, in blocks. At gno's five-second cadence 720
188// is an hour, which is the figure the rest of this design was measured at.
189// NewLedgerWithClock is NewLedger with the height source named. A realm whose
190// own clock can be fast-forwarded passes itself; everyone else calls NewLedger.
191func NewLedgerWithClock(name, symbol string, decimals int, epochBlocks int64, clk Clock) *Ledger {
192	l := NewLedger(name, symbol, decimals, epochBlocks)
193	l.clock = clk
194	return l
195}
196
197func NewLedger(name, symbol string, decimals int, epochBlocks int64) *Ledger {
198	if name == "" || symbol == "" {
199		panic("grc20votes: a token needs a name and a symbol")
200	}
201	if decimals < 0 || decimals > 18 {
202		panic("grc20votes: decimals must be between 0 and 18")
203	}
204	if epochBlocks <= 0 {
205		panic("grc20votes: an epoch has to be at least one block long")
206	}
207	return &Ledger{
208		name: name, symbol: symbol, decimals: decimals,
209		// The pkgpath is not knowable from here, so the id is the symbol
210		// qualified by whatever the consumer wants. A realm that wants the
211		// grc20reg shape passes its own path in the name.
212		id:          symbol,
213		epochBlocks: epochBlocks,
214		accounts:    bptree.NewBPTree32(),
215		allowances:  bptree.NewBPTree32(),
216		archive:     checkpoint.NewArchive(),
217	}
218}
219
220// SetID names this token the way an indexer will see it, usually
221// "gno.land/r/you/realm.SYMBOL". Called once, at construction time, by the
222// realm that knows its own path — a /p/ package cannot ask.
223func (l *Ledger) SetID(id string) { l.id = id }
224
225// ------------------------------------------------------------------ reads --
226
227// The GRC20 reads, with the names that standard uses, answering about NOW.
228//
229// Governance does not read any of these. It asks PastVotes and PastTotal,
230// which answer about a sealed epoch — the distinction the whole design turns
231// on, and the reason these are grouped apart from them.
232
233func (l *Ledger) Name() string       { return l.name }
234func (l *Ledger) Symbol() string     { return l.symbol }
235func (l *Ledger) Decimals() int      { return l.decimals }
236func (l *Ledger) ID() string         { return l.id }
237func (l *Ledger) TotalSupply() int64 { return l.total }
238
239func (l *Ledger) BalanceOf(owner address) int64 {
240	if a := l.getAccount(owner); a != nil {
241		return a.balance
242	}
243	return 0
244}
245
246func (l *Ledger) Allowance(owner, spender address) int64 {
247	if v := l.allowances.Get(string(owner) + sep + string(spender)); v != nil {
248		return v.(int64)
249	}
250	return 0
251}
252
253// VotesOf is voting power right now — the sum of every balance delegated here,
254// including the holder's own unless they delegated it away.
255func (l *Ledger) VotesOf(who address) int64 {
256	if a := l.getAccount(who); a != nil {
257		return a.votes.Value()
258	}
259	return 0
260}
261
262// DelegateOf is who votes an address's balance. Self unless they said
263// otherwise, which is the default nobody has to remember.
264func (l *Ledger) DelegateOf(who address) address {
265	a := l.getAccount(who)
266	if a == nil || a.delegate == "" {
267		return who
268	}
269	return a.delegate
270}
271
272// PastVotes is voting power throughout a SEALED epoch. Refusing the current one
273// is the anti-flash-loan property as an invariant rather than a convention: a
274// transaction runs inside one block, a block inside one epoch, and this will
275// not answer for the epoch it is in.
276func (l *Ledger) PastVotes(who address, at uint32) int64 {
277	l.mustBeSealed(at)
278	a := l.getAccount(who)
279	if a == nil {
280		return 0
281	}
282	return a.votes.ValueAt(l.archive, string(who), at)
283}
284
285// PastTotal is every unit in existence during a sealed epoch, which a quorum is
286// a fraction of.
287//
288// Because delegation defaults to self, everyone's PastVotes sums to exactly
289// this. OZ cannot say so: getPastTotalSupply counts all supply while
290// getPastVotes counts only DELEGATED supply, so the denominator exceeds the
291// achievable numerator and quorum is unreachable where few have delegated.
292func (l *Ledger) PastTotal(at uint32) int64 {
293	l.mustBeSealed(at)
294	return l.supply.ValueAt(l.archive, supplyKey, at)
295}
296
297// EngagedTotal is what a governor measures quorum against. Every holder is
298// engaged here by definition, since power is self-delegated by default — a
299// realm wanting idle weight dropped out of the denominator wraps this.
300func (l *Ledger) EngagedTotal(at uint32) int64 { return l.PastTotal(at) }
301
302// WalkSupply visits the supply's CHANGE points, newest first, and stops early
303// when fn returns true. An epoch in which supply did not move is not a point:
304// a reader forward-fills between them.
305//
306// A WALK, not a per-epoch read. PastTotal answers one epoch and is the wrong
307// shape for drawing a line — a chart over a year of hourly epochs would be
308// 8,760 lookups to find the handful of epochs that actually moved. The archive
309// already stores only the movements, so this hands them over directly and the
310// cost is the number of mints, not the age of the court.
311//
312// Unsealed on purpose, unlike PastTotal: the newest point may be the running
313// epoch, which is exactly the value a live chart wants at its right edge. It
314// may still coalesce before the epoch seals, so it is a reading and not a
315// record.
316func (l *Ledger) WalkSupply(fn func(e uint32, v int64) bool) {
317	// The two hot slots hold the newest points and are not in the archive yet.
318	if e0 := l.supply.Since(); e0 != 0 {
319		if fn(e0, l.supply.Value()) {
320			return
321		}
322	}
323	if e1, prev := l.supply.Prev(); e1 != 0 {
324		if fn(e1, prev) {
325			return
326		}
327	}
328	l.archive.WalkDesc(supplyKey, fn)
329}
330
331// WalkAccounts hands every account's address and balance to fn, ascending by
332// address, and stops early when fn returns true.
333//
334// NO NEW STATE. The accounts tree is the ledger's own index and has always held
335// exactly this; what was missing was a way to read it as a set rather than one
336// address at a time. A consumer wanting a holder ranking would otherwise have
337// had to maintain a second index alongside every mint, burn and transfer — a
338// duplicate that can only ever drift out of step with the first.
339//
340// ZERO BALANCES ARE HANDED OVER TOO, deliberately. An account can reach zero and
341// stay in the tree — Remove would drop the delegate and the vote history with
342// it, which is why the balance path never removes — so filtering here would
343// quietly hide accounts that still carry voting state. The caller knows which
344// question it is asking; this one answers "what does the ledger hold".
345func (l *Ledger) WalkAccounts(fn func(who address, bal int64) bool) {
346	l.accounts.Iterate("", "", func(k string, v any) bool {
347		a, ok := v.(*account)
348		if !ok {
349			return false
350		}
351		return fn(address(k), a.balance)
352	})
353}
354
355// Epoch is now. A proposal snapshots Epoch()-1 and stores it; nothing
356// re-derives it later.
357func (l *Ledger) Epoch() uint32 {
358	// 1-based, so zero can mean "before this token existed" — which is what
359	// lets an untouched series answer correctly without storing anything.
360	return uint32(l.Height()/l.epochBlocks) + 1
361}
362
363// Height is this ledger's clock, and the governor's: it is on the Electorate
364// interface so a governor weighing votes uses the same height the ledger
365// quantised its snapshots by. Falling back to the chain when no clock was
366// supplied keeps every existing construction working unchanged.
367func (l *Ledger) Height() int64 {
368	if l.clock != nil {
369		return l.clock.Height()
370	}
371	return runtime.ChainHeight()
372}
373
374// Now is this ledger's wall clock, forwarded to the governor through the
375// Electorate interface. It is NOT used to quantise anything here: epochs are
376// height-quantised by design (see Clock), and this exists only so a deadline
377// the governor publishes can be a date.
378func (l *Ledger) Now() int64 {
379	if l.clock != nil {
380		return l.clock.Now()
381	}
382	return time.Now().Unix()
383}
384
385// EpochBlocks is the quantisation this ledger was built with, so a consumer
386// rendering a deadline in hours does not have to be told twice.
387func (l *Ledger) EpochBlocks() int64 { return l.epochBlocks }
388
389func (l *Ledger) mustBeSealed(at uint32) {
390	if at == 0 || at >= l.Epoch() {
391		panic("grc20votes: that epoch has not been sealed yet")
392	}
393}
394
395// ----------------------------------------------------------------- writes --
396//
397// Every one of these takes the acting address rather than reading it. The realm
398// above has the `cur realm` and is the only thing that can authenticate; being
399// handed an address here is what keeps this package free of a capability it
400// would have no way to check.
401
402// Transfer moves `from`'s own tokens.
403func (l *Ledger) Transfer(from, to address, amount int64) {
404	l.move(from, to, amount)
405}
406
407// Approve lets a spender move some of the owner's balance.
408func (l *Ledger) Approve(owner, spender address, amount int64) {
409	mustBeValid(spender)
410	if amount < 0 {
411		panic("grc20votes: negative allowance")
412	}
413	key := string(owner) + sep + string(spender)
414	if amount == 0 {
415		l.allowances.Remove(key)
416	} else {
417		l.allowances.Set(key, amount)
418	}
419	chain.Emit(approvalEvent,
420		"token", l.id,
421		"owner", owner.String(),
422		"spender", spender.String(),
423		"value", strconv.Itoa(int(amount)),
424	)
425}
426
427// TransferFrom spends an allowance.
428func (l *Ledger) TransferFrom(spender, from, to address, amount int64) {
429	key := string(from) + sep + string(spender)
430	allowed := l.Allowance(from, spender)
431	if allowed < amount {
432		panic("grc20votes: allowance exceeded")
433	}
434	// Debited before the move. Defence in depth rather than a live fix: nothing
435	// in move calls out, so today the order is unobservable and no test can
436	// tell the difference. It is written this way for the day something here
437	// does call out, when it becomes the difference between spending an
438	// allowance once and twice.
439	if rest := allowed - amount; rest == 0 {
440		l.allowances.Remove(key)
441	} else {
442		l.allowances.Set(key, rest)
443	}
444	l.move(from, to, amount)
445}
446
447// Mint creates tokens. This package holds no minter: who may call it is the
448// consuming realm's rule, because that is where the caller is known.
449func (l *Ledger) Mint(to address, amount int64) {
450	mustBeValid(to)
451	mustBePositive(amount)
452
453	next, ok := overflow.Add64(l.total, amount)
454	if !ok || next > MaxSupply {
455		panic("grc20votes: supply would exceed what a tally can weigh without overflowing")
456	}
457	l.total = next
458	l.supply.SetAt(l.archive, supplyKey, l.Epoch(), next)
459
460	a := l.openAccount(to)
461	a.balance += amount
462	l.addVotes(delegateeOf(to, a), amount)
463
464	// Both events, and the Transfer is the one that matters to anybody else.
465	//
466	// grc20 declares MintEvent and BurnEvent and emits neither: its Mint sends
467	// a TRANSFER with an empty `from` and its Burn one with an empty `to`. That
468	// is the ERC20 convention, and how anything written against the standard
469	// reconstructs balances — sum the Transfers, treat the empty counterparty
470	// as the supply change. Emitting only "Mint" matches the names while
471	// breaking what they are for.
472	//
473	// "Mint" is kept because it says plainly what happened. Anything totalling
474	// supply should total Transfers and not both, which is safe for a standard
475	// indexer since the standard never emits "Mint".
476	chain.Emit(transferEvent,
477		"token", l.id,
478		"from", "",
479		"to", to.String(),
480		"value", strconv.Itoa(int(amount)),
481	)
482	chain.Emit(mintEvent,
483		"token", l.id,
484		"to", to.String(),
485		"value", strconv.Itoa(int(amount)),
486	)
487}
488
489// Burn destroys `from`'s own tokens.
490func (l *Ledger) Burn(from address, amount int64) {
491	mustBePositive(amount)
492	a := l.getAccount(from)
493	if a == nil || a.balance < amount {
494		panic("grc20votes: insufficient balance")
495	}
496	a.balance -= amount
497	l.addVotes(delegateeOf(from, a), -amount)
498
499	l.total -= amount
500	l.supply.SetAt(l.archive, supplyKey, l.Epoch(), l.total)
501
502	// The other half of the convention: a burn is a Transfer to nowhere. See
503	// the note in Mint for why both events go out.
504	chain.Emit(transferEvent,
505		"token", l.id,
506		"from", from.String(),
507		"to", "",
508		"value", strconv.Itoa(int(amount)),
509	)
510	chain.Emit(burnEvent,
511		"token", l.id,
512		"from", from.String(),
513		"value", strconv.Itoa(int(amount)),
514	)
515}
516
517// Delegate points an address's voting power at someone else.
518//
519// Self-delegation is the DEFAULT, which OZ cannot afford: there it would put
520// two SSTOREs on every transfer forever, so it is opt-in and everybody is
521// surprised by it once. Here the checkpoint lives in a record the transfer
522// already writes.
523//
524// One hop, not transitive: if A delegates to B and B to C, A's weight sits with
525// B. Transitivity invites cycles and unbounded walks and buys nothing a second
526// call cannot.
527func (l *Ledger) Delegate(from, to address) {
528	mustBeValid(to)
529
530	if l.getAccount(from) == nil && to == from {
531		// Already self-delegated, because everybody is: DelegateOf reads a
532		// missing account and a blank delegate the same way, so a record here
533		// buys a key to store a fact true of every address that never existed.
534		//
535		// An address holding nothing CAN still name somebody else, and that has
536		// to persist for when it is funded. What it cannot usefully do is name
537		// itself.
538		return
539	}
540	a := l.openAccount(from)
541
542	was := delegateeOf(from, a)
543	if to == from {
544		a.delegate = "" // normalised, so "self" has one representation
545	} else {
546		a.delegate = to
547	}
548	now := delegateeOf(from, a)
549	if was == now {
550		return
551	}
552	l.addVotes(was, -a.balance)
553	l.addVotes(now, a.balance)
554
555	// The only state change here that nothing else reveals. Delegating moves
556	// power without moving a coin, so an indexer sees a holder's weight vanish
557	// with nothing to explain it, and the only alternative is polling
558	// DelegateOf for every address anyone has heard of.
559	//
560	// OZ also emits DelegateVotesChanged on every power change, which here
561	// would fire on every transfer, mint and burn — for something a caller can
562	// ask: VotesOf answers now, PastVotes for any sealed epoch. Emitting it
563	// would be transliteration.
564	chain.Emit(delegateChangedEvent,
565		"token", l.id,
566		"delegator", from.String(),
567		"fromDelegate", was.String(),
568		"toDelegate", now.String(),
569	)
570}
571
572// --------------------------------------------------------------- innards --
573
574func (l *Ledger) move(from, to address, amount int64) {
575	mustBeValid(to)
576	mustBePositive(amount)
577	src := l.getAccount(from)
578	if src == nil || src.balance < amount {
579		panic("grc20votes: insufficient balance")
580	}
581	dst := l.openAccount(to)
582
583	src.balance -= amount
584	dst.balance += amount
585
586	// Resolved once each. When both sides vote their own balance — the
587	// overwhelmingly common case — these are the records already in hand, so a
588	// checkpointed transfer dirties the same two objects a plain one would. The one
589	// exception is a side's FIRST write in a new epoch, which also rolls one point
590	// into the archive (one added page); amortised across the epoch's writes that
591	// is ~nothing, but it is not literally two objects at the boundary.
592	l.addVotes(delegateeOf(from, src), -amount)
593	l.addVotes(delegateeOf(to, dst), amount)
594
595	chain.Emit(transferEvent,
596		"token", l.id,
597		"from", from.String(),
598		"to", to.String(),
599		"value", strconv.Itoa(int(amount)),
600	)
601}
602
603func delegateeOf(self address, a *account) address {
604	if a.delegate == "" {
605		return self
606	}
607	return a.delegate
608}
609
610// addVotes moves voting power and checkpoints it.
611func (l *Ledger) addVotes(who address, delta int64) {
612	if delta == 0 {
613		return
614	}
615	a := l.openAccount(who)
616	next := a.votes.Value() + delta
617	if next < 0 {
618		panic("grc20votes: voting power would go negative")
619	}
620	a.votes.SetAt(l.archive, string(who), l.Epoch(), next)
621}
622
623func (l *Ledger) getAccount(who address) *account {
624	if v := l.accounts.Get(string(who)); v != nil {
625		return v.(*account)
626	}
627	return nil
628}
629
630func (l *Ledger) openAccount(who address) *account {
631	if a := l.getAccount(who); a != nil {
632		return a
633	}
634	a := &account{}
635	l.accounts.Set(string(who), a)
636	return a
637}
638
639func mustBeValid(a address) {
640	if !a.IsValid() {
641		panic("grc20votes: not an address")
642	}
643}
644
645func mustBePositive(amount int64) {
646	if amount <= 0 {
647		panic("grc20votes: amount must be positive")
648	}
649}