govdao.gno
5.08 Kb ยท 197 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
112 if status.Denied || status.Accepted {
113 return errors.New(ufmt.Sprintf("proposal closed. Accepted: %v", status.Accepted))
114 }
115
116 if !status.IsAllowed(tie) {
117 return errors.New("member on specified tier is not allowed to vote on this proposal")
118 }
119
120 mVoted, _ := status.AllVotes.GetMember(caller)
121 if mVoted != nil {
122 return errors.New("already voted on proposal")
123 }
124
125 switch r.Option {
126 case dao.YesVote:
127 status.AllVotes.SetMember(tie, caller, mem)
128 status.YesVotes.SetMember(tie, caller, mem)
129 case dao.NoVote:
130 status.AllVotes.SetMember(tie, caller, mem)
131 status.NoVotes.SetMember(tie, caller, mem)
132 default:
133 return errors.New("voting can only be YES or NO")
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) Render(pkgPath string, path string) string {
170 return g.render.Render(pkgPath, path)
171}
172
173func (g *GovDAO) isValidCall() bool {
174 // We need to verify two cases:
175 // 1: r/gov/dao (proxy) functions called directly by an user
176 // 2: r/gov/dao/v3/impl methods called directly by an user
177
178 // case 1
179 if runtime.CurrentRealm().PkgPath() == "gno.land/r/gov/dao" {
180 // called directly by an user through MsgCall
181 if runtime.PreviousRealm().IsUser() {
182 return true
183 }
184 isMsgRun := chain.PackageAddress(runtime.PreviousRealm().PkgPath()) == runtime.OriginCaller()
185 // called directly by an user through MsgRun
186 if isMsgRun {
187 return true
188 }
189 }
190
191 // case 2
192 if runtime.CurrentRealm().IsUser() {
193 return true
194 }
195
196 return false
197}