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

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}