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

types.gno

10.42 Kb · 345 lines
  1package dao
  2
  3import (
  4	"errors"
  5	"strings"
  6
  7	"gno.land/p/nt/bptree/v0"
  8	"gno.land/p/nt/seqid/v0"
  9)
 10
 11type ProposalID int64
 12
 13func (pid ProposalID) String() string {
 14	return seqid.ID(pid).String()
 15}
 16
 17// VoteOption is the limited voting option for a DAO proposal
 18// New govDAOs can create their own VoteOptions if needed in the
 19// future.
 20type VoteOption string
 21
 22const (
 23	AbstainVote VoteOption = "ABSTAIN" // Side is not chosen
 24	YesVote     VoteOption = "YES"     // Proposal should be accepted
 25	NoVote      VoteOption = "NO"      // Proposal should be rejected
 26)
 27
 28type VoteRequest struct {
 29	Option     VoteOption
 30	ProposalID ProposalID
 31	Metadata   interface{}
 32}
 33
 34func NewVoteRequest(option VoteOption, proposalID ProposalID) VoteRequest {
 35	return VoteRequest{
 36		Option:     option,
 37		ProposalID: proposalID,
 38	}
 39}
 40
 41func NewVoteRequestWithMetadata(option VoteOption, proposalID ProposalID, metadata interface{}) VoteRequest {
 42	return VoteRequest{
 43		Option:     option,
 44		ProposalID: proposalID,
 45		Metadata:   metadata,
 46	}
 47}
 48
 49func NewProposalRequest(title string, description string, executor Executor) ProposalRequest {
 50	return ProposalRequest{
 51		title:       title,
 52		description: description,
 53		executor:    executor,
 54	}
 55}
 56
 57func NewProposalRequestWithFilter(title string, description string, executor Executor, filter Filter) ProposalRequest {
 58	return ProposalRequest{
 59		title:       title,
 60		description: description,
 61		executor:    executor,
 62		filter:      filter,
 63	}
 64}
 65
 66type Filter interface{}
 67
 68type ProposalRequest struct {
 69	title       string
 70	description string
 71	executor    Executor
 72	filter      Filter
 73}
 74
 75func (p *ProposalRequest) Title() string {
 76	return p.title
 77}
 78
 79func (p *ProposalRequest) Description() string {
 80	return p.description
 81}
 82
 83func (p *ProposalRequest) Filter() Filter {
 84	return p.filter
 85}
 86
 87type Proposal struct {
 88	author address
 89
 90	title       string
 91	description string
 92
 93	executor    Executor
 94	allowedDAOs []string
 95}
 96
 97func (p *Proposal) Author() address {
 98	return p.author
 99}
