proxy.gno
9.18 Kb · 269 lines
1package dao
2
3import (
4 "chain"
5 "errors"
6 "strconv"
7 "strings"
8
9 "gno.land/p/nt/ufmt/v0"
10)
11
12// dao is the actual govDAO implementation, having all the needed business logic
13var dao DAO
14
15// allowedDAOs contains realms that can be used to update the actual govDAO implementation,
16// and validate Proposals.
17// This is like that to be able to rollback using a previous govDAO implementation in case
18// the latest implementation has a breaking bug. After a test period, a proposal can be
19// executed to remove all previous govDAOs implementations and leave the last one.
20var allowedDAOs []string
21
22// proposals contains all the proposals in history.
23var proposals *Proposals = NewProposals()
24
25// Render calls directly to Render's DAO implementation.
26// This allows to have this realm as the main entry point for everything.
27func Render(cur realm, p string) string {
28 if dao == nil {
29 return "DAO not initialized"
30 }
31 return dao.Render(cross(cur), cur.PkgPath(), p)
32}
33
34// MustCreateProposal is an utility method that does the same as CreateProposal,
35// but instead of erroing if something happens, it panics.
36func MustCreateProposal(cur realm, r ProposalRequest) ProposalID {
37 pid, err := CreateProposal(cur, r)
38 if err != nil {
39 panic(err.Error())
40 }
41
42 return pid
43}
44
45// ExecuteProposal will try to execute the proposal with the provided ProposalID.
46// If the proposal was denied, it will return false. If the proposal is correctly
47// executed, it will return true. If something happens this function will panic.
48func ExecuteProposal(cur realm, pid ProposalID) bool {
49 return executeProposal(cur, pid, false)
50}
51
52// ExecuteOrRejectProposal executes the proposal with the provided ProposalID or rejects
53// it when there is an execution error.
54// If the proposal was denied, it will return false. If the proposal is correctly
55// executed, it will return true, unless execution fails with an error, in which case
56// proposal is rejected with the error as the reason.
57// This function allows to finish proposals by rejecting them when there is a state
58// change or an error in the proposal parameters that makes execution fail, potentially
59// leaving the proposal active forever because it can't be successfully executed.
60func ExecuteOrRejectProposal(cur realm, pid ProposalID) bool {
61 return executeProposal(cur, pid, true)
62}
63
64// CreateProposal will try to create a new proposal, that will be validated by the actual
65// govDAO implementation. If the proposal cannot be created, an error will be returned.
66func CreateProposal(cur realm, r ProposalRequest) (ProposalID, error) {
67 if dao == nil {
68 return -1, errors.New("DAO not initialized")
69 }
70 author, err := dao.PreCreateProposal(0, cur, r)
71 if err != nil {
72 return -1, err
73 }
74
75 p := &Proposal{
76 author: author,
77 title: r.title,
78 description: r.description,
79 executor: r.executor,
80 allowedDAOs: allowedDAOs[:],
81 }
82
83 pid := proposals.SetProposal(p)
84 dao.PostCreateProposal(0, cur, r, pid)
85
86 chain.Emit("ProposalCreated",
87 "id", strconv.FormatInt(int64(pid), 10),
88 )
89
90 return pid, nil
91}
92
93func MustVoteOnProposal(cur realm, r VoteRequest) {
94 if err := VoteOnProposal(cur, r); err != nil {
95 panic(err.Error())
96 }
97}
98
99// VoteOnProposal sends a vote to the actual govDAO implementation.
100// If the voter cannot vote the specified proposal, this method will return an error
101// with the explanation of why.
102func VoteOnProposal(cur realm, r VoteRequest) error {
103 if dao == nil {
104 return errors.New("DAO not initialized")
105 }
106 return dao.VoteOnProposal(0, cur, r)
107}
108
109// MustVoteOnProposalSimple is like MustVoteOnProposal but intended to be used through gnokey with basic types.
110func MustVoteOnProposalSimple(cur realm, pid int64, option string) {
111 MustVoteOnProposal(cur, VoteRequest{
112 Option: VoteOption(option),
113 ProposalID: ProposalID(pid),
114 })
115}
116
117func MustGetProposal(pid ProposalID) *Proposal {
118 p, err := GetProposal(pid)
119 if err != nil {
120 panic(err.Error())
121 }
122
123 return p
124}
125
126// GetProposal gets created proposal by its ID. Non-crossing pure read:
127// looks up the proposal in this realm's package var. Callable directly
128// from any realm without cross-call syntax.
129func GetProposal(pid ProposalID) (*Proposal, error) {
130 if dao == nil {
131 return nil, errors.New("DAO not initialized")
132 }
133 prop := proposals.GetProposal(pid)
134 if prop == nil {
135 return nil, errors.New(ufmt.Sprintf("Proposal %v does not exist.", int64(pid)))
136 }
137 return prop, nil
138}
139
140// UpdateImpl is a method intended to be used on a proposal.
141// This method will update the current govDAO implementation
142// to a new one. AllowedDAOs are a list of realms that can
143// call this method, in case the new DAO implementation had
144// a breaking bug. A nil DAO is ignored.
145// If AllowedDAOs field is not set correctly, the actual DAO
146// implementation wont be able to execute new Proposals!
147//
148// An empty AllowedDAOs is ignored rather than stored. An empty list makes
149// InAllowedDAOs() return true for every caller — the bootstrap-only state that
150// lets the genesis MsgRun seed the member set. Since this is the only site that
151// assigns allowedDAOs, ignoring empty here makes the transition
152// empty -> non-empty one-way: once locked down the DAO cannot be reopened,
153// whether the empty value arrives as a literal []string{} or from
154// NewUpdateRequest(d, nil), which copies nil into a non-nil empty slice.
155// Individual entries must be non-blank realm paths; an empty entry would match
156// a user realm's empty PkgPath() and is rejected.
157func UpdateImpl(cur realm, r UpdateRequest) {
158 // AGENTS.md: in a crossing function, always check IsCurrent() before
159 // deriving caller identity from cur.Previous(). Redundant under the
160 // crossing-frame guarantee, but mandated, and this is the single most
161 // powerful entrypoint (it rewrites the allowlist and swaps the impl).
162 if !cur.IsCurrent() {
163 panic("UpdateImpl: realm value is not the caller's live cur")
164 }
165 gRealm := cur.Previous().PkgPath()
166
167 if !InAllowedDAOs(gRealm) {
168 panic("permission denied for prev realm: " + gRealm)
169 }
170
171 if len(r.AllowedDAOs) != 0 {
172 // Every entry must be a real realm path. len() != 0 alone is the wrong
173 // invariant: InAllowedDAOs compares by exact string, and a user realm's
174 // PkgPath() is "", so a single "" entry admits any caller whose previous
175 // frame is a user realm -- the same fail-open outcome this guard exists
176 // to prevent, just spelled differently. An empty entry can only be a
177 // drafting mistake, so reject the whole request rather than silently
178 // dropping it and storing a list the proposal did not describe.
179 for i, d := range r.AllowedDAOs {
180 trimmed := strings.TrimSpace(d)
181 if trimmed == "" {
182 panic("AllowedDAOs entries must be realm paths; got an empty one")
183 }
184 // Entries are stored exactly as given, and InAllowedDAOs compares
185 // whole strings, so an entry with surrounding spaces matches no
186 // caller at all. A non-empty list also closes the bootstrap
187 // window, so a list of only padded entries is locked shut against
188 // everyone, the DAO included, with no way to reopen it.
189 //
190 // This does not make the list typo-proof, and is not meant to be:
191 // any wrong path locks the DAO out exactly the same way, and no
192 // check here can tell a typo from a realm that does not exist yet.
193 // Whitespace is worth rejecting because it is the one spelling a
194 // human reviewing the proposal cannot see. Rejected rather than
195 // trimmed, so what gets stored is what the proposal said.
196 // Reported by position, not by value. A panic message becomes the
197 // proposal's DeniedReason, which is stored, and the entry is
198 // caller-supplied and unbounded — echoing it would put an
199 // arbitrary amount of someone else's text into this realm's
200 // storage. The index is enough to find it in a list the proposal
201 // author wrote.
202 if d != trimmed {
203 panic("AllowedDAOs entries must not have leading or trailing spaces; entry " + strconv.Itoa(i))
204 }
205 }
206 // Stored as given. A defensive copy here looks prudent but would
207 // guard a write no outside realm can perform, which was checked from
208 // a separate realm rather than assumed:
209 //
210 // - Building this request as a literal fails outright, with
211 // "cannot allocate gno.land/r/gov/dao.UpdateRequest in realm ...".
212 // - Writing through a request obtained from NewUpdateRequest fails
213 // with "cannot directly modify readonly tainted object".
214 //
215 // So the only way in is NewUpdateRequest, which copies already. Both
216 // checks are language guarantees, not conventions.
217 allowedDAOs = r.AllowedDAOs
218 }
219
220 if r.DAO != nil {
221 dao = r.DAO
222 }
223}
224
225func AllowedDAOs() []string {
226 dup := make([]string, len(allowedDAOs))
227 copy(dup, allowedDAOs)
228 return dup
229}
230
231func InAllowedDAOs(pkg string) bool {
232 if len(allowedDAOs) == 0 {
233 return true // corner case for initialization
234 }
235 for _, d := range allowedDAOs {
236 if pkg == d {
237 return true
238 }
239 }
240 return false
241}
242
243func executeProposal(cur realm, pid ProposalID, execErrorRejects bool) bool {
244 if dao == nil {
245 return false
246 }
247 execute, err := dao.PreExecuteProposal(0, cur, pid)
248 if err != nil {
249 panic(err.Error())
250 }
251
252 if !execute {
253 return false
254 }
255 prop, err := GetProposal(pid)
256 if err != nil {
257 panic(err.Error())
258 }
259
260 err = dao.ExecuteProposal(0, cur, pid, prop.executor)
261 if err != nil {
262 if execErrorRejects {
263 return false
264 }
265
266 panic(err.Error())
267 }
268 return true
269}