Cosmic Bull

upgrade_registry

gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upgrade_registry on gno.land mainnet

realmpipeline-applicationupgrade-trackinglive on mainnet

Record which version of a contract is current.

A contract registers itself and marks its own successor, so callers can follow the chain to the version in use today.

In detail curated

Records successive versions of a logical component and which is current. GitHub-pipeline application #3. This mainnet artifact is NOT a pure re-namespacing of the pearl-1 twin: it carries an authorized, declared divergence (events on the four write paths, an accurate Render scope disclosure, corrected comments) recorded in mainnet/DIVERGENCE.json and verified as FAITHFUL WITH DECLARED DIVERGENCE by tools/namespace_tree.py.

Use it

A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.

Change its state

Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 4 crossing functions, with the argument names filled in.

The gno.land mainnet builder spends real GNOT. Start with the read above, and size -gas-fee from a simulation rather than from any example.

Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.

Import it from another realm

import "gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upgrade_registry"

The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.

Deployments

gno.land mainnet gnoland-1

RenderedSourceCall builderState

chain-attested
Import pathgno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upgrade_registry
Namespaceg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Deployed at height357,964
Deploy transactionbd67e9398babb6a9ef0fa61f4e68152739d08229bd3d385203f663234bb87517 look it up on the RPC
Deployerg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Gas used53,913,330
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml upgrade_registry.gno upgrade_registry_test.gno
Deployed bytesupgrade_registry.gno — 19,807 bytes
sha256b0f36bcab4b44b56974c43b9ebb49578e0a62d94cf9477845cf688f0a84499c6
Realm addressg1duc2x3t53pe6w9kaq8mtzcn9xck9dpgqg7udtc derived, never confirmed against the realm

Do not take the hash above on trust. $download returns the bytes gnoland-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upgrade_registry$download&file=upgrade_registry.gno' | shasum -a 256

Expected: b0f36bcab4b44b56974c43b9ebb49578e0a62d94cf9477845cf688f0a84499c6 — 19,807 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 8 exported functions, 1 type.

Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:

AcceptOwnershipDeprecateGetInfoGetMigrationChainGetOwnerContractsRegisterRenderTransferOwnership

Overview

This package carries no package doc comment on chain, so there is nothing for vm/qdoc to return and gnoweb's $help Overview is empty. Deployed bytes are immutable, so this cannot be repaired in place — see [an unpublished record] §2.4.

Imports

Constants and variables

const (
	MaxNameLen = 64

	// MaxChainLen bounds two WALKS, and is NOT a bound on how long a
	// migration chain can become (round-4 audit: the previous comment
	// named only the first of the two uses, which read as though chain
	// length itself were capped at 50):
	//
	//   1. GetMigrationChain's traversal and output — a chain longer than
	//      this is explicitly marked truncated rather than presented as
	//      ending early.
	//   2. Deprecate's cycle-verification walk, which refuses rather than
	//      walking unbounded.
	//
	// Neither bounds TOTAL chain length, because Deprecate's walk runs
	// FORWARD from the new successor and never sees the chain BEHIND the
	// predecessor. Its cost is proportional to the chain AHEAD of the
	// successor, which is:
	//
	//   - zero hops when the successor is terminal — the ordinary
	//     tail-append. The walk breaks immediately, so tail-appending grows
	//     a chain without limit. The realm's own
	//     TestMigrationChain_MarksTruncation builds MaxChainLen+5 links this
	//     way and is expected to succeed.
	//   - O(chain ahead) when the successor is ITSELF already deprecated — a
	//     head-append. Deprecate does NOT reject an already-deprecated
	//     successor (it validates only existence and ownership), so this is
	//     a permitted call, and past MaxChainLen hops it refuses. See
	//     TestDeprecate_BoundedWalkRefusal.
	//
	// This is why GetLatest documents an UNBOUNDED cost (see its COST
	// WARNING) — that warning is accurate, not conservative.
	//
	// Extending a chain requires owning both ends of THE ONE NEW LINK at
	// call time (see Deprecate, which checks exactly that and never
	// re-verifies earlier links). That is a per-call consent check, not an
	// invariant of the finished chain: an owner who transfers away only the
	// TAIL entry hands the recipient the ability to extend it, and so to
	// move GetLatest for every entry behind it, without that recipient ever
	// having owned the root. Acquiring the tail still takes the two-step
	// nominate/accept handoff from its owner, so no chain can be lengthened
	// without some consenting owner along it.
	MaxChainLen = 50

	// Render bounds (re-audit): rendering is paginated and the per-row
	// "latest" annotation walks a bounded number of hops, so an
	// attacker-built long chain cannot blow up the realm page.
	MaxRenderEntries = 100
	RenderLatestHops = 10
)

