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