PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

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/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upgrade_registry

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
upgrade_registry
Namespace
g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Files
3 (gnomod.toml)
Exported functions
9
Module
gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upgrade_registry
gno
0.9

Files (3)

  • gnomod.tomltoml
  • upgrade_registry.gnogno
  • upgrade_registry_test.gnogno
upgrade_registry.gnogno
1package upgrade_registry23import (4	"chain"5	"chain/runtime/unsafe"6	"strconv"7	"strings"8)910// ContractEntry represents a registered contract and its upgrade status.11//12// An entry's Address is ALWAYS the address that called Register — a13// contract can only register itself. That call is the proof of control:14// nobody can claim an address they don't command, so a Deprecate15// redirect on an entry always originates from whoever genuinely16// controlled the contract. Entries are permanent by design: migration17// history is what consumers rely on, so it must not be erasable (and a18// delete would reopen re-registration squatting).19//20// Ownership is two-step (re-audit 2026-09-02): Register/Transfer only21// NOMINATE an owner; the nominee must AcceptOwnership. Until then the22// entry is owned by the contract itself, so nobody's address can be23// attached as "owner" without their consent.24type ContractEntry struct {25	Address      address26	Owner        address // current manager (accepted)27	PendingOwner address // nominated, not yet accepted; empty if none28	Name         string29	Deprecated   bool30	Successor    address // address of the upgraded contract, empty if current31}3233const (34	MaxNameLen = 643536	// MaxChainLen bounds two WALKS, and is NOT a bound on how long a37	// migration chain can become (round-4 audit: the previous comment38	// named only the first of the two uses, which read as though chain39	// length itself were capped at 50):40	//41	//   1. GetMigrationChain's traversal and output — a chain longer than42	//      this is explicitly marked truncated rather than presented as43	//      ending early.44	//   2. Deprecate's cycle-verification walk, which refuses rather than45	//      walking unbounded.46	//47	// Neither bounds TOTAL chain length, because Deprecate's walk runs48	// FORWARD from the new successor and never sees the chain BEHIND the49	// predecessor. Its cost is proportional to the chain AHEAD of the50	// successor, which is:51	//52	//   - zero hops when the successor is terminal — the ordinary53	//     tail-append. The walk breaks immediately, so tail-appending grows54	//     a chain without limit. The realm's own55	//     TestMigrationChain_MarksTruncation builds MaxChainLen+5 links this56	//     way and is expected to succeed.57	//   - O(chain ahead) when the successor is ITSELF already deprecated — a58	//     head-append. Deprecate does NOT reject an already-deprecated59	//     successor (it validates only existence and ownership), so this is60	//     a permitted call, and past MaxChainLen hops it refuses. See61	//     TestDeprecate_BoundedWalkRefusal.62	//63	// This is why GetLatest documents an UNBOUNDED cost (see its COST64	// WARNING) — that warning is accurate, not conservative.65	//66	// Extending a chain requires owning both ends of THE ONE NEW LINK at67	// call time (see Deprecate, which checks exactly that and never68	// re-verifies earlier links). That is a per-call consent check, not an69	// invariant of the finished chain: an owner who transfers away only the70	// TAIL entry hands the recipient the ability to extend it, and so to71	// move GetLatest for every entry behind it, without that recipient ever72	// having owned the root. Acquiring the tail still takes the two-step73	// nominate/accept handoff from its owner, so no chain can be lengthened74	// without some consenting owner along it.75	MaxChainLen = 507677	// Render bounds (re-audit): rendering is paginated and the per-row78	// "latest" annotation walks a bounded number of hops, so an79	// attacker-built long chain cannot blow up the realm page.80	MaxRenderEntries = 10081	RenderLatestHops = 1082)8384// NOTE (re-audit): there is deliberately NO global entry cap, and the85// reason is the second clause below, NOT the first.86//87// Entries are one-per-address by construction (self-registration), but88// round-4 audit correction: that does NOT make spam expensive. Addresses89// are cheap — anyone may deploy unlimited realms under their own90// personal-address namespace, and each deployed path derives a distinct91// address — so entry count is effectively unbounded regardless. The92// earlier wording presented per-address uniqueness as a spam control; it93// is not one.94//95// The cap is omitted because a hard cap, combined with the96// registered-successor rule, would let an attacker saturate the registry97// and permanently disable deprecation for every legitimate entry. That98// failure is permanent and global; unbounded growth is neither. Growth99// is also paid for by the registrant, not by this realm's operator.100//101// Consequence accepted here and NOT mitigated: Render shows the most102// recent MaxRenderEntries entries, so a registrant who floods that many103// entries pushes older ones off this realm's rendered page permanently.104// The data remains queryable by address via GetInfo/GetMigrationChain.105106var (107	entries        map[address]*ContractEntry108	entryAddrs     []address // insertion-ordered for deterministic Render109	ownerContracts map[address][]address110)111112func init() {113	entries = make(map[address]*ContractEntry)114	entryAddrs = []address{}115	ownerContracts = make(map[address][]address)116}117118// rejectStraySend aborts when coins are attached to a call (audit Y2):119// this realm handles no funds and holds no banker, so an attached send120// would strand on the realm address forever. Aborting reverts the121// transfer back to the sender. Fails open for realm-routed calls, whose122// attached send lands on the intermediary realm, never here.123func rejectStraySend(cur realm) {124	if cur.Previous().IsUserCall() && len(unsafe.OriginSend()) > 0 {125		panic("this realm does not accept coins")126	}127}128129// isValidName restricts names to lowercase alphanumeric with130// underscores. Names appear in rendered markdown and query output, so131// no delimiter or markdown character may enter one.132func isValidName(name string) bool {133	if name == "" || len(name) > MaxNameLen {134		return false135	}136	for _, c := range name {137		if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') {138			return false139		}140	}141	return true142}143144func ownerIndexRemove(owner, contractAddr address) {145	old := ownerContracts[owner]146	for i, addr := range old {147		if addr == contractAddr {148			ownerContracts[owner] = append(old[:i], old[i+1:]...)149			break150		}151	}152	if len(ownerContracts[owner]) == 0 {153		delete(ownerContracts, owner)154	}155}156157// ---------- write operations ----------158159// Register adds the CALLING contract to the registry — the entry's160// address is the caller's own address, which is the proof of control.161// owner NOMINATES a manager (team EOA or governance realm); it holds no162// power until it calls AcceptOwnership (nobody can be made an owner163// without consent — re-audit). Pass "" to manage from the contract164// itself — but note (audit Y4): entries are permanent, so an entry165// managed by a contract that has no code path for calling this166// registry again is FROZEN as active forever: never deprecatable,167// never transferable. Realms registering from init() should nominate168// an EOA or governance manager instead.169func Register(cur realm, name string, owner address) string {170	rejectStraySend(cur)171	if !isValidName(name) {172		panic("name must be 1-" + strconv.Itoa(MaxNameLen) +173			" chars, lowercase alphanumeric with underscores only")174	}175	contractAddr := cur.Previous().Address()176	if _, exists := entries[contractAddr]; exists {177		panic("contract already registered: " + string(contractAddr))178	}179	pending := address("")180	if owner != "" && owner != contractAddr {181		if !owner.IsValid() {182			panic("invalid owner address: " + string(owner))183		}184		pending = owner185	}186187	entries[contractAddr] = &ContractEntry{188		Address:      contractAddr,189		Owner:        contractAddr,190		PendingOwner: pending,191		Name:         name,192	}193	entryAddrs = append(entryAddrs, contractAddr)194	ownerContracts[contractAddr] = append(ownerContracts[contractAddr], contractAddr)195	// `owner` is the EFFECTIVE owner after this event, which at registration196	// is the contract itself; `pendingOwner` is the nomination, empty when197	// the entry is self-managed. Emitting both is what makes the key `owner`198	// mean the same thing in every event this realm emits (round-4 audit Y2:199	// omitting it here left `owner` readable as "the authorizing actor" in200	// some events and "the new owner" in others).201	chain.Emit("Register",202		"contract", string(contractAddr),203		"name", name,204		"owner", string(contractAddr),205		"pendingOwner", string(pending),206	)207	return "registered " + name + " at " + string(contractAddr)208}209210// AcceptOwnership completes a nominated ownership: only the pending211// owner can accept, and only acceptance moves the entry (and the212// ownerContracts index) to them.213func AcceptOwnership(cur realm, contractAddr address) string {214	rejectStraySend(cur)215	entry := mustGet(contractAddr)216	c := cur.Previous().Address()217	if entry.PendingOwner == "" || c != entry.PendingOwner {218		panic("caller is not the pending owner of " + string(contractAddr))219	}220	previousOwner := entry.Owner221	ownerIndexRemove(entry.Owner, contractAddr)222	ownerContracts[c] = append(ownerContracts[c], contractAddr)223	entry.Owner = c224	entry.PendingOwner = ""225	chain.Emit("OwnershipAccepted",226		"contract", string(contractAddr),227		"owner", string(c),228		"previousOwner", string(previousOwner),229	)230	return "ownership accepted by " + string(c)231}232233// TransferOwnership NOMINATES a new owner for a registry entry; the234// nominee must AcceptOwnership to take control (two-step — re-audit).235// Passing "" clears a pending nomination.236func TransferOwnership(cur realm, contractAddr, newOwner address) string {237	rejectStraySend(cur)238	entry := mustGet(contractAddr)239	if entry.Owner != cur.Previous().Address() {240		panic("only the owner can transfer ownership")241	}242	if newOwner == "" {243		// Emit only when a nomination actually existed (round-4 audit L2:244		// clearing an entry that has no pending owner is a no-op, and an245		// event for it is indistinguishable from a real clearing to an246		// indexer). The return value is unconditional either way — this247		// changes what is OBSERVED, not what happens.248		cleared := entry.PendingOwner249		entry.PendingOwner = ""250		if cleared != "" {251			chain.Emit("OwnershipNominationCleared",252				"contract", string(contractAddr),253				"owner", string(entry.Owner),254				"clearedNominee", string(cleared),255			)256		}257		return "pending ownership nomination cleared"258	}259	if !newOwner.IsValid() {260		panic("invalid new owner address: " + string(newOwner))261	}262	entry.PendingOwner = newOwner263	chain.Emit("OwnershipNominated",264		"contract", string(contractAddr),265		"owner", string(entry.Owner),266		"pendingOwner", string(newOwner),267	)268	return "ownership nominated to " + string(newOwner) + " (pending acceptance)"269}270271// Deprecate marks a contract as deprecated and points to its successor.272// Only the entry's owner can deprecate. The successor must itself be a273// REGISTERED entry — registration is self-proving, so a successor can274// never be a dangling pointer or an address squatted by a third party —275// and the successor entry must be OWNED BY THE CALLER (re-audit: without276// consent, an attacker could chain their entry INTO a legitimate277// contract, forging "official predecessor" provenance). Consent is278// checked AT CALL TIME: transferring the successor entry away later279// does not unlink an existing chain — the attestation is that both280// ends shared an owner when the deprecation was recorded.281func Deprecate(cur realm, contractAddr, successorAddr address) string {282	rejectStraySend(cur)283	entry := mustGet(contractAddr)284	if entry.Owner != cur.Previous().Address() {285		panic("only the owner can deprecate this contract")286	}287	if entry.Deprecated {288		panic("contract is already deprecated")289	}290	if successorAddr == contractAddr {291		panic("successor cannot be the contract itself")292	}293	succ, exists := entries[successorAddr]294	if !exists {295		panic("successor is not a registered contract: " + string(successorAddr))296	}297	if succ.Owner != cur.Previous().Address() {298		panic("successor entry is not owned by the caller: " + string(successorAddr))299	}300301	// Prevent circular chains: walk from successor and ensure we don't302	// loop back to contractAddr. Successors are immutable once set, so303	// a check over the existing chain is complete.304	visited := make(map[address]bool)305	visited[contractAddr] = true306	walk := successorAddr307	for hops := 0; ; hops++ {308		if hops > MaxChainLen {309			// bounded-walk discipline (round-3 audit): a successor chain310			// this long is pathological; refuse rather than walk unbounded311			panic("successor chain too long to verify; cannot deprecate")312		}313		if visited[walk] {314			panic("deprecating " + string(contractAddr) + " to " +315				string(successorAddr) + " would create a circular migration chain")316		}317		e, exists := entries[walk]318		if !exists || !e.Deprecated || e.Successor == "" {319			break320		}321		visited[walk] = true322		walk = e.Successor323	}324325	entry.Deprecated = true326	entry.Successor = successorAddr327	chain.Emit("Deprecate",328		"contract", string(contractAddr),329		"name", entry.Name,330		"successor", string(successorAddr),331	)332	return "deprecated " + entry.Name + ", successor: " + string(successorAddr)333}334335// ---------- read-only queries ----------336337// GetLatest follows the migration chain from a contract address and338// returns the latest non-deprecated address. Safe against circular refs.339//340// COST WARNING (audit Y3): unlike the write-path walks, this read is341// deliberately UNBOUNDED in hops so it always resolves the true342// endpoint. Chain length is attacker-buildable, so an on-chain343// integrator resolving an UNTRUSTED address inside its own transaction344// must treat the gas cost as O(chain length) — wrap it, bound the345// input set, or resolve off-chain via query.346func GetLatest(contractAddr address) address {347	visited := make(map[address]bool)348	current := contractAddr349350	for {351		entry, exists := entries[current]352		if !exists {353			return current354		}355		if !entry.Deprecated || entry.Successor == "" {356			return current357		}358		if visited[current] {359			return current // break on circular360		}361		visited[current] = true362		current = entry.Successor363	}364}365366// GetMigrationChain returns the full upgrade path starting from the367// given address, at most MaxChainLen hops. A chain longer than the368// bound is explicitly marked as truncated (re-audit: silent truncation369// presented a mid-chain node as the endpoint).370func GetMigrationChain(contractAddr address) string {371	visited := make(map[address]bool)372	current := contractAddr373	var parts []string374375	for {376		if len(parts) >= MaxChainLen {377			parts = append(parts, "... (truncated at "+strconv.Itoa(MaxChainLen)+378				" hops; use GetLatest for the endpoint)")379			break380		}381		entry, exists := entries[current]382		if !exists {383			if len(parts) == 0 {384				return "not found: " + string(contractAddr)385			}386			parts = append(parts, string(current)+" (unregistered)")387			break388		}389		if visited[current] {390			parts = append(parts, string(current)+" (circular)")391			break392		}393		visited[current] = true394395		tag := ""396		if entry.Deprecated {397			tag = " [deprecated]"398		}399		parts = append(parts, string(entry.Address)+" ("+entry.Name+")"+tag)400401		if !entry.Deprecated || entry.Successor == "" {402			break403		}404		current = entry.Successor405	}406407	return strings.Join(parts, " -> ")408}409410// GetInfo returns a one-line summary for a contract.411func GetInfo(contractAddr address) string {412	entry := mustGet(contractAddr)413	status := "active"414	if entry.Deprecated {415		status = "deprecated -> " + string(entry.Successor)416	}417	return entry.Name + " | owner: " + string(entry.Owner) + " | " + status418}419420// GetOwnerContracts returns all contract addresses whose ACCEPTED owner421// is the given address (nominations don't count until accepted).422func GetOwnerContracts(owner address) string {423	addrs, exists := ownerContracts[owner]424	if !exists || len(addrs) == 0 {425		return "none"426	}427	parts := make([]string, len(addrs))428	for i, a := range addrs {429		parts[i] = string(a)430	}431	return strings.Join(parts, ", ")432}433434// ---------- render ----------435436// renderLatest walks at most RenderLatestHops hops for the per-row437// annotation; longer chains are marked rather than walked (render gas438// bound — re-audit).439func renderLatest(contractAddr address) (address, bool) {440	current := contractAddr441	for i := 0; i < RenderLatestHops; i++ {442		entry, exists := entries[current]443		if !exists || !entry.Deprecated || entry.Successor == "" {444			return current, true445		}446		current = entry.Successor447	}448	// round-3 audit off-by-one: the node reached ON the budget boundary449	// may itself be terminal — check before declaring the walk incomplete450	entry, exists := entries[current]451	if !exists || !entry.Deprecated || entry.Successor == "" {452		return current, true453	}454	return current, false455}456457// Render returns a markdown overview of the most recent entries.458// Never panics.459//460// The rendered page carries an explicit scope disclosure (round-4 audit).461// What this registry proves is narrow and easy to over-read: an entry's462// ADDRESS is self-registered and therefore verified, but its NAME is an463// arbitrary self-asserted label that is neither unique nor checked464// against anything. Two entries may carry the same name, and a name may465// impersonate a well-known project. A reader who keys on the bold name466// rather than the address is trusting something this realm does not467// attest. Stating that in the output is the only available mitigation:468// the entries are immutable, and name uniqueness is deliberately NOT469// enforced because first-come uniqueness on a permissionless immutable470// registry would let an attacker burn every valuable name permanently.471func Render(path string) string {472	disclosure := "> [!NOTE]\n" +473		"> Addresses are self-registered and therefore verified: an entry exists\n" +474		"> only because that address itself called `Register`. **Names are not.**\n" +475		"> A name is an arbitrary self-asserted label — it is NOT unique, NOT\n" +476		"> verified, and may impersonate another project. Identify contracts by\n" +477		"> address, never by name.\n\n"478479	if entries == nil || len(entries) == 0 {480		return "# Upgrade Registry\n\n" + disclosure + "No contracts registered.\n"481	}482483	var b strings.Builder484	b.WriteString("# Upgrade Registry\n\n")485	b.WriteString(disclosure)486	b.WriteString("**Registered contracts:** " + strconv.Itoa(len(entries)) + "\n\n")487488	show := entryAddrs489	if len(show) > MaxRenderEntries {490		b.WriteString("_Showing the most recent " + strconv.Itoa(MaxRenderEntries) +491			" entries of " + strconv.Itoa(len(show)) + ". Older entries are not " +492			"listed here and remain queryable by address via GetInfo and " +493			"GetMigrationChain._\n\n")494		show = show[len(show)-MaxRenderEntries:]495	}496497	var active []string498	var deprecated []string499500	for _, addr := range show {501		entry := entries[addr]502		if entry == nil {503			continue504		}505		line := "- **" + entry.Name + "** (`" + string(addr) + "`) — owner: `" + string(entry.Owner) + "`"506		if entry.Deprecated {507			line += " — **DEPRECATED** -> `" + string(entry.Successor) + "`"508			latest, complete := renderLatest(addr)509			if !complete {510				line += " (long chain; use GetLatest)"511			} else if latest != entry.Successor {512				line += " (latest: `" + string(latest) + "`)"513			}514			deprecated = append(deprecated, line)515		} else {516			active = append(active, line)517		}518	}519520	if len(active) > 0 {521		b.WriteString("## Active\n\n")522		for _, l := range active {523			b.WriteString(l + "\n")524		}525		b.WriteString("\n")526	}527528	if len(deprecated) > 0 {529		b.WriteString("## Deprecated\n\n")530		for _, l := range deprecated {531			b.WriteString(l + "\n")532		}533		b.WriteString("\n")534	}535536	return b.String()537}538539// ---------- helpers ----------540541func mustGet(addr address) *ContractEntry {542	entry, ok := entries[addr]543	if !ok {544		panic("contract not found: " + string(addr))545	}546	return entry547}548

Functions

  • AcceptOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, contractAddr string) string

  • Deprecate(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, contractAddr string, successorAddr string) string

  • GetInfo(contractAddr string) string

  • GetLatest(contractAddr string) string

  • GetMigrationChain(contractAddr string) string

  • GetOwnerContracts(owner string) string

  • Register(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, owner string) string

  • Render(path string) string

  • TransferOwnership(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, contractAddr string, newOwner string) string

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

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.