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

v0 source pure

Package grc20votes is a GRC20-shaped ledger that remembers what every holder's voting power used to be.

Overview

Package grc20votes is a GRC20-shaped ledger that remembers what every holder's voting power used to be.

An analog of OpenZeppelin's ERC20Votes, not a transliteration. Where the EVM's shape exists only because the EVM is what it is — opt-in delegation, a storage model that charges for every extra word forever — gno gets to do the obvious thing instead: delegation defaults to self, and the checkpoint rides in the account record a transfer was going to write anyway.

What a consumer does

A realm allocates one Ledger and keeps it in an unexported package variable:

Example
1var ledger = grc20votes.NewLedger("Kourt Governance", "COURT", 6, 720)
2
3func Transfer(cur realm, to address, amount int64) {
4	if !cur.IsCurrent() {
5		panic("stale realm")
6	}
7	ledger.Transfer(cur.Previous().Address(), to, amount)
8}

Nothing here takes a `cur realm`, because a /p/ package cannot declare a crossing function. That is not a limitation worked around — it is the right split. Authentication belongs to the realm that has a caller to authenticate; this package is told WHICH address is acting and does the bookkeeping.

Why it is safe to keep this in /p/

Every field is unexported and no method hands back an interior pointer, so a consumer holds a *Ledger and nothing else. That matters more here than it looks: /p/-declared types can be named by other /p/ packages, so an exported *account or a method returning one would let a stranger declare a mutator over it — and gno's storage-realm borrow would run that mutator under the CONSUMING realm's authority. See gno-security-guide.md §3(B) and §4; this follows the encapsulation pattern p/nt/grc20/v0 sets, with the authentication moved out to the realm rather than carried on a teller.

The one exception is deliberate: Ledger has no method taking a callback, so there is nothing to launder through. Adding one later would need the parameter type declared in the consuming realm, not here.

Constants 2

const Bps

1const Bps = int64(10000)
source

Bps is the basis-point scale, named once because it is load-bearing in three places at once here and in any governor reading this ledger: the supply ceiling, a rendered percentage, and a quorum. A scale written out separately in each is a number that can come to disagree with itself.

const MaxSupply

1const MaxSupply = int64(9223372036854775807) / Bps
source

MaxSupply keeps a tally's arithmetic honest for anything weighing this ledger. A threshold comparison is yes*Bps >= (yes+no)*threshold, and yes*Bps overflows int64 once the supply passes MaxInt64/Bps — a wrapped tally goes negative rather than failing loudly, so a won vote reports as lost.

Not a remote bound: six decimals and a billion units in issue is 1e15 base units, already past it. Capped at mint, so a tally needs no overflow checks — roughly 922 million whole tokens at six decimals.

Functions 2

func NewLedger

1func NewLedger(name, symbol string, decimals int, epochBlocks int64) *Ledger
source

func NewLedgerWithClock

1func NewLedgerWithClock(name, symbol string, decimals int, epochBlocks int64, clk Clock) *Ledger
source

NewLedger creates an empty ledger. The identity is fixed here rather than read from a constant, so one realm can run several and a court can name its own coin.

epochBlocks is the quantisation, in blocks. At gno's five-second cadence 720 is an hour, which is the figure the rest of this design was measured at. NewLedgerWithClock is NewLedger with the height source named. A realm whose own clock can be fast-forwarded passes itself; everyone else calls NewLedger.

Types 2

type Clock

interface
 1type Clock interface {
 2	// Height is the block height this ledger should quantise epochs by.
 3	Height() int64
 4
 5	// Now is the same block's wall-clock time, unix seconds. The ledger itself
 6	// quantises by HEIGHT and always will — epoch quantisation is what makes the
 7	// anti-flash-loan property structural — but it passes this through to the
 8	// governor on the Electorate interface, where a voting DEADLINE lives.
 9	Now() int64
10}
source

Ledger is one token's balances and the history of their voting power.

Allocated by the consuming realm, which is what makes this work at all: the trees inside carry that realm's storage stamp, so a method here borrows the consumer's authority for the write and the storage is billed to them. A /p/ package's own state is frozen after init and could hold none of this. Clock is where this ledger's sense of height comes from. A realm supplies one so its OWN clock governs epochs; nil means the chain's, which is what an ordinary deployment wants.

