auth
*v0.Authorizer- OID
- 023699…434b:160
auth details
Auth
func() stringAuth returns a read-only description of the realm's current governance authority (for rendering / inspection). It renders as contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity] i.e. "GovDAO governs this realm, and only GovDAO may drive it" — a statement an on-chain reader can act on. The proposer half is load-bearing: without it the string is byte-identical whether the authority is gated or wide open (see ContractAuthority.String). It deliberately does NOT return the live \*authz.Authorizer: an exported handle to the authority would let any realm reach its mutators (Transfer, DoByPrevious, ...) directly. Returning a description instead of the handle keeps that surface unreachable from here in the first place. Valoper.AuthOwner follows the same rule for per-operator auth lists.
- OID
- 023699…434b:4
Auth details
updateInstructions
func(int, rlm .uverse.realm, newInstructions string) .uverse.errorupdateInstructions is the realm's only privileged write. It authorizes as the realm that CROSSED INTO valopers, not as valopers itself. Why DoByPrevious and not DoByCurrent: own-path + DoByCurrent is a tautology (see NewContractAuthority's godoc for the mechanism, once). \`rlm.Previous()\` is the caller — on the legitimate path r/gov/dao's executor frame, which is what init.gno's authority is pointed at. What it buys: it is the code-level guard against this realm's most likely regression, a maintainer later adding an exported crossing entrypoint that reaches here. Under DoByCurrent such an entrypoint authorizes for any caller; under DoByPrevious the caller's own address is the principal and the gate rejects it. Pinned by filetests/z\_govdao\_only\_principal\_filetest.gno. Two things it does NOT cover on its own, both closed elsewhere: re-exporting the privileged closure (guarded by the seal in NewInstructionsProposalRequest), and exporting a function whose signature is assignable to dao's \`func(realm) error\` callback type, which the proxy would invoke on an attacker's behalf. The second was a live bypass of this gate until SimpleExecutor.Execute began rejecting invocation from outside r/gov/dao (r/gov/dao/types.gno); keep BOTH conditions in mind before adding an exported symbol here. Returns the authorization error rather than panicking on it. That matters on the governance path: impl.ExecuteProposal turns an executor ERROR into status Denied plus a DeniedReason, but it cannot see a panic — a panic aborts the whole ExecuteProposal transaction, burning its gas and leaving the proposal stuck Accepted, retryable forever and never deniable. The refusal is currently unreachable on the approved route (Previous() there is always r/gov/dao), but init.gno's handler is explicitly offered as the seam for an intent check (timelock, quorum, audit trail), and the first such check to return an error would hit exactly that.
- OID
- 023699…434b:6
updateInstructions details
NewInstructionsProposalRequest
func(newInstructions string) dao.ProposalRequestNewInstructionsProposalRequest builds the GovDAO proposal that rewrites this realm's instructions. It replaces the previously exported NewInstructionsProposalCallback. SECURITY: the privileged closure must never leave this package. The VM mints a crossing frame's \`cur\` from the CALLEE's declaring package, so a closure declared here always runs with valopers' identity no matter who invokes it — and whoever holds the closure chooses its Previous() by wrapping it in an executor of their own. Handing such a closure to a caller therefore hands out the ability to satisfy this realm's gate whichever principal it is pointed at: the handler in init.gno runs the action and \`instructions\` is rewritten with no proposal and no vote. That is what the exported callback did. Returning a dao.ProposalRequest instead seals the capability: the executor is an unexported field with no accessor (dao.ProposalRequest exposes only Title/Description/Filter), so the only way to reach the closure is for GovDAO to execute the proposal that contains it. Note this is deliberately stronger than returning a dao.Executor — even a dao.NewSafeExecutor-wrapped one. Executor.Execute is an exported method, so a returned Executor stays directly invocable by its holder, and SafeExecutor's only gate (InAllowedDAOs) FAILS OPEN while the allowedDAOs list is empty, which is the documented bootstrap state (see r/gov/dao/loader/v0). A sealed request has no such conditional. The rule for this realm: no exported function may return a crossing closure, a dao.Executor, or anything else that carries valopers' frame identity — and no exported function may itself BE such a thing, i.e. have a signature assignable to dao's \`func(realm) error\` callback type, since the proxy would then invoke it on an attacker's behalf. filetests/z\_foreign\_realm\_capability\_filetest.gno pins the first; filetests/z\_govdao\_only\_principal\_filetest.gno pins the second.
- OID
- 023699…434b:7
NewInstructionsProposalRequest details
instructionsProposalTitle
untyped stringinstructionsProposalTitle is the on-chain title of the instructions proposal, named as a constant so a test can pin it: it is rendered to GovDAO voters and is the string off-chain tooling matches on. It was "/p/gnops/valopers: ..." before this realm's proposal construction moved here; the "/p/" was simply wrong (valopers is an /r/ realm).
Value
"/r/gnops/valopers: Update instructions"
rotateAuthority
func(int, rlm .uverse.realm, newContractPath string) .uverse.errorrotateAuthority replaces this realm's governance authority. Unexported, and reachable only through the sealed request built by NewAuthorityRotationProposalRequest — same discipline as updateInstructions, for the same reason. authz.Authorizer.Transfer derives its principal as rlm.Previous().Address(), so inside a GovDAO-executed callback declared here Previous() is r/gov/dao — exactly the principal init.gno's authority asserts. No same-package cross() trick is needed.
- OID
- 023699…434b:8
rotateAuthority details
NewAuthorityRotationProposalRequest
func(newContractPath string) dao.ProposalRequestNewAuthorityRotationProposalRequest builds the GovDAO proposal that re-points this realm's governance authority at \`newContractPath\`. WHY THIS EXISTS. Without it the authority installed by init.gno is permanently frozen: nothing else calls auth.Transfer, Auth() returns a description rather than the live Authorizer, and Transfer routes through the authority's own gate — so if the asserted principal ever stops being presentable, \`instructions\` becomes unwritable with no on-chain recovery. That is not hypothetical: the r/gov/dao proxy path is a governance artifact that can be superseded, and a well-formed but wrong path (say the impl path instead of the proxy) is accepted at construction and dead forever. Redeploying instead is expensive — r/sys/validators/v0/cache.gno hardcodes \`const valopersRealmPath\`, so a new pkgpath means editing a second genesis realm and re-registering every valoper. It must exist before deploy, because code cannot be added to a deployed realm. WHY A PATH AND NOT AN authz.Authority. Taking an Authority would make this an open-interface input (Class-3 impl-substitution): a proposal could install an always-approve or always-deny authority, and readers of Auth() could not tell from the rendered description alone. Taking a path keeps the installed authority canonical by construction — always a ContractAuthority with the pass-through handler and the contract-identity gate — so the only thing governance can change is WHICH principal is asserted. The path is validated by authz.NewContractAuthority, which panics on a malformed one, aborting the proposal execution rather than bricking the realm. This remains a privileged surface: a majority that can pass this proposal can point the authority at a principal it controls. That is the same power a majority already has over \`instructions\`, and it is the price of being recoverable at all.
- OID
- 023699…434b:9
NewAuthorityRotationProposalRequest details
init.7
func()- OID
- 023699…434b:10
init.7 details
MonikerMaxLength
untyped bigintValue
(32 <untyped> bigint)
DescriptionMaxLength
untyped bigintValue
(2048 <untyped> bigint)
ServerTypeCloud
untyped stringValue
"cloud"
ServerTypeOnPrem
untyped stringValue
"on-prem"
ServerTypeDataCenter
untyped stringValue
"data-center"
ErrValoperExists
*errors.errorString- OID
- 023699…434b:13
ErrValoperExists details
ErrValoperMissing
*errors.errorString- OID
- 023699…434b:16
ErrValoperMissing details
ErrInvalidAddress
*errors.errorString- OID
- 023699…434b:19
ErrInvalidAddress details
ErrInvalidMoniker
*errors.errorString- OID
- 023699…434b:22
ErrInvalidMoniker details
ErrInvalidDescription
*errors.errorString- OID
- 023699…434b:25
ErrInvalidDescription details
ErrInvalidServerType
*errors.errorString- OID
- 023699…434b:28
ErrInvalidServerType details
ErrOperatorSquatGuard
*errors.errorString- OID
- 023699…434b:31
ErrOperatorSquatGuard details
ErrSigningKeyTaken
*errors.errorString- OID
- 023699…434b:34
ErrSigningKeyTaken details
ErrFrontrunValidator
*errors.errorString- OID
- 023699…434b:37
ErrFrontrunValidator details
ErrRotationThrottled
*errors.errorString- OID
- 023699…434b:40
ErrRotationThrottled details
ErrRegistryEntryMissing
*errors.errorString- OID
- 023699…434b:43
ErrRegistryEntryMissing details
ErrPaidCallNotDirect
*errors.errorString- OID
- 023699…434b:46
ErrPaidCallNotDirect details
ErrFeeParamOutOfRange
*errors.errorString- OID
- 023699…434b:49
ErrFeeParamOutOfRange details
ErrDisallowedPubKeyType
*errors.errorString- OID
- 023699…434b:52
ErrDisallowedPubKeyType details
valopers
*v0.Tree// operator-address -> Valoper
- OID
- 023699…434b:158
valopers details
instructions
string// markdown instructions for valoper's registration
Value
"\n# Welcome to the **Valopers** realm\n\n## 📌 Purpose of this Contract\n\nThe **Valopers** contract is designed to maintain a registry of **validator profiles**. This registry provides essential information to **GovDAO members**, enabling them to make inform"...
signingRegistry
*v0.BPTree- OID
- 023699…434b:57
signingRegistry details
monikerMaxLengthMiddle
stringValue
"30"
validateMonikerRe
*regexp.Regexp// 32 characters, including spaces, hyphens or underscores in the middle
- OID
- 023699…434b:61
validateMonikerRe details
regEntry
typeregEntry tracks signing-address -> operator with retirement metadata. retiredAtHeight == 0 means the entry is currently active for the operator.
Value
valopers.regEntry
Valoper
typeValoper represents a validator operator profile.
Value
valopers.Valoper
AddToAuthList
func(addr .uverse.address, member .uverse.address)- OID
- 023699…434b:136
AddToAuthList details
DeleteFromAuthList
func(addr .uverse.address, member .uverse.address)- OID
- 023699…434b:138
DeleteFromAuthList details
Register
func(moniker string, description string, serverType string, addr .uverse.address, pubKey string)Register registers a new valoper. The \`addr\` parameter is the operator address (stable identity, profile key); \`pubKey\` is the consensus signing pubkey, from which the signing address is derived. Auth shape: - Post-genesis: OriginCaller must equal addr (operator-slot squat guard). Genesis-mode replay (ChainHeight()==0) bypasses, so migration .jsonl txs and historical Register replays succeed. - Signing-address uniqueness: derived(pubKey) must not already be in signingRegistry, active or retired. - Front-running guard: post-genesis, derived(pubKey) must not already be an active validator (a fresh registration cannot squat on the consensus address of an existing validator). Why OriginCaller==addr is sufficient for the SQUAT guard (no IsUserCall): squatting requires the attacker to satisfy OriginCaller==victim, which requires the victim's signing key. The PAYMENT check is a different matter and does need IsUserCall — see assertPaidCallIsDirect. An earlier version of this comment claimed the fee was "validated against banker.OriginSend in a way that's symmetric to IsUserCall via direct comparison"; there was no such comparison, and the fee was bypassable (reported by @D4ryl00). Auth-list seeding: the profile's Authorizable owner is set to addr (NOT OriginCaller). At H>0 the squat guard makes them equal anyway; at H==0 the deployer pattern (one signer registers many operators) requires owner == addr so each operator can manage their own profile post-genesis without needing the deployer's auth.
- OID
- 023699…434b:139
Register details
UpdateMoniker
func(addr .uverse.address, moniker string)UpdateMoniker updates an existing valoper's moniker.
- OID
- 023699…434b:140
UpdateMoniker details
UpdateDescription
func(addr .uverse.address, description string)UpdateDescription updates an existing valoper's description.
- OID
- 023699…434b:141
UpdateDescription details
UpdateKeepRunning
func(addr .uverse.address, keepRunning bool)UpdateKeepRunning updates an existing valoper's active status. Calls v0.NotifyValoperChanged because the cache stores KeepRunning.
- OID
- 023699…434b:142
UpdateKeepRunning details
UpdateServerType
func(addr .uverse.address, serverType string)UpdateServerType updates an existing valoper's server type.
- OID
- 023699…434b:143
UpdateServerType details
UpdateSigningKey
func(addr .uverse.address, newPubKey string)UpdateSigningKey rotates an operator's consensus signing key. Auth: caller must be on the operator's auth list (defaults to operator at Register time; extendable via AddToAuthList). Invariants checked at entry: - throttle: ChainHeight() - v.LastRotationHeight >= rotationPeriodBlocks - signingRegistry uniqueness: derived(newPubKey) not in registry (active OR retired); permanently blocks key reuse - fee: unsafe.OriginSend() >= rotationFee (mirrors Register's fee-check pattern) Effect: profile's SigningPubKey/SigningAddress/LastRotationHeight updated; old registry entry marked retired (retiredAtHeight = ChainHeight()); new entry inserted into signingRegistry; v0 emits remove+add to sysparams via RotateValoperSigningKey; v0 cache refreshed via NotifyValoperChanged. Rotation lands in consensus at H+2. Atomicity: Gno tx atomicity rolls back all state if any step panics. If v0.RotateValoperSigningKey panics, the registry insert and profile mutation revert with it.
- OID
- 023699…434b:144
UpdateSigningKey details
GetByAddr
func(addr .uverse.address) valopers.ValoperGetByAddr fetches the valoper using the operator address, if present.
- OID
- 023699…434b:145
GetByAddr details
Render
func(fullPath string) stringRender renders a single valoper with their information.
- OID
- 023699…434b:146
Render details
renderHome
func(path string) string- OID
- 023699…434b:147
renderHome details
isValoper
func(addr .uverse.address) boolisValoper checks if the valoper exists.
- OID
- 023699…434b:148
isValoper details
validateMoniker
func(moniker string) .uverse.errorvalidateMoniker checks if the moniker is valid.
- OID
- 023699…434b:149
validateMoniker details
validateDescription
func(description string) .uverse.errorvalidateDescription checks if the description is valid.
- OID
- 023699…434b:150
validateDescription details
validateBech32
func(addr .uverse.address) .uverse.errorvalidateBech32 checks if the value is a valid bech32 address.
- OID
- 023699…434b:151
validateBech32 details
validatePubKey
func(pubKey string) .uverse.errorvalidatePubKey checks if the public key is valid.
- OID
- 023699…434b:152
validatePubKey details
assertPaidCallIsDirect
func(int, rlm .uverse.realm)assertPaidCallIsDirect requires the immediate caller to be a plain EOA, and must be paired with every unsafe.OriginSend() amount check in this realm. Called only when a fee is actually configured. unsafe.OriginSend() reports the transaction's declared send ENVELOPE, not what this realm received. Only a direct \`maketx call\` on valopers credits the envelope to this realm's address. For a \`maketx run\` the keeper sets pkgAddr := caller (gno.land/pkg/sdk/vm/keeper.go), so the coins move from the caller to the caller and never land anywhere at all, while OriginSend() still reports the full amount to us. See gno.land/adr/pr6062\_payable\_send\_check.md: "MsgRun is exempt: the coins are moved from the caller to the caller, so nothing actually moves." So the amount check alone verifies intent, never receipt. Reported by @D4ryl00, with a local-chain reproduction: governance sets register\_fee to 1000ugnot, an operator runs a script calling Register with \`-send 1000ugnot\`, registration succeeds, valopers receives zero, and the operator still holds the coins. No hostile script is needed — the envelope is free by construction. Both fees default to 0, so nothing was live. Why IsUserCall and not an address comparison: a run script's realm is address-INDISTINGUISHABLE from its user. Inside \`main(cur realm)\` the ephemeral realm's own address IS the caller's EOA address, so every address-based guard in this realm accepts a \`maketx run\` — Register's squat guard (OriginCaller == addr) and UpdateSigningKey's auth-list check (Previous().Address() on the list) both pass. Only the pkgPath differs, and IsUserCall() is the check that reads it. It is therefore the only PreviousRealm shape where the envelope is guaranteed to have landed here: it excludes intermediate code realms and user-run ephemeral realms alike. Same reasoning and same pairing as r/sys/namereg/v0.Register, which reads OriginSend for its anti-squatting payment; see the two-guard comment there. Deliberately inside the \`fee > 0\` branch rather than at the top of the function: while a fee is unset there is no payment to establish receipt of, and gating unconditionally would break the operator-authored \`maketx run\` flows that the squat guard is happy to accept. The restriction appears exactly when, and only when, money is involved. KNOWN LIMIT. AddToAuthList takes a plain \`member address\`, so a REALM can sit on an operator's auth list, and once rotation\_fee is nonzero such a member can no longer drive UpdateSigningKey — it is not a user call. An EOA rotation bot is unaffected, which is the shape the auth-list docs describe ("HSM-bound or rotation-bot"), so nothing documented breaks; a realm-mediated rotation would. The alternative that would keep realms working is a true receipt check: track cumulative fees in realm state and require the realm's own balance to have risen by \`fee\` since the last one. That works for any caller shape, but it puts new persisted state and a banker into a genesis realm, and lets anyone pre-pay another operator's fee by donating to the realm address. Not worth it while both fees are 0 and collected fees are unwithdrawable anyway (this realm has no banker, so they strand at its address). Revisit if a realm-mediated paid rotation is ever actually wanted.
- OID
- 023699…434b:153
assertPaidCallIsDirect details
minFeeCoin
func(fee uint64) chain.CoinminFeeCoin converts a configured fee (uint64, from sysparams) into the ugnot Coin the amount checks compare against. Coin.Amount is an int64. A fee above MaxInt64 wraps negative, and \`sentCoins\[0].IsLT(minFee)\` is then false for every payment — the realm reads the fee as configured, runs assertPaidCallIsDirect, and collects 1ugnot while reporting "payment must not be less than -1ugnot". Failing closed here keeps an unrepresentable fee from opening the paid path instead of closing it. Reachable two ways today, neither requiring malice: governance can set any uint64 through the generic r/sys/params factories, and valopers/proposal.ProposeNewMinFeeProposalRequest takes an int64 and stores uint64(newMinFee), so a negative fee — the obvious way to write "disable the fee" — round-trips to 2^64-1. That signature is preserved for historical-replay compatibility and is deliberately left alone; the bound belongs at the consumption site, which covers both paths.
- OID
- 023699…434b:154
minFeeCoin details
assertPubKeyTypeAllowed
func(pubKey string)assertPubKeyTypeAllowed panics if pubKey's type is not in the chain's validator allow-list (empty list accepts any).
- OID
- 023699…434b:155
assertPubKeyTypeAllowed details
pubKeyTypeURL
func(pubKey string) (string, .uverse.error)pubKeyTypeURL returns the amino type URL (e.g. "/tm.PubKeyEd25519") of a bech32 consensus pubkey.
- OID
- 023699…434b:156
pubKeyTypeURL details
validateServerType
func(serverType string) .uverse.errorvalidateServerType checks if the server type is valid.
- OID
- 023699…434b:157