Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

delegate.gno

6.77 Kb · 156 lines
  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}