It exists because a realm that can fast-forward its clock for testing cannot fast-forward this package's: a pure package may not import a realm, and a SETTABLE package global would be a capability any caller could take. Bound at construction by the realm that owns the ledger, it is neither.

type Ledger

struct
 1type Ledger struct {
 2	// clock is nil on an ordinary deployment; see Clock.
 3	clock    Clock
 4	name     string
 5	symbol   string
 6	decimals int
 7	id       string
 8
 9	// epochBlocks quantises the clock. Every change inside one epoch coalesces
10	// into a single update of a record already being written, which is what
11	// bounds archive growth by wall-clock activity rather than by block
12	// production — and it makes the anti-flash-loan property an invariant
13	// rather than a discipline, since a transaction cannot outlive its block
14	// and a block cannot outlive its epoch.
15	epochBlocks int64
16
17	accounts   *bptree.BPTree // address -> *account
18	allowances *bptree.BPTree // owner + sep + spender -> int64
19
20	// THIS ARCHIVE IS NEVER TRIMMED, and that is a property of the design rather
21	// than an omission. checkpoint.Archive has a Trim, the claim's hourly stake
22	// series uses it, and one line here would look like an ordinary storage
23	// saving.
24	//
25	// Trim's own contract is "ValueAt exact AT AND ABOVE keepFrom". Every question
26	// in the realm above is anchored at a PAST epoch and reads two values from
27	// here — PastVotes for each voter's ceiling and PastTotal for the bar those
28	// votes are judged against — so trimming past a live question's epoch makes
29	// both inexact, independently of each other. That is the numerator and the
30	// denominator drifting to different instants, which is the defect that
31	// produced turnout at 200-400% of its own bar and was reverted.
32	//
33	// What would have to be true to relax it: keepFrom below the oldest epoch any
34	// open question is anchored at, which means knowing every open question — a
35	// fact this package deliberately does not have. check-epoch-coherence arm 12
36	// pins the call sites meanwhile.
37	archive *checkpoint.Archive
38
39	supply checkpoint.Series
40	total  int64
41}
source

Methods on Ledger

func Allowance

method on Ledger
1func (l *Ledger) Allowance(owner, spender address) int64
source

func Approve

method on Ledger
1func (l *Ledger) Approve(owner, spender address, amount int64)
source

Approve lets a spender move some of the owner's balance.

func BalanceOf

method on Ledger
1func (l *Ledger) BalanceOf(owner address) int64
source

func Burn

method on Ledger
1func (l *Ledger) Burn(from address, amount int64)
source

Burn destroys `from`'s own tokens.

func Decimals

method on Ledger
1func (l *Ledger) Decimals() int
source

func Delegate

method on Ledger
1func (l *Ledger) Delegate(from, to address)
source

Delegate points an address's voting power at someone else.

Self-delegation is the DEFAULT, which OZ cannot afford: there it would put two SSTOREs on every transfer forever, so it is opt-in and everybody is surprised by it once. Here the checkpoint lives in a record the transfer already writes.

One hop, not transitive: if A delegates to B and B to C, A's weight sits with B. Transitivity invites cycles and unbounded walks and buys nothing a second call cannot.

func DelegateOf

method on Ledger
1func (l *Ledger) DelegateOf(who address) address
source

DelegateOf is who votes an address's balance. Self unless they said otherwise, which is the default nobody has to remember.

func EngagedTotal

method on Ledger
1func (l *Ledger) EngagedTotal(at uint32) int64
source

EngagedTotal is what a governor measures quorum against. Every holder is engaged here by definition, since power is self-delegated by default — a realm wanting idle weight dropped out of the denominator wraps this.

func Epoch

method on Ledger
1func (l *Ledger) Epoch() uint32
source

Epoch is now. A proposal snapshots Epoch()-1 and stores it; nothing re-derives it later.

func EpochBlocks

method on Ledger
1func (l *Ledger) EpochBlocks() int64
source

EpochBlocks is the quantisation this ledger was built with, so a consumer rendering a deadline in hours does not have to be told twice.

