Search Apps Documentation Source Content File Folder Download Copy Actions Download

govdao.gno

5.67 Kb · 195 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/v3/memberstore"
 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.Denied || status.Accepted {
125		return false, errors.New(ufmt.Sprintf("proposal already executed. Accepted: %v", status.Accepted))
126	}
127
128	if status.YesPercent(0, rlm) >= law.Supermajority {
129		status.Accepted = true
130		return true, nil
131	}
132
133	if status.NoPercent(0, rlm) >= law.Supermajority {
134		status.Denied = true
135		return false, nil
136	}
137
138	return false, errors.New(ufmt.Sprintf("proposal didn't reach supermajority yet: %v", law.Supermajority))
139}
140
141func (g *GovDAO) ExecuteProposal(_ int, rlm realm, pid dao.ProposalID, e dao.Executor) error {
142	if e == nil {
143		panic("an executor is required to execute the proposal")
144	}
145
146	status := g.pss.GetStatus(pid)
147	if status == nil {
148		panic("proposal not found")
149	}
150
151	err := e.Execute(cross(rlm))
152	if err != nil {
153		status.Accepted = false
154		status.Denied = true
155		status.DeniedReason = "execution failed: " + err.Error()
156	}
157	return err
158}
159
160func (g *GovDAO) Render(cur realm, pkgPath string, path string) string {
161	// Same-realm dispatch: pass cur through as data (non-crossing).
162	return g.render.Render(0, cur, pkgPath, path)
163}
164
165// isValidCall verifies that the impl method is being invoked from the
166// r/gov/dao proxy via a legitimate user transaction (MsgCall or MsgRun).
167//
168// The proxy passes its own crossing-frame Cur as rlm when calling the
169// impl methods. rlm.IsCurrent() rejects stale or stashed realm values —
170// a malicious realm cannot replay a captured proxy cur to impersonate
171// the proxy. After the IsCurrent() check:
172//   - rlm.PkgPath() == "gno.land/r/gov/dao" identifies the proxy
173//     unforgeably (pkg path is set at mint time by installCrossingCur).
174//   - rlm.Previous() is the caller of the proxy.
175//
176// The proxy is the only legitimate entrypoint. The impl methods are
177// non-crossing and take rlm as a regular argument, so a direct user
178// MsgCall to them cannot supply a valid rlm: the IsCurrent() check
179// rejects any forged or stashed realm value.
180func (g *GovDAO) isValidCall(_ int, rlm realm) bool {
181	if !rlm.IsCurrent() {
182		return false
183	}
184	if rlm.PkgPath() != "gno.land/r/gov/dao" {
185		return false
186	}
187	prev := rlm.Previous()
188	// MsgCall: proxy was called directly by an EOA (UserRealm).
189	if prev.IsUser() {
190		return true
191	}
192	// MsgRun: proxy was called from the ephemeral run realm; that
193	// realm's package address equals the EOA OriginCaller.
194	return chain.PackageAddress(prev.PkgPath()) == unsafe.OriginCaller()
195}