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

executor_invocation_authority_filetest.gno

7.90 Kb · 184 lines
  1// PKGPATH: gno.land/r/test/execauthority
  2package execauthority
  3
  4// Regression pin for who may invoke a proposal executor.
  5//
  6// dao.Executor.Execute is a public method on a public type and
  7// dao.NewSimpleExecutor is a public constructor, so any realm can build an
  8// executor around any func(realm) error it can obtain. Execute is a CROSSING
  9// method declared in r/gov/dao, so invoking it mints a gno.land/r/gov/dao
 10// frame for the callback — and that value is a capability: realms gate on it
 11// (authz.NewContractAuthority("gno.land/r/gov/dao") + DoByPrevious, ownable,
 12// anything reading Previous().Address()).
 13//
 14// Until SimpleExecutor.Execute gated on its invoker, the forged frame was
 15// byte-identical to an approved one: same PkgPath, same Previous().PkgPath(),
 16// same Previous().Address(). A callback could not tell a supermajority-backed
 17// execution from one no member ever voted on, and the forgery minted no
 18// proposal id, so there was nothing to render, audit or deny afterwards.
 19//
 20// The documented containment was never the one holding the line. types.gno
 21// said invocation was "contained by the executor being unexported inside
 22// ProposalRequest/Proposal with no accessor" — that protects the executor
 23// OBJECT, but an attacker never needs someone else's executor: a reachable
 24// CALLBACK is enough, and an ordinary exported entrypoint of the victim whose
 25// signature is assignable to func(realm) error is such a callback.
 26// r/gnops/valopers had exactly that shape.
 27//
 28// Sections 2, 3 and 6 are the inversion: the forgery is now refused, the two
 29// frames are no longer comparable, and the proxy identity is not mintable.
 30// Section 5 keeps SafeExecutor characterized — it remains dead code that
 31// cannot be wired as written, since its allowlist holds the impl path while
 32// an executor's caller is the proxy.
 33
 34import (
 35	"chain"
 36	"strings"
 37	"testing"
 38
 39	"gno.land/r/gov/dao"
 40	"gno.land/r/gov/dao/impl/v0"
 41	"gno.land/r/gov/dao/memberstore/v0"
 42)
 43
 44const (
 45	member  address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
 46	outside address = "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn"
 47)
 48
 49// observed records the frame the callback was handed, so the approved and the
 50// forged invocation can be compared.
 51type frame struct {
 52	cur      string
 53	prev     string
 54	prevAddr address
 55	ran      bool
 56}
 57
 58var seen frame
 59
 60func probe(cur realm) error {
 61	seen = frame{
 62		cur:      cur.PkgPath(),
 63		prev:     cur.Previous().PkgPath(),
 64		prevAddr: cur.Previous().Address(),
 65		ran:      true,
 66	}
 67	return nil
 68}
 69
 70func init(cur realm) {
 71	memberstore.Get(0, cur).DeleteAll()
 72	memberstore.Get(0, cur).SetTier(memberstore.T1)
 73	memberstore.Get(0, cur).SetMember(memberstore.T1, member, memberstore.NewMember(3))
 74	dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(
 75		impl.NewGovDAO(), []string{"gno.land/r/gov/dao/impl/v0"}))
 76}
 77
 78func main(cur realm) {
 79	// 1. The approved path still works: a member proposes, votes it to
 80	// supermajority, and executes. The gate must not break governance.
 81	testing.SetOriginCaller(member)
 82	testing.SetRealm(testing.NewUserRealm(member))
 83
 84	pid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
 85		"approved", "carried by a supermajority vote", dao.NewSimpleExecutor(0, cur, probe, "")))
 86	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
 87	println("approved path executes:", dao.ExecuteProposal(cross(cur), pid))
 88	approved := seen
 89	println("callback ran:", approved.ran)
 90	println("approved frame cur :", approved.cur)
 91	println("approved frame prev:", approved.prev)
 92
 93	// 2. The forged path: no proposal, no vote, and a caller who is not a
 94	// member of any tier. Execute is called directly on a locally built
 95	// executor, and is refused.
 96	seen = frame{}
 97	testing.SetOriginCaller(outside)
 98	testing.SetRealm(testing.NewUserRealm(outside))
 99
