1package upgrade_registry23import (4 "chain"5 "strconv"6 "strings"7 "testing"89 "gno.land/p/nt/testutils/v0"10 "gno.land/p/nt/uassert/v0"11)1213// In production the registrant is usually a realm; user addresses give14// the same self-registration semantics in tests (Register binds the15// entry to the CALLER's address either way).16var (17 v1 = testutils.TestAddress("contract_v1")18 v2 = testutils.TestAddress("contract_v2")19 v3 = testutils.TestAddress("contract_v3")20 teamEOA = testutils.TestAddress("team")21 attacker = testutils.TestAddress("attacker")22)2324// resetState resets all package-level state between tests.25func resetState() {26 entries = make(map[address]*ContractEntry)27 entryAddrs = []address{}28 ownerContracts = make(map[address][]address)29}3031// register self-registers addr under name; when owner is non-empty the32// nominee accepts, completing the two-step handover.33func register(cur realm, addr address, name string, owner address) {34 testing.SetRealm(testing.NewUserRealm(addr))35 Register(cross(cur), name, owner)36 if owner != "" && owner != addr {37 testing.SetRealm(testing.NewUserRealm(owner))38 AcceptOwnership(cross(cur), addr)39 }40}4142// ---------- Register (proof of control) ----------4344func TestRegister_BindsToCallerAddress(cur realm, t *testing.T) {45 resetState()46 register(cur, v1, "vault", teamEOA)4748 e := entries[v1]49 uassert.Equal(t, string(v1), string(e.Address))50 uassert.Equal(t, string(teamEOA), string(e.Owner))51 uassert.Equal(t, "vault", e.Name)5253 // the audit's P1: there is no way to register an address you do not54 // control — the entry address is ALWAYS the caller's own. An55 // attacker registering gets an entry for themselves, not the victim.56 testing.SetRealm(testing.NewUserRealm(attacker))57 Register(cross(cur), "fake_vault", "")58 uassert.Equal(t, string(attacker), string(entries[attacker].Address))59 _, hijacked := entries[v2]60 uassert.False(t, hijacked, "no entry may exist for an address that never registered itself")61}6263func TestRegister_OwnerDefaultsToContract(cur realm, t *testing.T) {64 resetState()65 register(cur, v1, "vault", "")6667 uassert.Equal(t, string(v1), string(entries[v1].Owner))68}6970func TestRegister_DuplicateRejected(cur realm, t *testing.T) {71 resetState()72 register(cur, v1, "vault", "")7374 testing.SetRealm(testing.NewUserRealm(v1))75 uassert.AbortsWithMessage(t, cur,76 "contract already registered: "+string(v1),77 func() { Register(cross(cur), "vault_again", "") },78 )79}8081func TestRegister_NameValidation(cur realm, t *testing.T) {82 resetState()83 testing.SetRealm(testing.NewUserRealm(v1))8485 for _, bad := range []string{"", "Vault", "has space", "tick`name", strings.Repeat("a", MaxNameLen+1)} {86 uassert.AbortsContains(t, cur,87 "name must be",88 func() { Register(cross(cur), bad, "") },89 )90 }91}9293func TestRegister_InvalidOwnerRejected(cur realm, t *testing.T) {94 resetState()95 testing.SetRealm(testing.NewUserRealm(v1))9697 uassert.AbortsWithMessage(t, cur,98 "invalid owner address: junk",99 func() { Register(cross(cur), "vault", address("junk")) },100 )101}102103// ---------- Deprecate ----------104105func TestDeprecate_OwnerOnly(cur realm, t *testing.T) {106 resetState()107 register(cur, v1, "vault_v1", teamEOA)108 register(cur, v2, "vault_v2", teamEOA)109110 testing.SetRealm(testing.NewUserRealm(attacker))111 uassert.AbortsWithMessage(t, cur,112 "only the owner can deprecate this contract",113 func() { Deprecate(cross(cur), v1, v2) },114 )115116 testing.SetRealm(testing.NewUserRealm(teamEOA))117 Deprecate(cross(cur), v1, v2)118 uassert.Equal(t, string(v2), string(GetLatest(v1)))119}120121func TestDeprecate_SuccessorMustBeRegistered(cur realm, t *testing.T) {122 resetState()123 register(cur, v1, "vault_v1", teamEOA)124125 // the audit's dangling-successor + front-running route: pointing at126 // an unregistered address is refused outright, and a successor entry127 // can only ever be created by the successor contract itself128 testing.SetRealm(testing.NewUserRealm(teamEOA))129 uassert.AbortsWithMessage(t, cur,130 "successor is not a registered contract: "+string(v2),131 func() { Deprecate(cross(cur), v1, v2) },132 )133}134135func TestDeprecate_SelfAndDoubleRejected(cur realm, t *testing.T) {136 resetState()137 register(cur, v1, "vault_v1", teamEOA)138 register(cur, v2, "vault_v2", teamEOA)139140 testing.SetRealm(testing.NewUserRealm(teamEOA))141 uassert.AbortsWithMessage(t, cur,142 "successor cannot be the contract itself",143 func() { Deprecate(cross(cur), v1, v1) },144 )145 Deprecate(cross(cur), v1, v2)146 uassert.AbortsWithMessage(t, cur,147 "contract is already deprecated",148 func() { Deprecate(cross(cur), v1, v2) },149 )150}151152func TestDeprecate_CircularChainRejected(cur realm, t *testing.T) {153 resetState()154 register(cur, v1, "vault_v1", teamEOA)155 register(cur, v2, "vault_v2", teamEOA)156 register(cur, v3, "vault_v3", teamEOA)157158 testing.SetRealm(testing.NewUserRealm(teamEOA))159 Deprecate(cross(cur), v1, v2)160 Deprecate(cross(cur), v2, v3)161162 // closing the loop from the tail must abort163 uassert.AbortsWithMessage(t, cur,164 "deprecating "+string(v3)+" to "+string(v1)+" would create a circular migration chain",165 func() { Deprecate(cross(cur), v3, v1) },166 )167}168169// ---------- chain queries ----------170171func TestGetLatest_WalksChain(cur realm, t *testing.T) {172 resetState()173 register(cur, v1, "vault_v1", teamEOA)174 register(cur, v2, "vault_v2", teamEOA)175 register(cur, v3, "vault_v3", teamEOA)176177 testing.SetRealm(testing.NewUserRealm(teamEOA))178 Deprecate(cross(cur), v1, v2)179 Deprecate(cross(cur), v2, v3)180181 uassert.Equal(t, string(v3), string(GetLatest(v1)))182 uassert.Equal(t, string(v3), string(GetLatest(v2)))183 uassert.Equal(t, string(v3), string(GetLatest(v3)))184 // unknown addresses come back unchanged185 uassert.Equal(t, string(attacker), string(GetLatest(attacker)))186}187188func TestGetMigrationChain(cur realm, t *testing.T) {189 resetState()190 register(cur, v1, "vault_v1", teamEOA)191 register(cur, v2, "vault_v2", teamEOA)192193 testing.SetRealm(testing.NewUserRealm(teamEOA))194 Deprecate(cross(cur), v1, v2)195196 out := GetMigrationChain(v1)197 uassert.True(t, strings.Contains(out, "vault_v1"), "chain must name v1")198 uassert.True(t, strings.Contains(out, "[deprecated]"), "chain must tag deprecation")199 uassert.True(t, strings.Contains(out, "vault_v2"), "chain must name v2")200 uassert.Equal(t, "not found: "+string(attacker), GetMigrationChain(attacker))201}202203// ---------- TransferOwnership ----------204205func TestTransferOwnership(cur realm, t *testing.T) {206 resetState()207 register(cur, v1, "vault", teamEOA)208 newOwner := testutils.TestAddress("newowner")209210 testing.SetRealm(testing.NewUserRealm(attacker))211 uassert.AbortsWithMessage(t, cur,212 "only the owner can transfer ownership",213 func() { TransferOwnership(cross(cur), v1, attacker) },214 )215216 testing.SetRealm(testing.NewUserRealm(teamEOA))217 uassert.AbortsWithMessage(t, cur,218 "invalid new owner address: junk",219 func() { TransferOwnership(cross(cur), v1, address("junk")) },220 )221 TransferOwnership(cross(cur), v1, newOwner)222 // nomination alone changes nothing (two-step)223 uassert.Equal(t, string(teamEOA), string(entries[v1].Owner))224 uassert.Equal(t, string(v1), GetOwnerContracts(teamEOA))225226 testing.SetRealm(testing.NewUserRealm(newOwner))227 AcceptOwnership(cross(cur), v1)228 uassert.Equal(t, string(newOwner), string(entries[v1].Owner))229 uassert.Equal(t, "none", GetOwnerContracts(teamEOA))230 uassert.Equal(t, string(v1), GetOwnerContracts(newOwner))231}232233// ---------- render ----------234235func TestRender_EmptyState(cur realm, t *testing.T) {236 resetState()237 out := Render("")238 uassert.True(t, strings.HasPrefix(out, "# Upgrade Registry\n\n"), "heading first")239 uassert.True(t, strings.HasSuffix(out, "No contracts registered.\n"),240 "empty state must still say the registry is empty")241 // round-4 audit: the scope disclosure is not conditional on there being242 // entries — an empty page must not read as an endorsement-free zone.243 uassert.True(t, strings.Contains(out, "**Names are not.**"),244 "the name-scope disclosure must appear even in the empty state")245}246247// TestRender_CarriesNameScopeDisclosure is the regression guard for the248// round-4 audit finding that all three review passes raised: the rendered249// page gives a self-asserted NAME the most visual weight while only the250// ADDRESS is actually proven. Names are deliberately never made unique251// (first-come uniqueness on an immutable permissionless registry would let252// an attacker burn every valuable name forever), so disclosing the scope in253// the output is the entire mitigation. It cannot be added after deploy.254//255// Each assertion below names a distinct claim, so deleting or softening any256// one of them fails this test rather than silently degrading the warning.257func TestRender_CarriesNameScopeDisclosure(cur realm, t *testing.T) {258 resetState()259 register(cur, v1, "vault_v1", teamEOA)260261 // The impersonation this disclosure exists for: a second, unrelated262 // address registering the SAME name is permitted and renders equally.263 register(cur, attacker, "vault_v1", "")264265 out := Render("")266 for _, claim := range []string{267 "> [!NOTE]",268 "Addresses are self-registered and therefore verified",269 "**Names are not.**",270 "NOT unique",271 "may impersonate another project",272 "address, never by name.",273 } {274 uassert.True(t, strings.Contains(out, claim),275 "rendered disclosure must carry the claim: "+claim)276 }277278 // Both duplicate-named entries really are present — the disclosure is279 // describing a live condition, not a hypothetical one.280 uassert.Equal(t, 2, strings.Count(out, "**vault_v1**"),281 "duplicate names are permitted and both must render")282}283284func TestRender_Sections(cur realm, t *testing.T) {285 resetState()286 register(cur, v1, "vault_v1", teamEOA)287 register(cur, v2, "vault_v2", teamEOA)288289 testing.SetRealm(testing.NewUserRealm(teamEOA))290 Deprecate(cross(cur), v1, v2)291292 out := Render("")293 uassert.True(t, strings.Contains(out, "## Active"), "active section")294 uassert.True(t, strings.Contains(out, "## Deprecated"), "deprecated section")295 uassert.True(t, strings.Contains(out, "DEPRECATED"), "deprecation flag")296 uassert.True(t, strings.Contains(out, string(v2)), "successor shown")297}298299// ---------- re-audit regressions (2026-09-02) ----------300301func TestOwnerNomination_RequiresConsent(cur realm, t *testing.T) {302 resetState()303304 // re-audit P2: registering with someone else's address as owner must305 // NOT attach them — no pollution of GetOwnerContracts, no GetInfo306 // showing them as owner, until they accept307 victim := teamEOA308 testing.SetRealm(testing.NewUserRealm(attacker))309 Register(cross(cur), "gnoswap_router", victim)310311 uassert.Equal(t, string(attacker), string(entries[attacker].Owner))312 uassert.Equal(t, "none", GetOwnerContracts(victim))313 uassert.True(t, strings.Contains(GetInfo(attacker), string(attacker)),314 "info must show the contract itself as owner pre-acceptance")315316 // only the nominee can accept; a bystander cannot317 testing.SetRealm(testing.NewUserRealm(newOwnerAddr()))318 uassert.AbortsWithMessage(t, cur,319 "caller is not the pending owner of "+string(attacker),320 func() { AcceptOwnership(cross(cur), attacker) },321 )322}323324func newOwnerAddr() address {325 return testutils.TestAddress("bystander")326}327328func TestDeprecate_SuccessorConsentRequired(cur realm, t *testing.T) {329 resetState()330 register(cur, v1, "vault", teamEOA)331332 // re-audit P2: an attacker chains their entry INTO a legit contract,333 // forging predecessor provenance — refused unless the caller owns334 // the successor entry too335 testing.SetRealm(testing.NewUserRealm(attacker))336 Register(cross(cur), "vault_v1_old", "")337 uassert.AbortsWithMessage(t, cur,338 "successor entry is not owned by the caller: "+string(v1),339 func() { Deprecate(cross(cur), attacker, v1) },340 )341}342343func TestMigrationChain_MarksTruncation(cur realm, t *testing.T) {344 resetState()345 // build a chain longer than MaxChainLen entirely under one owner346 prev := address("")347 addrs := make([]address, MaxChainLen+5)348 for i := range addrs {349 addrs[i] = testutils.TestAddress("link_" + strconv.Itoa(i))350 testing.SetRealm(testing.NewUserRealm(addrs[i]))351 Register(cross(cur), "link", teamEOA)352 testing.SetRealm(testing.NewUserRealm(teamEOA))353 AcceptOwnership(cross(cur), addrs[i])354 if prev != "" {355 Deprecate(cross(cur), prev, addrs[i])356 }357 prev = addrs[i]358 }359360 out := GetMigrationChain(addrs[0])361 uassert.True(t, strings.Contains(out, "truncated"),362 "over-length chains must carry an explicit truncation marker")363 // GetLatest still resolves the true endpoint364 uassert.Equal(t, string(addrs[len(addrs)-1]), string(GetLatest(addrs[0])))365}366367func TestTransferNomination_CanBeCleared(cur realm, t *testing.T) {368 resetState()369 register(cur, v1, "vault", teamEOA)370371 testing.SetRealm(testing.NewUserRealm(teamEOA))372 TransferOwnership(cross(cur), v1, attacker)373 TransferOwnership(cross(cur), v1, "")374375 // round-4 audit L2: clearing again is a pure no-op, and the emitted376 // event is now suppressed for it. The return value and the state are377 // unchanged either way, which is all a unit test can observe here --378 // this toolchain's testing stdlib carries no events accessor. That the379 // second clear emits NOTHING is asserted externally by380 // tools/test_upgrade_registry_events.sh, which counts the events381 // `gno test -print-events` prints for this very suite.382 uassert.Equal(t, "pending ownership nomination cleared",383 TransferOwnership(cross(cur), v1, ""))384 uassert.Equal(t, "", string(mustGet(v1).PendingOwner))385386 testing.SetRealm(testing.NewUserRealm(attacker))387 uassert.AbortsWithMessage(t, cur,388 "caller is not the pending owner of "+string(v1),389 func() { AcceptOwnership(cross(cur), v1) },390 )391}392393// ---------- round-3 audit regressions (2026-09-02) ----------394395func TestRenderLatest_ExactBudgetChainIsComplete(cur realm, t *testing.T) {396 resetState()397 // exactly RenderLatestHops deprecated hops + terminal endpoint:398 // the walk must be reported COMPLETE (round-3 off-by-one)399 n := RenderLatestHops + 1400 addrs := make([]address, n)401 for i := range addrs {402 addrs[i] = testutils.TestAddress("hop_" + strconv.Itoa(i))403 testing.SetRealm(testing.NewUserRealm(addrs[i]))404 Register(cross(cur), "hop", teamEOA)405 testing.SetRealm(testing.NewUserRealm(teamEOA))406 AcceptOwnership(cross(cur), addrs[i])407 if i > 0 {408 Deprecate(cross(cur), addrs[i-1], addrs[i])409 }410 }411 latest, complete := renderLatest(addrs[0])412 uassert.True(t, complete, "an exactly-budget chain must be complete")413 uassert.Equal(t, string(addrs[n-1]), string(latest))414}415416// ---------- audit gap closures (2026-09-21, apps-pipeline round) ----------417418func TestStraySendRejectedEverywhere(cur realm, t *testing.T) {419 resetState()420 register(cur, v1, "vault", teamEOA)421422 // audit Y2: the realm holds no banker, so attached coins would423 // strand forever — every crossing function must abort them424 msg := "this realm does not accept coins"425 testing.SetRealm(testing.NewUserRealm(teamEOA))426 testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: 5}})427 uassert.AbortsWithMessage(t, cur, msg, func() { Register(cross(cur), "paid", "") })428 uassert.AbortsWithMessage(t, cur, msg, func() { AcceptOwnership(cross(cur), v1) })429 uassert.AbortsWithMessage(t, cur, msg, func() { TransferOwnership(cross(cur), v1, attacker) })430 uassert.AbortsWithMessage(t, cur, msg, func() { Deprecate(cross(cur), v1, v2) })431 testing.SetOriginSend(nil)432}433434func TestDeprecate_BoundedWalkRefusal(cur realm, t *testing.T) {435 resetState()436 // audit Y5.1: the round-3 bounded-walk panic had zero coverage.437 // Build a chain longer than MaxChainLen, then point a fresh entry438 // at its head — the verification walk must refuse, not walk it.439 prev := address("")440 addrs := make([]address, MaxChainLen+5)441 for i := range addrs {442 addrs[i] = testutils.TestAddress("bw_" + strconv.Itoa(i))443 testing.SetRealm(testing.NewUserRealm(addrs[i]))444 Register(cross(cur), "link", teamEOA)445 testing.SetRealm(testing.NewUserRealm(teamEOA))446 AcceptOwnership(cross(cur), addrs[i])447 if prev != "" {448 Deprecate(cross(cur), prev, addrs[i])449 }450 prev = addrs[i]451 }452 fresh := testutils.TestAddress("bw_fresh")453 testing.SetRealm(testing.NewUserRealm(fresh))454 Register(cross(cur), "fresh", teamEOA)455 testing.SetRealm(testing.NewUserRealm(teamEOA))456 AcceptOwnership(cross(cur), fresh)457 uassert.AbortsWithMessage(t, cur,458 "successor chain too long to verify; cannot deprecate",459 func() { Deprecate(cross(cur), fresh, addrs[0]) },460 )461}462463func TestRenderLatest_LongChainMarked(cur realm, t *testing.T) {464 resetState()465 // audit Y5.2: the incomplete branch and Render's long-chain marker466 // were untested. Budget-boundary node must be NON-terminal here.467 n := RenderLatestHops + 3468 addrs := make([]address, n)469 for i := range addrs {470 addrs[i] = testutils.TestAddress("lc_" + strconv.Itoa(i))471 testing.SetRealm(testing.NewUserRealm(addrs[i]))472 Register(cross(cur), "hop", teamEOA)473 testing.SetRealm(testing.NewUserRealm(teamEOA))474 AcceptOwnership(cross(cur), addrs[i])475 if i > 0 {476 Deprecate(cross(cur), addrs[i-1], addrs[i])477 }478 }479 _, complete := renderLatest(addrs[0])480 uassert.False(t, complete, "an over-budget chain must be reported incomplete")481 uassert.True(t, strings.Contains(Render(""), "(long chain; use GetLatest)"),482 "render must mark over-budget chains instead of mislabeling an endpoint")483}484485func TestRender_Pagination(cur realm, t *testing.T) {486 resetState()487 // audit Y5.3: the MaxRenderEntries window was untested488 for i := 0; i < MaxRenderEntries+5; i++ {489 a := testutils.TestAddress("pg_" + strconv.Itoa(i))490 testing.SetRealm(testing.NewUserRealm(a))491 Register(cross(cur), "entry", "")492 }493 out := Render("")494 uassert.True(t, strings.Contains(out,495 "_Showing the most recent "+strconv.Itoa(MaxRenderEntries)+" entries of "+496 strconv.Itoa(MaxRenderEntries+5)+"."),497 "over-window registries must announce the window AND the true total")498 // round-4 audit: announcing the window is not enough on an immutable499 // realm — the page must also say where the unlisted entries went, since500 // a flood of new entries evicts older ones from this view permanently.501 uassert.True(t, strings.Contains(out, "remain queryable by address via GetInfo"),502 "the eviction notice must point at the accessors that still serve them")503}504505func TestRenomination_OverwritesPrior(cur realm, t *testing.T) {506 resetState()507 register(cur, v1, "vault", teamEOA)508 a := testutils.TestAddress("nominee_a")509 b := testutils.TestAddress("nominee_b")510511 // audit Y5.4: a newer nomination must revoke the older one512 testing.SetRealm(testing.NewUserRealm(teamEOA))513 TransferOwnership(cross(cur), v1, a)514 TransferOwnership(cross(cur), v1, b)515 testing.SetRealm(testing.NewUserRealm(a))516 uassert.AbortsWithMessage(t, cur,517 "caller is not the pending owner of "+string(v1),518 func() { AcceptOwnership(cross(cur), v1) },519 )520 testing.SetRealm(testing.NewUserRealm(b))521 AcceptOwnership(cross(cur), v1)522 uassert.Equal(t, string(b), string(entries[v1].Owner))523}524525func TestSelfNominationAcceptKeepsIndexClean(cur realm, t *testing.T) {526 resetState()527 register(cur, v1, "vault", teamEOA)528529 // audit Y5.5: owner nominating themselves and accepting must not530 // duplicate or lose the ownerContracts index entry531 testing.SetRealm(testing.NewUserRealm(teamEOA))532 TransferOwnership(cross(cur), v1, teamEOA)533 AcceptOwnership(cross(cur), v1)534 uassert.Equal(t, string(teamEOA), string(entries[v1].Owner))535 uassert.Equal(t, string(v1), GetOwnerContracts(teamEOA))536}537538func TestOpsOnUnregisteredAbort(cur realm, t *testing.T) {539 resetState()540 // audit Y5.6: the not-found aborts were never asserted541 ghost := testutils.TestAddress("ghost")542 msg := "contract not found: " + string(ghost)543 testing.SetRealm(testing.NewUserRealm(teamEOA))544 uassert.AbortsWithMessage(t, cur, msg, func() { AcceptOwnership(cross(cur), ghost) })545 uassert.AbortsWithMessage(t, cur, msg, func() { TransferOwnership(cross(cur), ghost, teamEOA) })546 uassert.AbortsWithMessage(t, cur, msg, func() { Deprecate(cross(cur), ghost, v1) })547 // GetInfo shares mustGet with the three crossing paths above; its548 // same-realm panic is not catchable by the revive-based Aborts helpers549}550551func TestRegister_OwnerEqualsSelfNoPending(cur realm, t *testing.T) {552 resetState()553 // audit Y5.7: registering with your own address as owner must leave554 // no pending nomination (the explicit owner==contractAddr branch)555 testing.SetRealm(testing.NewUserRealm(v1))556 Register(cross(cur), "vault", v1)557 uassert.Equal(t, string(v1), string(entries[v1].Owner))558 uassert.Equal(t, "", string(entries[v1].PendingOwner))559}560AcceptOwnership(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.
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.