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.