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 path | gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/upgrade_registry |
|---|---|
| Namespace | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Deployed at height | 357,964 |
| Deploy transaction | bd67e9398babb6a9ef0fa61f4e68152739d08229bd3d385203f663234bb87517 look it up on the RPC |
| Deployer | g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39 |
| Gas used | 53,913,330 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | gnomod.toml upgrade_registry.gno upgrade_registry_test.gno |
| Deployed bytes | upgrade_registry.gno — 19,807 bytes |
| sha256 | b0f36bcab4b44b56974c43b9ebb49578e0a62d94cf9477845cf688f0a84499c6 |
| Realm address | g1duc2x3t53pe6w9kaq8mtzcn9xck9dpgqg7udtc 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 256Expected: 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
chainchain/runtime/unsafestrconvstrings
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 field | Type | Doc |
|---|---|---|
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 |
Functions
AcceptOwnership
func AcceptOwnership(cur realm, contractAddr address) string
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAcceptOwnership(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 asDeprecate(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 asRegister(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 asTransferOwnership(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| Imports | chain, chain/runtime/unsafe, strconv, strings |
|---|---|
| First-party dependencies | none |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- source.divergence is null and means what C6 means by it: the committed source in this repository equals the bytes this chain stores. It is NOT a statement about the pearl-1 twin. This package DOES diverge from pearl-1 deliberately, and that axis lives in mainnet/DIVERGENCE.json, not in this field -- the two uses of the word 'divergence' collide, and this entry is the first place the collision is load-bearing (docs/MAINNET_READINESS.md Amendment AB, MINOR-3). Reading source.divergence null as 'identical to pearl' would be exactly wrong.
- The declared divergence waives two of namespace_tree.py's nine proof obligations (byte equality under reverse substitution, and byte-length preservation) for the two .gno files, and only those two obligations. It replaces them with base_sha256/base_bytes, result_sha256/result_bytes, diff_sha256 and a pin on the exported symbol set. Those three digests are a tamper-evidence seal over a reviewed artifact, not a bound on what the artifact may contain: a declared divergence may change anything inside a function body and only reading `python3 tools/namespace_tree.py diff` will tell you it did.
- No state-changing call was made against this realm on gnoland-1. The live evidence is byte custody plus a successful vm/qrender showing the new M2 disclosure on an empty registry; every state transition rests on the committed test suite (28/28 under the mainnet pin on deploy day), the external event assertions (tools/test_upgrade_registry_events.sh, 12/12), and the round-4 audit plus its challenge passes -- not on mainnet behaviour ([an unpublished record], Live verification).
- The added events are asserted by an external Python harness, not by the .gno suite: this toolchain's testing stdlib has no events accessor, so no in-language test can observe an emit. That coverage is therefore a different tool's output and is only as good as tools/test_upgrade_registry_events.sh, whose own control run counted 575 events over 19 checks.
- storage_bytes and storage_deposit are null because gnoland-1 emits no /tm.StorageDepositEvent ON ADDPKG -- a scope that is load-bearing, because it DOES emit one on MsgCall (measured 2026-10-17, AL62); see the chain's storage_deposit_events field. The deposit actually charged was 1,763,900 ugnot (17,639 stored bytes), measured by differencing auth/accounts across the deploy heights and recorded in [an unpublished record], Cost; it is not chain-attested per package by this manifest.
- The deployed file set includes the _test.gno file. The pearl-1 deployment of the same realm carried only the source and gnomod.toml, so the mainnet package is three files where pearl's is two -- the test file is stored on chain and paid for in the deposit. It declares no crossing function and so adds no callable surface, but it is part of the immutable package.
- Names are self-asserted labels: the realm verifies the registering ADDRESS and checks nothing about the NAME, which is neither unique nor meaningful. Two entries may carry the same name. The deployed Render now says so unconditionally -- that disclosure IS the M2 remediation, and it is a disclosure rather than a fix because adding name uniqueness was explicitly out of scope.
- No global entry cap, and Render shows a bounded window. The cap is omitted for the saturation reason alone; per-address uniqueness is not a spam control because addresses are cheap. Entries past the window remain queryable by address but are not rendered. Accepted and unmitigated.
- Zero first-party and zero chain dependencies: the deploy-axis closure verdict compares nothing, so its digest e3b0c442... means 'no chain dependencies at all', not 'a closure was verified'. The --include-tests axis compared 4 dependencies and was EQUIVALENT. Residual in both cases: the stdlib packages execute from the node binary and are not covered.
Source
| Source file | mainnet/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 bytes | yes — byte-identical |
| Upstream repository | https://github.com/SillyZir/upgrade_registry |
| Deployed from commit | 2d5a74870b08d70ff8e56e6e1efbace1803dd3bd |