100	// The same caller cannot open a proposal -- PreCreateProposal rejects
101	// non-members. That is the authority level the DAO grants them, and
102	// Execute must not grant more.
103	_, err := dao.CreateProposal(cross(cur), dao.NewProposalRequest(
104		"rejected", "a non-member cannot propose", dao.NewSimpleExecutor(0, cur, probe, "")))
105	println("forger cannot even open a proposal:", err != nil)
106	println("callback ran:", seen.ran)
107
108	forged := dao.NewSimpleExecutor(0, cur, probe, "no proposal backs this")
109	println("forged Execute is refused:", forged.Execute(cross(cur)))
110	println("callback ran:", seen.ran)
111
112	// 3. And because it never ran, there is no forged frame to compare: the
113	// governance identity was not minted for an unauthorized invoker.
114	println("no frame was handed out:", seen.cur == "" && seen.prev == "")
115	println("approved prev address was the gov/dao package address:",
116		approved.prevAddr == chain.PackageAddress("gno.land/r/gov/dao"))
117
118	// 4. The forgery leaves no trace on the DAO either: no proposal id was
119	// minted, so there is nothing to render, audit, or deny after the fact.
120	_, missing := dao.GetProposal(pid + 1)
121	println("no proposal was created for the forged execution:", missing != nil)
122
123	// 5. SafeExecutor still cannot be wired as written: allowedDAOs holds the
124	// impl path, while an executor's caller is the proxy path, so its check
125	// rejects the APPROVED path. It remains dead code; the gate that works
126	// lives in SimpleExecutor.Execute and keys on the proxy, not on
127	// InAllowedDAOs.
128	println("InAllowedDAOs(proxy path):", dao.InAllowedDAOs("gno.land/r/gov/dao"))
129	println("allowedDAOs holds instead:", dao.AllowedDAOs()[0])
130
131	seen = frame{}
132	testing.SetOriginCaller(member)
133	testing.SetRealm(testing.NewUserRealm(member))
134	spid := dao.MustCreateProposal(cross(cur), dao.NewProposalRequest(
135		"safe", "same approved path, wrapped in SafeExecutor",
136		dao.NewSafeExecutor(dao.NewSimpleExecutor(0, cur, probe, ""))))
137	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, spid))
138	println("SafeExecutor rejects the approved path:", !dao.ExecuteOrRejectProposal(cross(cur), spid))
139	println("and its callback never ran:", !seen.ran)
140	println("denial reason names the allowlist:",
141		strings.Contains(dao.Render(cross(cur), spid.String()), "execution only allowed by validated govDAOs"))
142
143	// 6. The reachability precondition is now closed at the primitive. A
144	// realm-authored callback is stuck with its own current realm (the one
145	// value a forger never controlled), and it can no longer borrow the
146	// proxy as Previous() either -- so gates keying on Previous(), which
147	// were the forgeable ones, hold.
148	testing.SetOriginCaller(outside)
149	testing.SetRealm(testing.NewUserRealm(outside))
150	seen = frame{}
151	println("forged Execute refused again:",
152		dao.NewSimpleExecutor(0, cur, probe, "").Execute(cross(cur)) != nil)
153	println("no proxy identity was borrowed:", seen.prev != "gno.land/r/gov/dao")
154
155	// The member store was never reachable this way -- it gates on PkgPath(),
156	// which the forgery does not control -- but the attempt is now refused one
157	// layer earlier, at Execute, so the abort below never happens.
158	e := dao.NewSimpleExecutor(0, cur, func(cur realm) error {
159		memberstore.Get(0, cur).SetMember(memberstore.T1, outside, memberstore.NewMember(3))
160		return nil
161	}, "")
162	println("member-store escalation refused at Execute:", e.Execute(cross(cur)) != nil)
163}
164
165// Output:
166// approved path executes: true
167// callback ran: true
168// approved frame cur : gno.land/r/test/execauthority
169// approved frame prev: gno.land/r/gov/dao
170// forger cannot even open a proposal: true
171// callback ran: false
172// forged Execute is refused: execution denied: executors are only invocable by gno.land/r/gov/dao
173// callback ran: false
174// no frame was handed out: true
175// approved prev address was the gov/dao package address: true
176// no proposal was created for the forged execution: true
177// InAllowedDAOs(proxy path): false
178// allowedDAOs holds instead: gno.land/r/gov/dao/impl/v0
179// SafeExecutor rejects the approved path: true
180// and its callback never ran: true
181// denial reason names the allowlist: true
182// forged Execute refused again: true
183// no proxy identity was borrowed: true
184// member-store escalation refused at Execute: true