PathrockNetwork Gno Explorer
HomeBlocksTransactionsRealmsPackagesValidators

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/grc20votes/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr / grc20votes
Files
2 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/grc20votes/v0
gno
0.9

Files (2)

  • gnomod.tomltoml
  • grc20votes.gnogno
grc20votes.gnogno
1// Package grc20votes is a GRC20-shaped ledger that remembers what every2// holder's voting power used to be.3//4// An analog of OpenZeppelin's ERC20Votes, not a transliteration. Where the5// EVM's shape exists only because the EVM is what it is — opt-in delegation, a6// storage model that charges for every extra word forever — gno gets to do the7// obvious thing instead: delegation defaults to self, and the checkpoint rides8// in the account record a transfer was going to write anyway.9//10// # What a consumer does11//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 a24// crossing function. That is not a limitation worked around — it is the right25// 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 a31// consumer holds a *Ledger and nothing else. That matters more here than it32// looks: /p/-declared types can be named by other /p/ packages, so an exported33// *account or a method returning one would let a stranger declare a mutator34// over it — and gno's storage-realm borrow would run that mutator under the35// CONSUMING realm's authority. See gno-security-guide.md §3(B) and §4; this36// follows the encapsulation pattern p/nt/grc20/v0 sets, with the37// 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, so40// there is nothing to launder through. Adding one later would need the41// parameter type declared in the consuming realm, not here.42package grc20votes4344import (45	"chain"46	"chain/runtime"47	"math/overflow"48	"strconv"49	"time"5051	checkpoint "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/checkpoint/v0"52	bptree "gno.land/p/nt/bptree/v0"53)5455// Bps is the basis-point scale, named once because it is load-bearing in three56// places at once here and in any governor reading this ledger: the supply57// ceiling, a rendered percentage, and a quorum. A scale written out separately58// in each is a number that can come to disagree with itself.59const Bps = int64(10000)6061// MaxSupply keeps a tally's arithmetic honest for anything weighing this62// ledger. A threshold comparison is yes*Bps >= (yes+no)*threshold, and yes*Bps63// overflows int64 once the supply passes MaxInt64/Bps — a wrapped tally goes64// 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 base67// 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) / Bps7071// Event names are grc20's exactly (p/nt/grc20/v0: types.gno72// TransferEvent, token.gno chain.Emit), so an indexer following73// Transfer/Approval sees the same stream a stock token produces.74//75// What this does NOT do is implement grc20.Teller. The SECURITY note on76// types.gno's Teller tells consumers to reject anything failing77// IsCanonicalTeller, so satisfying it would be worthless to a careful caller78// and misleading to a careless one.79const (80	mintEvent     = "Mint"81	burnEvent     = "Burn"82	transferEvent = "Transfer"83	approvalEvent = "Approval"8485	// delegateChangedEvent has no counterpart in grc20 because grc20 has no86	// delegation. The name is OpenZeppelin's, since anybody indexing a87	// governance token already knows it.88	delegateChangedEvent = "DelegateChanged"89)9091// sep joins the two halves of an allowance key. The checkpoint package's92// separator rather than a byte that happens to match it: two constants with the93// same value in different files eventually disagree.94const sep = checkpoint.Sep9596// supplyKey checkpoints the supply like any holder, under a key no address can97// take. "~" is not in the bech32 alphabet and an address always begins with98// "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"101102// 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 this105// 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 zero108// (grc20 token.gno, led.balances.Remove); doing that here would delete the109// voting history, and an account oscillating through zero would churn keys.110type account struct {111	balance  int64112	delegate address // "" means self — see Delegate113	votes    checkpoint.Series114}115116// 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: the119// trees inside carry that realm's storage stamp, so a method here borrows the120// 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 supplies123// one so its OWN clock governs epochs; nil means the chain's, which is what an124// ordinary deployment wants.125//126// It exists because a realm that can fast-forward its clock for testing cannot127// fast-forward this package's: a pure package may not import a realm, and a128// SETTABLE package global would be a capability any caller could take. Bound at129// 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() int64133134	// Now is the same block's wall-clock time, unix seconds. The ledger itself135	// quantises by HEIGHT and always will — epoch quantisation is what makes the136	// anti-flash-loan property structural — but it passes this through to the137	// governor on the Electorate interface, where a voting DEADLINE lives.138	Now() int64139}140141type Ledger struct {142	// clock is nil on an ordinary deployment; see Clock.143	clock    Clock144	name     string145	symbol   string146	decimals int147	id       string148149	// epochBlocks quantises the clock. Every change inside one epoch coalesces150	// into a single update of a record already being written, which is what151	// bounds archive growth by wall-clock activity rather than by block152	// production — and it makes the anti-flash-loan property an invariant153	// rather than a discipline, since a transaction cannot outlive its block154	// and a block cannot outlive its epoch.155	epochBlocks int64156157	accounts   *bptree.BPTree // address -> *account158	allowances *bptree.BPTree // owner + sep + spender -> int64159160	// THIS ARCHIVE IS NEVER TRIMMED, and that is a property of the design rather161	// than an omission. checkpoint.Archive has a Trim, the claim's hourly stake162	// series uses it, and one line here would look like an ordinary storage163	// saving.164	//165	// Trim's own contract is "ValueAt exact AT AND ABOVE keepFrom". Every question166	// in the realm above is anchored at a PAST epoch and reads two values from167	// here — PastVotes for each voter's ceiling and PastTotal for the bar those168	// votes are judged against — so trimming past a live question's epoch makes169	// both inexact, independently of each other. That is the numerator and the170	// denominator drifting to different instants, which is the defect that171	// 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 any174	// open question is anchored at, which means knowing every open question — a175	// fact this package deliberately does not have. check-epoch-coherence arm 12176	// pins the call sites meanwhile.177	archive *checkpoint.Archive178179	supply checkpoint.Series180	total  int64181}182183// NewLedger creates an empty ledger. The identity is fixed here rather than184// read from a constant, so one realm can run several and a court can name its185// own coin.186//187// epochBlocks is the quantisation, in blocks. At gno's five-second cadence 720188// 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 whose190// 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 = clk194	return l195}196197func 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 symbol210		// qualified by whatever the consumer wants. A realm that wants the211		// 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}219220// SetID names this token the way an indexer will see it, usually221// "gno.land/r/you/realm.SYMBOL". Called once, at construction time, by the222// realm that knows its own path — a /p/ package cannot ask.223func (l *Ledger) SetID(id string) { l.id = id }224225// ------------------------------------------------------------------ reads --226227// 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 turns231// on, and the reason these are grouped apart from them.232233func (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 }238239func (l *Ledger) BalanceOf(owner address) int64 {240	if a := l.getAccount(owner); a != nil {241		return a.balance242	}243	return 0244}245246func (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 0251}252253// 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 0260}261262// DelegateOf is who votes an address's balance. Self unless they said263// 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 who268	}269	return a.delegate270}271272// PastVotes is voting power throughout a SEALED epoch. Refusing the current one273// is the anti-flash-loan property as an invariant rather than a convention: a274// transaction runs inside one block, a block inside one epoch, and this will275// 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 0281	}282	return a.votes.ValueAt(l.archive, string(who), at)283}284285// PastTotal is every unit in existence during a sealed epoch, which a quorum is286// a fraction of.287//288// Because delegation defaults to self, everyone's PastVotes sums to exactly289// this. OZ cannot say so: getPastTotalSupply counts all supply while290// getPastVotes counts only DELEGATED supply, so the denominator exceeds the291// 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}296297// EngagedTotal is what a governor measures quorum against. Every holder is298// engaged here by definition, since power is self-delegated by default — a299// realm wanting idle weight dropped out of the denominator wraps this.300func (l *Ledger) EngagedTotal(at uint32) int64 { return l.PastTotal(at) }301302// WalkSupply visits the supply's CHANGE points, newest first, and stops early303// 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 wrong307// shape for drawing a line — a chart over a year of hourly epochs would be308// 8,760 lookups to find the handful of epochs that actually moved. The archive309// already stores only the movements, so this hands them over directly and the310// 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 running313// epoch, which is exactly the value a live chart wants at its right edge. It314// may still coalesce before the epoch seals, so it is a reading and not a315// 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			return321		}322	}323	if e1, prev := l.supply.Prev(); e1 != 0 {324		if fn(e1, prev) {325			return326		}327	}328	l.archive.WalkDesc(supplyKey, fn)329}330331// WalkAccounts hands every account's address and balance to fn, ascending by332// 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 held335// exactly this; what was missing was a way to read it as a set rather than one336// address at a time. A consumer wanting a holder ranking would otherwise have337// had to maintain a second index alongside every mint, burn and transfer — a338// 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 and341// stay in the tree — Remove would drop the delegate and the vote history with342// it, which is why the balance path never removes — so filtering here would343// quietly hide accounts that still carry voting state. The caller knows which344// 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 false350		}351		return fn(address(k), a.balance)352	})353}354355// Epoch is now. A proposal snapshots Epoch()-1 and stores it; nothing356// re-derives it later.357func (l *Ledger) Epoch() uint32 {358	// 1-based, so zero can mean "before this token existed" — which is what359	// lets an untouched series answer correctly without storing anything.360	return uint32(l.Height()/l.epochBlocks) + 1361}362363// Height is this ledger's clock, and the governor's: it is on the Electorate364// interface so a governor weighing votes uses the same height the ledger365// quantised its snapshots by. Falling back to the chain when no clock was366// 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}373374// Now is this ledger's wall clock, forwarded to the governor through the375// Electorate interface. It is NOT used to quantise anything here: epochs are376// height-quantised by design (see Clock), and this exists only so a deadline377// 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}384385// EpochBlocks is the quantisation this ledger was built with, so a consumer386// rendering a deadline in hours does not have to be told twice.387func (l *Ledger) EpochBlocks() int64 { return l.epochBlocks }388389func (l *Ledger) mustBeSealed(at uint32) {390	if at == 0 || at >= l.Epoch() {391		panic("grc20votes: that epoch has not been sealed yet")392	}393}394395// ----------------------------------------------------------------- writes --396//397// Every one of these takes the acting address rather than reading it. The realm398// above has the `cur realm` and is the only thing that can authenticate; being399// handed an address here is what keeps this package free of a capability it400// would have no way to check.401402// Transfer moves `from`'s own tokens.403func (l *Ledger) Transfer(from, to address, amount int64) {404	l.move(from, to, amount)405}406407// 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}426427// 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: nothing435	// in move calls out, so today the order is unobservable and no test can436	// tell the difference. It is written this way for the day something here437	// does call out, when it becomes the difference between spending an438	// 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}446447// Mint creates tokens. This package holds no minter: who may call it is the448// 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)452453	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 = next458	l.supply.SetAt(l.archive, supplyKey, l.Epoch(), next)459460	a := l.openAccount(to)461	a.balance += amount462	l.addVotes(delegateeOf(to, a), amount)463464	// 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 sends467	// a TRANSFER with an empty `from` and its Burn one with an empty `to`. That468	// is the ERC20 convention, and how anything written against the standard469	// reconstructs balances — sum the Transfers, treat the empty counterparty470	// as the supply change. Emitting only "Mint" matches the names while471	// breaking what they are for.472	//473	// "Mint" is kept because it says plainly what happened. Anything totalling474	// supply should total Transfers and not both, which is safe for a standard475	// 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}488489// 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 -= amount497	l.addVotes(delegateeOf(from, a), -amount)498499	l.total -= amount500	l.supply.SetAt(l.archive, supplyKey, l.Epoch(), l.total)501502	// The other half of the convention: a burn is a Transfer to nowhere. See503	// 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}516517// Delegate points an address's voting power at someone else.518//519// Self-delegation is the DEFAULT, which OZ cannot afford: there it would put520// two SSTOREs on every transfer forever, so it is opt-in and everybody is521// surprised by it once. Here the checkpoint lives in a record the transfer522// already writes.523//524// One hop, not transitive: if A delegates to B and B to C, A's weight sits with525// B. Transitivity invites cycles and unbounded walks and buys nothing a second526// call cannot.527func (l *Ledger) Delegate(from, to address) {528	mustBeValid(to)529530	if l.getAccount(from) == nil && to == from {531		// Already self-delegated, because everybody is: DelegateOf reads a532		// missing account and a blank delegate the same way, so a record here533		// 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 has536		// to persist for when it is funded. What it cannot usefully do is name537		// itself.538		return539	}540	a := l.openAccount(from)541542	was := delegateeOf(from, a)543	if to == from {544		a.delegate = "" // normalised, so "self" has one representation545	} else {546		a.delegate = to547	}548	now := delegateeOf(from, a)549	if was == now {550		return551	}552	l.addVotes(was, -a.balance)553	l.addVotes(now, a.balance)554555	// The only state change here that nothing else reveals. Delegating moves556	// power without moving a coin, so an indexer sees a holder's weight vanish557	// with nothing to explain it, and the only alternative is polling558	// DelegateOf for every address anyone has heard of.559	//560	// OZ also emits DelegateVotesChanged on every power change, which here561	// would fire on every transfer, mint and burn — for something a caller can562	// ask: VotesOf answers now, PastVotes for any sealed epoch. Emitting it563	// 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}571572// --------------------------------------------------------------- innards --573574func (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)582583	src.balance -= amount584	dst.balance += amount585586	// Resolved once each. When both sides vote their own balance — the587	// overwhelmingly common case — these are the records already in hand, so a588	// checkpointed transfer dirties the same two objects a plain one would. The one589	// exception is a side's FIRST write in a new epoch, which also rolls one point590	// into the archive (one added page); amortised across the epoch's writes that591	// 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)594595	chain.Emit(transferEvent,596		"token", l.id,597		"from", from.String(),598		"to", to.String(),599		"value", strconv.Itoa(int(amount)),600	)601}602603func delegateeOf(self address, a *account) address {604	if a.delegate == "" {605		return self606	}607	return a.delegate608}609610// addVotes moves voting power and checkpoints it.611func (l *Ledger) addVotes(who address, delta int64) {612	if delta == 0 {613		return614	}615	a := l.openAccount(who)616	next := a.votes.Value() + delta617	if next < 0 {618		panic("grc20votes: voting power would go negative")619	}620	a.votes.SetAt(l.archive, string(who), l.Epoch(), next)621}622623func (l *Ledger) getAccount(who address) *account {624	if v := l.accounts.Get(string(who)); v != nil {625		return v.(*account)626	}627	return nil628}629630func (l *Ledger) openAccount(who address) *account {631	if a := l.getAccount(who); a != nil {632		return a633	}634	a := &account{}635	l.accounts.Set(string(who), a)636	return a637}638639func mustBeValid(a address) {640	if !a.IsValid() {641		panic("grc20votes: not an address")642	}643}644645func mustBePositive(amount int64) {646	if amount <= 0 {647		panic("grc20votes: amount must be positive")648	}649}650

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.