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}