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

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}