Search Apps Documentation Source Content File Folder Download Copy Actions Download

govdao.gno

5.14 Kb ยท 200 lines
  1package impl
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"errors"
  7
  8	"gno.land/p/nt/ufmt"
  9	"gno.land/r/gov/dao"
 10	"gno.land/r/gov/dao/v3/memberstore"
 11	"gno.land/r/sys/users"
 12)
 13
 14var ErrMemberNotFound = errors.New("member not found")
 15
 16type GovDAO struct {
 17	pss    ProposalsStatuses
 18	render *render
 19}
 20
 21func NewGovDAO() *GovDAO {
 22	pss := NewProposalsStatuses()
 23	d := &GovDAO{
 24		pss: pss,
 25	}
 26
 27	d.render = NewRender(d)
 28
 29	// There was no realm, from main(), so it succeeded, And
 30	// when returning, there was no finalization.  We don't
 31	// finalize anyways because there wasn't a realm boundary.
 32	// XXX make filetest main package a realm.
 33	//
 34	// filetest.init() ->
 35	//   v3/init.Init() ->
 36	//     NewGovDAO() ->
 37	//       returns an unsaved DAO NOTE NO REALM!
 38	//     dao.UpdateImpl =>
 39	//       saves dao under
 40	//
 41	// r/gov/dao.CrossPropposal() ->
 42	//   proposals.SetProposal(),
 43	//     that proposal lives in r/gov/dao.
 44	// r/gov/dao.ExecuteProposal() ->
 45	//   g.PreExecuteProposal() ->
 46	//     XXX g.test = 1 fails, owned by gov/dao.
 47	//
 48	//
 49	func(cur realm) {
 50		// TODO: replace with future attach()
 51		_govdao = d
 52	}(cross)
 53
 54	return d
 55}
 56
 57// Setting this to a global variable forces attaching the GovDAO struct to this
 58// realm. TODO replace with future `attach()`.
 59var _govdao *GovDAO
 60
 61func (g *GovDAO) PreCreateProposal(r dao.ProposalRequest) (address, error) {
 62	if !g.isValidCall() {
 63		return "", errors.New(ufmt.Sprintf("proposal creation must be done directly by a user or through the r/gov/dao proxy. current realm: %v; previous realm: %v",
 64			runtime.CurrentRealm(), runtime.PreviousRealm()))
 65	}
 66
 67	// Verify that the one creating the proposal is a member.
 68	caller := runtime.OriginCaller()
 69	mem, _ := getMembers(cross).GetMember(caller)
 70	if mem == nil {
 71		return caller, errors.New("only members can create new proposals")
 72	}
 73
 74	return caller, nil
 75}
 76
 77func (g *GovDAO) PostCreateProposal(r dao.ProposalRequest, pid dao.ProposalID) {
 78	// Tiers Allowed to Vote
 79	tatv := []string{memberstore.T1, memberstore.T2, memberstore.T3}
 80	switch v := r.Filter().(type) {
 81	case FilterByTier:
 82		// only members from T1 are allowed to vote when adding new members to T1
 83		if v.Tier == memberstore.T1 {
 84			tatv = []string{memberstore.T1}
 85		}
 86		// only members from T1 and T2 are allowed to vote when adding new members to T2
 87		if v.Tier == memberstore.T2 {
 88			tatv = []string{memberstore.T1, memberstore.T2}
 89		}
 90	}
 91	g.pss.Set(pid.String(), newProposalStatus(tatv))
 92}
 93
 94// Members **must** have a namespace to vote to enforce simpler tracking.
 95func (g *GovDAO) VoteOnProposal(r dao.VoteRequest) error {
 96	if !g.isValidCall() {
 97		return errors.New("proposal voting must be done directly by a user")
 98	}
 99
100	caller := runtime.OriginCaller()
101	mem, tie := getMembers(cross).GetMember(caller)
102	if mem == nil {
103		return ErrMemberNotFound
104	}
105
106	if users.ResolveAddress(caller) == nil {
107		return errors.New("voter should have a namespace")
108	}
109
110	status := g.pss.GetStatus(r.ProposalID)
111	if status == nil {
112		return errors.New("proposal not found")
113	}
114
115	if status.Denied || status.Accepted {
116		return errors.New(ufmt.Sprintf("proposal closed. Accepted: %v", status.Accepted))
117	}
118
119	if !status.IsAllowed(tie) {
120		return errors.New("member on specified tier is not allowed to vote on this proposal")
121	}
122
123	mVoted, _ := status.AllVotes.GetMember(caller)
124	if mVoted != nil {
125		return errors.New("already voted on proposal")
126	}
127
128	switch r.Option {
129	case dao.YesVote:
130		status.AllVotes.SetMember(tie, caller, mem)
131		status.YesVotes.SetMember(tie, caller, mem)
132	case dao.NoVote:
133		status.AllVotes.SetMember(tie, caller, mem)
134		status.NoVotes.SetMember(tie, caller, mem)
135	default:
136		return errors.New("voting can only be YES or NO")
137	}
138
139	return nil
140}
141
142func (g *GovDAO) PreGetProposal(pid dao.ProposalID) error {
143	return nil
144}
145
146func (g *GovDAO) PostGetProposal(pid dao.ProposalID, p *dao.Proposal) error {
147	return nil
148}
149
150func (g *GovDAO) PreExecuteProposal(pid dao.ProposalID) (bool, error) {
151	if !g.isValidCall() {
152		return false, errors.New("proposal execution must be done directly by a user")
153	}
154	status := g.pss.GetStatus(pid)
155	if status.Denied || status.Accepted {
156		return false, errors.New(ufmt.Sprintf("proposal already executed. Accepted: %v", status.Accepted))
157	}
158
159	if status.YesPercent() >= law.Supermajority {
160		status.Accepted = true
161		return true, nil
162	}
163
164	if status.NoPercent() >= law.Supermajority {
165		status.Denied = true
166		return false, nil
167	}
168
169	return false, errors.New(ufmt.Sprintf("proposal didn't reach supermajority yet: %v", law.Supermajority))
170}
171
172func (g *GovDAO) Render(pkgPath string, path string) string {
173	return g.render.Render(pkgPath, path)
174}
175
176func (g *GovDAO) isValidCall() bool {
177	// We need to verify two cases:
178	// 1: r/gov/dao (proxy) functions called directly by an user
179	// 2: r/gov/dao/v3/impl methods called directly by an user
180
181	// case 1
182	if runtime.CurrentRealm().PkgPath() == "gno.land/r/gov/dao" {
183		// called directly by an user through MsgCall
184		if runtime.PreviousRealm().IsUser() {
185			return true
186		}
187		isMsgRun := chain.PackageAddress(runtime.PreviousRealm().PkgPath()) == runtime.OriginCaller()
188		// called directly by an user through MsgRun
189		if isMsgRun {
190			return true
191		}
192	}
193
194	// case 2
195	if runtime.CurrentRealm().IsUser() {
196		return true
197	}
198
199	return false
200}