100
101func (p *Proposal) Title() string {
102	return p.title
103}
104
105func (p *Proposal) Description() string {
106	return p.description
107}
108
109func (p *Proposal) ExecutorString() string {
110	if p.executor != nil {
111		return p.executor.String()
112	}
113
114	return ""
115}
116
117func (p *Proposal) ExecutorCreationRealm() string {
118	if p.executor != nil {
119		return p.executor.CreationRealm()
120	}
121
122	return ""
123}
124
125func (p *Proposal) AllowedDAOs() []string {
126	return append([]string(nil), p.allowedDAOs...)
127}
128
129type Proposals struct {
130	seq            seqid.ID
131	*bptree.BPTree // *bptree.BPTree[ProposalID]*Proposal
132}
133
134func NewProposals() *Proposals {
135	return &Proposals{BPTree: bptree.NewBPTree32()}
136}
137
138func (ps *Proposals) SetProposal(p *Proposal) ProposalID {
139	pid := ProposalID(int64(ps.seq))
140	updated := ps.Set(pid.String(), p)
141	if updated {
142		panic("fatal error: Override proposals is not allowed")
143	}
144	ps.seq = ps.seq.Next()
145	return pid
146}
147
148func (ps *Proposals) GetProposal(pid ProposalID) *Proposal {
149	pv := ps.Get(pid.String())
150	if pv == nil {
151		return nil
152	}
153
154	return pv.(*Proposal)
155}
156
157type Executor interface {
158	Execute(cur realm) error
159	String() string
160	CreationRealm() string
161}
162
163// NewSimpleExecutor constructs an Executor whose creationRealm is captured
164// from rlm.PkgPath() at construction time. The IsCurrent() check rejects
165// stale or stashed realm values so the captured value is the authentic
166// caller realm. creationRealm is display-only (rendered as "Executor
167// created in: ..." in proposal listings) — no auth gate downstream.
168func NewSimpleExecutor(_ int, rlm realm, callback func(realm) error, description string) *SimpleExecutor {
169	if !rlm.IsCurrent() {
170		panic("NewSimpleExecutor: rlm is not the caller's live cur (stale capture or sibling frame)")
171	}
172	if callback == nil {
173		panic("executor callback must not be nil")
174	}
175
176	return &SimpleExecutor{
177		callback:      callback,
178		desc:          description,
179		creationRealm: rlm.PkgPath(),
180	}
181}
182
183// SimpleExecutor implements the Executor interface using
184// a callback function and a description string.
185type SimpleExecutor struct {
186	callback      func(realm) error
187	desc          string
188	creationRealm string
189}
190
191// proxyPkgPath is this package: the realm whose frame Execute mints for a
192// callback, and therefore the only realm entitled to invoke one.
193const proxyPkgPath = "gno.land/r/gov/dao"
194
195// Execute runs the proposal's callback. Invocable only from this proxy.
196//
197// SECURITY: Execute is a CROSSING method declared here, so invoking it mints
198// a gno.land/r/gov/dao frame for the callback — `cur.Previous()` inside the
199// callback is this path whoever called. That is a capability: realms gate on
200// it (authz.NewContractAuthority("gno.land/r/gov/dao") + DoByPrevious,
201// ownable, anything reading Previous().Address()). Ungated, any realm could
202// wrap a reachable `func(realm) error` — including an ordinary exported
203// entrypoint of the victim — and run it with governance's identity, with no
204// proposal id minted, so nothing to render, audit or deny afterwards.
205//
206// NOT InAllowedDAOs: that is SafeExecutor's bug. It holds the impl path while
207// an executor's caller is the proxy, so it rejects the approved route and
208// fails open while empty. Exact path plus subpackages, because
209// impl.ExecuteProposal is non-crossing (Previous() is the proxy exactly), so
210// a bare prefix would reject every real proposal.
211//
212// Gates the executor object's INVOCATION only. Keeping a privileged closure,
213// or an exported entrypoint shaped like `func(realm) error`, out of reach
214// stays the consumer's job — see r/gnops/valopers/admin.gno
215// and p/moul/authz's NewContractAuthority godoc.
216func (e *SimpleExecutor) Execute(cur realm) error {
217	// IsCurrent first, as every other gate here does: otherwise Previous() is
218	// read from whatever realm value the caller threaded in.
219	if !cur.IsCurrent() {
220		return errors.New("execution denied: cur is not the caller's live realm")
221	}
222	if prev := cur.Previous().PkgPath(); prev != proxyPkgPath &&
223		!strings.HasPrefix(prev, proxyPkgPath+"/") {
224		return errors.New("execution denied: executors are only invocable by " + proxyPkgPath)
225	}
226
227	// Check if executor was created using the constructor func
228	if e.callback == nil {
229		return nil
230	}
231
232	return e.callback(cross(cur))
233}
234
235func (e *SimpleExecutor) String() string {
236	return e.desc
237}
238
239func (e *SimpleExecutor) CreationRealm() string {
240	return e.creationRealm
241}
242
243func NewSafeExecutor(e Executor) *SafeExecutor {
244	return &SafeExecutor{
245		e: e,
246	}
247}
248
249// SafeExecutor wraps an Executor to only allow its execution
250// by allowed govDAOs.
251type SafeExecutor struct {
252	e Executor
253}
254
255func (e *SafeExecutor) Execute(cur realm) error {
256	// IsCurrent first, matching every other allowlist gate in this tree
257	// (proxy.go's UpdateImpl, treasury, memberstore). Without it this method
258	// trusts whatever realm value it is handed, so a caller threading a stale
259	// or sibling-frame cur would have its Previous() read from that value
260	// rather than from the live frame.
261	//
262	// NewSafeExecutor has no call sites, so this type is currently dead code.
263	// Live proposal execution goes through the Executor interface to
264	// SimpleExecutor.Execute, which now gates on the invoking realm being the
265	// proxy -- deliberately NOT on InAllowedDAOs, which holds the impl path
266	// and therefore rejects the approved route and fails open while empty.
267	// That is this method's bug; see SimpleExecutor.Execute.
268	if !cur.IsCurrent() {
269		return errors.New("execution denied: cur is not the caller's live realm")
270	}
271	// Verify the caller is an adequate Realm
272	if !InAllowedDAOs(cur.Previous().PkgPath()) {
273		return errors.New("execution only allowed by validated govDAOs")
274	}
275
276	return e.e.Execute(cross(cur))
277}
278
279func (e *SafeExecutor) String() string {
280	return e.e.String()
281}
282
283func (e *SafeExecutor) CreationRealm() string {
284	return e.e.CreationRealm()
285}
286
287// DAO is the govDAO implementation interface. All mutating/auth-gated
288// methods take rlm as their realm-typed parameter in the second position
289// (the `_ int, rlm realm` non-crossing form): callers thread the proxy's
290// cur as data without forcing a realm transition, so the impl's existing
291// unsafe.CurrentRealm()-based auth gates (isValidCall, memberstore.Get)
292// continue to see the proxy realm. Render stays unchanged.
293type DAO interface {
294	// PreCreateProposal is called just before creating a new Proposal
295	// It is intended to be used to get the address of the proposal, that
296	// may vary depending on the DAO implementation, and to validate that
297	// the requester is allowed to do a proposal
298	PreCreateProposal(_ int, rlm realm, r ProposalRequest) (address, error)
299
300	// PostCreateProposal is called after creating the Proposal. It is
301	// intended to be used as a way to store a new proposal status, that
302	// depends on the actuall govDAO implementation
303	PostCreateProposal(_ int, rlm realm, r ProposalRequest, pid ProposalID)
304
305	// VoteOnProposal will send a petition to vote for a specific proposal
306	// to the actual govDAO implementation
307	VoteOnProposal(_ int, rlm realm, r VoteRequest) error
308
309	// PreExecuteProposal is called when someone is trying to execute a proposal by ID.
310	// Is intended to be used to validate who can trigger the proposal execution.
311	PreExecuteProposal(_ int, rlm realm, pid ProposalID) (bool, error)
312
313	// ExecuteProposal executes the proposal executor and on error changes proposal
314	// status to denied with the error message being the denial reason.
315	// It returns the executor error when it fails.
316	ExecuteProposal(_ int, rlm realm, pid ProposalID, e Executor) error
317
318	// Render will return a human-readable string in markdown format that
319	// will be used to show new data through the dao proxy entrypoint.
320	// Crossing: the chain query layer auto-injects .cur, and
321	// implementations forward cur to internal rlm-aware helpers (mux
322	// RenderRlm + downstream cross(rlm) reads).
323	Render(cur realm, pkgpath string, path string) string
324}
325
326type UpdateRequest struct {
327	DAO         DAO
328	AllowedDAOs []string
329}
330
331// NewUpdateRequest copies allowedDAOs into a fresh slice owned by
332// /r/gov/dao. Under the storage=authority model, if we stored the
333// caller-passed slice directly, the base ArrayValue would retain
334// PkgID = caller_realm: storage rent would attribute to caller, and
335// /r/gov/dao could not mutate (e.g. append to) its own copy without
336// a DidUpdate panic. The internal copy ensures the UpdateRequest
337// and its AllowedDAOs both live entirely in /r/gov/dao's authority.
338func NewUpdateRequest(d DAO, allowedDAOs []string) UpdateRequest {
339	cp := make([]string, len(allowedDAOs))
340	copy(cp, allowedDAOs)
341	return UpdateRequest{
342		DAO:         d,
343		AllowedDAOs: cp,
344	}
345}