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