func Height

method on Ledger
1func (l *Ledger) Height() int64
source

Height is this ledger's clock, and the governor's: it is on the Electorate interface so a governor weighing votes uses the same height the ledger quantised its snapshots by. Falling back to the chain when no clock was supplied keeps every existing construction working unchanged.

func ID

method on Ledger
1func (l *Ledger) ID() string
source

func Mint

method on Ledger
1func (l *Ledger) Mint(to address, amount int64)
source

Mint creates tokens. This package holds no minter: who may call it is the consuming realm's rule, because that is where the caller is known.

func Name

method on Ledger
1func (l *Ledger) Name() string
source

func Now

method on Ledger
1func (l *Ledger) Now() int64
source

Now is this ledger's wall clock, forwarded to the governor through the Electorate interface. It is NOT used to quantise anything here: epochs are height-quantised by design (see Clock), and this exists only so a deadline the governor publishes can be a date.

func PastTotal

method on Ledger
1func (l *Ledger) PastTotal(at uint32) int64
source

PastTotal is every unit in existence during a sealed epoch, which a quorum is a fraction of.

Because delegation defaults to self, everyone's PastVotes sums to exactly this. OZ cannot say so: getPastTotalSupply counts all supply while getPastVotes counts only DELEGATED supply, so the denominator exceeds the achievable numerator and quorum is unreachable where few have delegated.

func PastVotes

method on Ledger
1func (l *Ledger) PastVotes(who address, at uint32) int64
source

PastVotes is voting power throughout a SEALED epoch. Refusing the current one is the anti-flash-loan property as an invariant rather than a convention: a transaction runs inside one block, a block inside one epoch, and this will not answer for the epoch it is in.

func SetID

method on Ledger
1func (l *Ledger) SetID(id string)
source

SetID names this token the way an indexer will see it, usually "gno.land/r/you/realm.SYMBOL". Called once, at construction time, by the realm that knows its own path — a /p/ package cannot ask.

func Symbol

method on Ledger
1func (l *Ledger) Symbol() string
source

func Transfer

method on Ledger
1func (l *Ledger) Transfer(from, to address, amount int64)
source

Transfer moves `from`'s own tokens.

func TransferFrom

method on Ledger
1func (l *Ledger) TransferFrom(spender, from, to address, amount int64)
source

TransferFrom spends an allowance.

func VotesOf

method on Ledger
1func (l *Ledger) VotesOf(who address) int64
source

VotesOf is voting power right now — the sum of every balance delegated here, including the holder's own unless they delegated it away.

func WalkAccounts

method on Ledger
1func (l *Ledger) WalkAccounts(fn func(who address, bal int64) bool)
source

WalkAccounts hands every account's address and balance to fn, ascending by address, and stops early when fn returns true.

NO NEW STATE. The accounts tree is the ledger's own index and has always held exactly this; what was missing was a way to read it as a set rather than one address at a time. A consumer wanting a holder ranking would otherwise have had to maintain a second index alongside every mint, burn and transfer — a duplicate that can only ever drift out of step with the first.

ZERO BALANCES ARE HANDED OVER TOO, deliberately. An account can reach zero and stay in the tree — Remove would drop the delegate and the vote history with it, which is why the balance path never removes — so filtering here would quietly hide accounts that still carry voting state. The caller knows which question it is asking; this one answers "what does the ledger hold".

func WalkSupply

method on Ledger
1func (l *Ledger) WalkSupply(fn func(e uint32, v int64) bool)
source

WalkSupply visits the supply's CHANGE points, newest first, and stops early when fn returns true. An epoch in which supply did not move is not a point: a reader forward-fills between them.

A WALK, not a per-epoch read. PastTotal answers one epoch and is the wrong shape for drawing a line — a chart over a year of hourly epochs would be 8,760 lookups to find the handful of epochs that actually moved. The archive already stores only the movements, so this hands them over directly and the cost is the number of mints, not the age of the court.

Unsealed on purpose, unlike PastTotal: the newest point may be the running epoch, which is exactly the value a live chart wants at its right edge. It may still coalesce before the epoch seals, so it is a reading and not a record.

Imports 7

Source Files 2