Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}