Types

type ContractEntry

type ContractEntry struct {
	Address      address
	Owner        address // current manager (accepted)
	PendingOwner address // nominated, not yet accepted; empty if none
	Name         string
	Deprecated   bool
	Successor    address // address of the upgraded contract, empty if current
}

ContractEntry represents a registered contract and its upgrade status.

An entry's Address is ALWAYS the address that called Register — a contract can only register itself. That call is the proof of control: nobody can claim an address they don't command, so a Deprecate redirect on an entry always originates from whoever genuinely controlled the contract. Entries are permanent by design: migration history is what consumers rely on, so it must not be erasable (and a delete would reopen re-registration squatting).

Ownership is two-step (re-audit 2026-09-02): Register/Transfer only NOMINATE an owner; the nominee must AcceptOwnership. Until then the entry is owned by the contract itself, so nobody's address can be attached as "owner" without their consent.

Exported fieldTypeDoc
Addressaddress
Owneraddresscurrent manager (accepted)
PendingOwneraddressnominated, not yet accepted; empty if none
Namestring
Deprecatedbool
Successoraddressaddress of the upgraded contract, empty if current

Functions

AcceptOwnership

func AcceptOwnership(cur realm, contractAddr address) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as AcceptOwnership(cross(cur), ...).

AcceptOwnership completes a nominated ownership: only the pending owner can accept, and only acceptance moves the entry (and the ownerContracts index) to them.

Deprecate

func Deprecate(cur realm, contractAddr, successorAddr address) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as Deprecate(cross(cur), ...).

Deprecate marks a contract as deprecated and points to its successor. Only the entry's owner can deprecate. The successor must itself be a REGISTERED entry — registration is self-proving, so a successor can never be a dangling pointer or an address squatted by a third party — and the successor entry must be OWNED BY THE CALLER (re-audit: without consent, an attacker could chain their entry INTO a legitimate contract, forging "official predecessor" provenance). Consent is checked AT CALL TIME: transferring the successor entry away later does not unlink an existing chain — the attestation is that both ends shared an owner when the deprecation was recorded.

GetInfo

func GetInfo(contractAddr address) string

GetInfo returns a one-line summary for a contract.

GetMigrationChain

func GetMigrationChain(contractAddr address) string

GetMigrationChain returns the full upgrade path starting from the given address, at most MaxChainLen hops. A chain longer than the bound is explicitly marked as truncated (re-audit: silent truncation presented a mid-chain node as the endpoint).

GetOwnerContracts

func GetOwnerContracts(owner address) string

GetOwnerContracts returns all contract addresses whose ACCEPTED owner is the given address (nominations don't count until accepted).

Register

func Register(cur realm, name string, owner address) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as Register(cross(cur), ...).

Register adds the CALLING contract to the registry — the entry's address is the caller's own address, which is the proof of control. owner NOMINATES a manager (team EOA or governance realm); it holds no power until it calls AcceptOwnership (nobody can be made an owner without consent — re-audit). Pass "" to manage from the contract itself — but note (audit Y4): entries are permanent, so an entry managed by a contract that has no code path for calling this registry again is FROZEN as active forever: never deprecatable, never transferable. Realms registering from init() should nominate an EOA or governance manager instead.

Render

func Render(path string) string

Render returns a markdown overview of the most recent entries. Never panics.

The rendered page carries an explicit scope disclosure (round-4 audit). What this registry proves is narrow and easy to over-read: an entry's ADDRESS is self-registered and therefore verified, but its NAME is an arbitrary self-asserted label that is neither unique nor checked against anything. Two entries may carry the same name, and a name may impersonate a well-known project. A reader who keys on the bold name rather than the address is trusting something this realm does not attest. Stating that in the output is the only available mitigation: the entries are immutable, and name uniqueness is deliberately NOT enforced because first-come uniqueness on a permissionless immutable registry would let an attacker burn every valuable name permanently.

TransferOwnership

func TransferOwnership(cur realm, contractAddr, newOwner address) string

Crossing function. Callable from a transaction via MsgCall, and from another realm as TransferOwnership(cross(cur), ...).

TransferOwnership NOMINATES a new owner for a registry entry; the nominee must AcceptOwnership to take control (two-step — re-audit). Passing "" clears a pending nomination.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importschain, chain/runtime/unsafe, strconv, strings
First-party dependenciesnone

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source filemainnet/r/upgrade_registry/upgrade_registry.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical
Upstream repositoryhttps://github.com/SillyZir/upgrade_registry
Deployed from commit2d5a74870b08d70ff8e56e6e1efbace1803dd3bd