← Back to Realms

Realm

gno.land/r/sys/params

Overview

Realm Path
gno.land/r/sys/params
Exported Functions
33
State Entries
31
Source Files
14
Total Package Entries
77

Exported Functions

33 exported functions

State

31 state entries

Source Code

FILES
delegate.gno
go
1package params
2
3// Delegation: letting a realm other than GovDAO manage one specific parameter.
4//
5// This realm holds a capability nothing else can: the `sys/params` stdlib
6// refuses every caller except `gno.land/r/sys/params`, checked in the VM by
7// package path, and the same gate covers the getters. So every parameter write
8// on the chain physically originates here, and "delegating a parameter" can only
9// mean adding a path in this file that authorizes someone other than a GovDAO
10// vote.
11//
12// Two shapes are deliberately NOT used.
13//
14// Not a registry of key -> realm. This realm can never be redeployed
15// (AddPackage refuses an occupied path), and the stdlib gate names this exact
16// path, so making a NEW parameter delegatable already requires editing this
17// file, which means a chain relaunch. A registry's runtime generality would
18// therefore never be exercised: the only freedom that matters is "which realm,
19// or none" for keys already blessed in source. A named slot says exactly that
20// and nothing more. It also removes a whole class of mistake — there is no
21// key string to mis-compose, no container to accidentally expose, and no way for
22// a proposal to name a key the author never considered. A shape-based allowlist
23// like "<module>:p:<name>" would have admitted bank:p:restricted_denoms and
24// auth:p:unrestricted_addrs, which is not a delegation anyone asked for.
25//
26// Not a capability object. Returning something the delegate holds would make
27// authority survive revocation, because the check would have happened at
28// construction. The authority is re-checked on every crossing call instead, so
29// clearing the slot takes effect immediately.
30
31import (
32	"chain"
33
34	"gno.land/r/gov/dao"
35)
36
37// Event names. PascalCase with a named const is the house style across r/sys
38// and r/gov; the bare lowercase "set" elsewhere in this realm predates it.
39const (
40	DelegateSetEvent     = "ParamDelegateSet"
41	DelegateClearedEvent = "ParamDelegateCleared"
42	DelegateWriteEvent   = "ParamDelegateWrite"
43)
44
45// assertDelegate authorizes a caller as the holder of a delegated capability.
46//
47// want is the authorized package path, or "" when nothing is delegated.
48//
49// The empty check comes FIRST and is load-bearing, not defensive. A direct call
50// from a user account has an empty previous package path — that is exactly what
51// IsUserCall tests — so comparing against an unset slot would compare "" to ""
52// and admit every user on the chain. r/gov/dao's own allowlist has the
53// mirror-image bug (it returns true for everyone when the list is empty), kept
54// there deliberately for genesis bootstrap. Nothing here needs that, so empty
55// denies.
56//
57// Matching is exact, never a prefix. A sub-realm identity minted by
58// cur.Sub(subpath) presents the synthesized path "host#subpath", so a delegate
59// may legitimately be a single DAO hosted by a multi-tenant realm — for
60// instance "gno.land/r/nt/commondao/v0#dao/42". An anchored-prefix match on the
61// host would hand the capability to every DAO that realm hosts, and a bare
62// prefix match would additionally match sibling packages. "#" cannot occur in a
63// real package path, so exact matching on the full string is unambiguous.
64//
65// Takes rlm in the non-crossing dispatch position, matching assertValsetCaller
66// so the two gates stay one shape.
67// subject names what is being written, so the refusal stays specific to the
68// capability rather than generic to the mechanism. params_valset_auth.txtar
69// pins valset's exact wording as a regression test for PR #5485.
70func assertDelegate(_ int, rlm realm, want, subject string) {
71	if want == "" {
72		panic("unauthorized: no delegate is configured for " + subject)
73	}
74	// The helper trusts its rlm input, so a future caller threading a stashed
75	// or sibling-frame realm value would otherwise bypass the path check below.
76	if !rlm.IsCurrent() {
77		panic("unauthorized: rlm is not the caller's live cur")
78	}
79	if rlm.Previous().PkgPath() != want {
80		panic("unauthorized: only " + want + " may write " + subject)
81	}
82}
83
84// assertDelegatePath rejects package paths that must never be stored in a
85// delegation slot, at the point a proposal is built rather than when it
86// executes.
87//
88// Rejecting the empty string is the same fail-open guard as in assertDelegate,
89// enforced on the way in as well so an accidental clear-by-empty-set cannot be
90// mistaken for a grant. Requiring the gno.land/r/ prefix excludes user accounts
91// (empty path), pure packages, and ephemeral `maketx run` realms — the last of
92// which matters because a run realm's path is non-empty, so a check based on
93// "is this code" would let one through.
94func assertDelegatePath(pkgpath string) {
95	if pkgpath == "" {
96		panic("invalid delegate: empty package path")
97	}
98	if !isRealmPath(pkgpath) {
99		panic("invalid delegate: must be a gno.land/r/ realm path, got " + pkgpath)
100	}
101	// Reject anything no caller could ever present, so a delegation cannot be
102	// silently dead: GovDAO would believe it granted the capability while the
103	// delegate is refused on every call. The sibling UpdateImpl rejects
104	// whitespace-padded entries for the same reason.
105	for i := 0; i < len(pkgpath); i++ {
106		c := pkgpath[i]
107		switch {
108		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
109		case c == '/' || c == '.' || c == '-' || c == '_' || c == '#':
110		default:
111			panic("invalid delegate: " + pkgpath +
112				" contains a character no package path or sub-identity can hold")
113		}
114	}
115	if n := countByte(pkgpath, '#'); n > 1 {
116		panic("invalid delegate: more than one '#' in " + pkgpath)
117	}
118}
119
120func countByte(s string, b byte) int {
121	n := 0
122	for i := 0; i < len(s); i++ {
123		if s[i] == b {
124			n++
125		}
126	}
127	return n
128}
129
130const realmPathPrefix = "gno.land/r/"
131
132func isRealmPath(pkgpath string) bool {
133	return len(pkgpath) > len(realmPathPrefix) &&
134		pkgpath[:len(realmPathPrefix)] == realmPathPrefix
135}
136
137// newDelegateProposal builds the GovDAO proposal that applies a delegation
138// change. apply mutates the slot and is run only when the vote passes.
139//
140// event is emitted from inside the executor, so an observer sees the delegation
141// change exactly when it takes effect rather than when it was proposed. from is
142// the previous holder ("" when none), included so a re-delegation is auditable
143// from the single event without reading prior state.
144// apply returns the holder it replaced, read at EXECUTION time. Capturing that
145// at creation would misreport a re-delegation: two set-proposals created while
146// the slot is empty, then executed in sequence, would both emit from="" even
147// though the second replaced the first.
148func newDelegateProposal(cur realm, event, key, to, title, desc string, apply func() string) dao.ProposalRequest {
149	callback := func(cur realm) error {
150		from := apply()
151		chain.Emit(event, "key", key, "from", from, "to", to)
152		return nil
153	}
154	return dao.NewProposalRequest(title, desc,
155		dao.NewSimpleExecutor(0, cur, callback, ""))
156}
157

Raw Package Data

Raw JSON data