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}