govdao.gno
7.28 Kb · 222 lines
1package impl
2
3import (
4 "chain"
5 "chain/runtime/unsafe"
6 "errors"
7
8 "gno.land/p/nt/ufmt/v0"
9 "gno.land/r/gov/dao"
10 "gno.land/r/gov/dao/memberstore/v0"
11)
12
13var ErrMemberNotFound = errors.New("member not found")
14
15type GovDAO struct {
16 pss ProposalsStatuses
17 render *render
18}
19
20func NewGovDAO() *GovDAO {
21 pss := NewProposalsStatuses()
22 d := &GovDAO{
23 pss: pss,
24 }
25
26 d.render = NewRender(d)
27
28 // Attach to package var (impl owns _govdao). Plain assignment is
29 // fine — we're in impl's package, no realm transition needed.
30 // TODO: replace with future attach().
31 _govdao = d
32
33 return d
34}
35
36// Setting this to a global variable forces attaching the GovDAO struct to this
37// realm. TODO replace with future `attach()`.
38var _govdao *GovDAO
39
40func (g *GovDAO) PreCreateProposal(_ int, rlm realm, r dao.ProposalRequest) (address, error) {
41 if !g.isValidCall(0, rlm) {
42 return "", errors.New(ufmt.Sprintf("proposal creation must be done directly by a user or through the r/gov/dao proxy. caller realm: %v; caller's previous: %v",
43 rlm, rlm.Previous()))
44 }
45
46 // Verify that the one creating the proposal is a member.
47 caller := unsafe.OriginCaller()
48 mem, _ := getMembers(cross(rlm)).GetMember(caller)
49 if mem == nil {
50 return caller, errors.New("only members can create new proposals")
51 }
52
53 return caller, nil
54}
55
56func (g *GovDAO) PostCreateProposal(_ int, rlm realm, r dao.ProposalRequest, pid dao.ProposalID) {
57 // Tiers Allowed to Vote
58 tatv := []string{memberstore.T1, memberstore.T2, memberstore.T3}
59 switch v := r.Filter().(type) {
60 case FilterByTier:
61 // only members from T1 are allowed to vote when adding new members to T1
62 if v.Tier == memberstore.T1 {
63 tatv = []string{memberstore.T1}
64 }
65 // only members from T1 and T2 are allowed to vote when adding new members to T2
66 if v.Tier == memberstore.T2 {
67 tatv = []string{memberstore.T1, memberstore.T2}
68 }
69 }
70 g.pss.Set(pid.String(), newProposalStatus(tatv))
71}
72
73func (g *GovDAO) VoteOnProposal(_ int, rlm realm, r dao.VoteRequest) error {
74 if !g.isValidCall(0, rlm) {
75 return errors.New("proposal voting must be done directly by a user")
76 }
77
78 caller := unsafe.OriginCaller()
79 mem, tie := getMembers(cross(rlm)).GetMember(caller)
80 if mem == nil {
81 return ErrMemberNotFound
82 }
83
84 status := g.pss.GetStatus(r.ProposalID)
85 if status == nil {
86 return errors.New("proposal not found")
87 }
88
89 if status.Denied || status.Accepted {
90 return errors.New(ufmt.Sprintf("proposal closed. Accepted: %v", status.Accepted))
91 }
92
93 if !status.IsAllowed(tie) {
94 return errors.New("member on specified tier is not allowed to vote on this proposal")
95 }
96
97 mVoted, _ := status.AllVotes.GetMember(caller)
98 if mVoted != nil {
99 return errors.New("already voted on proposal")
100 }
101
102 switch r.Option {
103 case dao.YesVote:
104 status.AllVotes.SetMember(tie, caller, mem)
105 status.YesVotes.SetMember(tie, caller, mem)
106 case dao.NoVote:
107 status.AllVotes.SetMember(tie, caller, mem)
108 status.NoVotes.SetMember(tie, caller, mem)
109 case dao.AbstainVote:
110 status.AllVotes.SetMember(tie, caller, mem)
111 status.AbstainVotes.SetMember(tie, caller, mem)
112 default:
113 return errors.New("voting can only be YES, NO, or ABSTAIN")
114 }
115
116 return nil
117}
118
119func (g *GovDAO) PreExecuteProposal(_ int, rlm realm, pid dao.ProposalID) (bool, error) {
120 if !g.isValidCall(0, rlm) {
121 return false, errors.New("proposal execution must be done directly by a user")
122 }
123 status := g.pss.GetStatus(pid)
124 if status == nil {
125 // Unknown to this implementation: either an unknown id, or a
126 // proposal created before a DAO upgrade replaced this GovDAO
127 // (statuses live on the instance, so a fresh instance has none).
128 // Mirrors VoteOnProposal above, which returns this same error for
129 // the same lookup. Without it the nil deref panics with an opaque
130 // "runtime error: nil pointer dereference" that reads like a VM
131 // fault. It reports the condition only — see the ADR for why the
132 // proposal still cannot be resolved.
133 return false, errors.New("proposal not found")
134 }
135 if status.Denied || status.Accepted {
136 return false, errors.New(ufmt.Sprintf("proposal already executed. Accepted: %v", status.Accepted))
137 }
138
139 if status.YesPercent(0, rlm) >= law.Supermajority {
140 status.Accepted = true
141 return true, nil
142 }
143
144 if status.NoPercent(0, rlm) >= law.Supermajority {
145 status.Denied = true
146 return false, nil
147 }
148
149 return false, errors.New(ufmt.Sprintf("proposal didn't reach supermajority yet: %v", law.Supermajority))
150}
151
152func (g *GovDAO) ExecuteProposal(_ int, rlm realm, pid dao.ProposalID, e dao.Executor) error {
153 if e == nil {
154 panic("an executor is required to execute the proposal")
155 }
156
157 status := g.pss.GetStatus(pid)
158 if status == nil {
159 panic("proposal not found")
160 }
161
162 err := e.Execute(cross(rlm))
163 if err != nil {
164 status.Accepted = false
165 status.Denied = true
166 // Clamped on the way in, not just on the way out. The error comes from
167 // the proposal's executor — third-party code — and this assignment is a
168 // write to realm storage, paid for by whoever executes the proposal
169 // rather than by whoever wrote the executor. Bounding it here means the
170 // realm never stores a reason larger than it can display. The clamp at
171 // the render site stays, because reasons stored before this change are
172 // still unbounded.
173 status.DeniedReason = "execution failed: " + clampField(err.Error(), maxRenderedReason)
174 }
175 return err
176}
177
178func (g *GovDAO) Render(cur realm, pkgPath string, path string) string {
179 // Same-realm dispatch: pass cur through as data (non-crossing).
180 return g.render.Render(0, cur, pkgPath, path)
181}
182
183// isValidCall verifies that the impl method is being invoked from the
184// r/gov/dao proxy via a legitimate user transaction (MsgCall or MsgRun).
185//
186// The proxy passes its own crossing-frame Cur as rlm when calling the
187// impl methods. rlm.IsCurrent() rejects stale or stashed realm values —
188// a malicious realm cannot replay a captured proxy cur to impersonate
189// the proxy. After the IsCurrent() check:
190// - rlm.PkgPath() == "gno.land/r/gov/dao" identifies the proxy
191// unforgeably (pkg path is set at mint time by installCrossingCur).
192// - rlm.Previous() is the caller of the proxy.
193//
194// The proxy is the only legitimate entrypoint. The impl methods are
195// non-crossing and take rlm as a regular argument, so a direct user
196// MsgCall to them cannot supply a valid rlm: the IsCurrent() check
197// rejects any forged or stashed realm value.
198//
199// This is also what makes the unsafe.OriginCaller()-based membership
200// checks safe. Those key on the transaction origin (an EOA), which is
201// correct only if the origin is the immediate caller. isValidCall
202// guarantees exactly that: it admits prev only when prev.IsUser() (a
203// direct EOA call or the origin's own ephemeral run realm) or when
204// prev's package address equals the origin — never a third-party realm
205// in the middle. Relaxing this to allow realm intermediaries would let
206// one member's vote be cast under another origin: keep the two in sync.
207func (g *GovDAO) isValidCall(_ int, rlm realm) bool {
208 if !rlm.IsCurrent() {
209 return false
210 }
211 if rlm.PkgPath() != "gno.land/r/gov/dao" {
212 return false
213 }
214 prev := rlm.Previous()
215 // MsgCall: proxy was called directly by an EOA (UserRealm).
216 if prev.IsUser() {
217 return true
218 }
219 // MsgRun: proxy was called from the ephemeral run realm; that
220 // realm's package address equals the EOA OriginCaller.
221 return chain.PackageAddress(prev.PkgPath()) == unsafe.OriginCaller()
222}