Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}