govdao_test.gno
12.23 Kb · 332 lines
1package impl
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/urequire/v0"
10 "gno.land/r/gov/dao"
11 "gno.land/r/gov/dao/v3/memberstore"
12)
13
14// cur is a zero-value realm used as a placeholder when forwarding to
15// uassert/urequire dispatch helpers that gained an `rlm realm` param.
16// These tests pass `func()` callbacks (no crossing inside the callback),
17// so rlm is ignored — a nil realm here is safe.
18var cur realm
19
20func init(cur realm) {
21 loadMembers(cur)
22
23 dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(govDAO, []string{"gno.land/r/gov/dao/v3/impl"}))
24}
25
26var (
27 m1 = testutils.TestAddress("m1")
28 m11 = testutils.TestAddress("m1.1")
29 m111 = testutils.TestAddress("m1.1.1")
30 m1111 = testutils.TestAddress("m1.1.1.1")
31 m2 = testutils.TestAddress("m2")
32 m3 = testutils.TestAddress("m3")
33 m4 = testutils.TestAddress("m4")
34 m5 = testutils.TestAddress("m5")
35 m6 = testutils.TestAddress("m6")
36
37 noMember = testutils.TestAddress("nm1")
38)
39
40func loadMembers(cur realm) {
41 // This is needed because state is saved between unit tests,
42 // and we want to avoid having real members used on tests
43 mstore := memberstore.Get(0, cur)
44 mstore.DeleteAll()
45
46 mstore.SetTier(memberstore.T1)
47 mstore.SetTier(memberstore.T2)
48 mstore.SetTier(memberstore.T3)
49
50 mstore.SetMember(memberstore.T1, m1, memberByTier(memberstore.T1))
51 mstore.SetMember(memberstore.T1, m11, memberByTier(memberstore.T1))
52 mstore.SetMember(memberstore.T1, m111, memberByTier(memberstore.T1))
53 mstore.SetMember(memberstore.T1, m1111, memberByTier(memberstore.T1))
54
55 mstore.SetMember(memberstore.T2, m2, memberByTier(memberstore.T2))
56 mstore.SetMember(memberstore.T2, m3, memberByTier(memberstore.T2))
57 mstore.SetMember(memberstore.T3, m4, memberByTier(memberstore.T3))
58 mstore.SetMember(memberstore.T3, m5, memberByTier(memberstore.T3))
59 mstore.SetMember(memberstore.T3, m6, memberByTier(memberstore.T3))
60}
61
62func TestCreateProposalAndVote(cur realm, t *testing.T) {
63 loadMembers(cur)
64
65 portfolio := "# This is my portfolio:\n\n- THINGS"
66
67 testing.SetOriginCaller(noMember)
68 testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
69
70 nm1 := testutils.TestAddress("nm1")
71
72 urequire.AbortsWithMessage(t, cur, "Only T1 and T2 members can be added by proposal. To add a T3 member use AddMember function directly.", func(cur realm) {
73 dao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T3, portfolio))
74 })
75
76 urequire.AbortsWithMessage(t, cur, "proposer is not a member", func(cur realm) {
77 dao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio))
78 })
79
80 testing.SetOriginCaller(m1)
81 testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
82
83 proposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)
84
85 testing.SetOriginCaller(m1)
86 testing.SetRealm(testing.NewUserRealm(m1))
87 pid := dao.MustCreateProposal(cross(cur), proposalRequest)
88 urequire.Equal(t, int(pid), 0)
89
90 // m1 votes yes because that member is interested on it
91 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))
92
93 testing.SetOriginCaller(m11)
94
95 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
96
97 testing.SetOriginCaller(m2)
98
99 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
100
101 testing.SetOriginCaller(m3)
102
103 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
104
105 testing.SetOriginCaller(m4)
106
107 urequire.AbortsWithMessage(t, cur, "member on specified tier is not allowed to vote on this proposal", func() {
108 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
109 })
110
111 testing.SetOriginCaller(m111)
112
113 // Same effect as:
114 // dao.MustVoteOnProposal(dao.VoteRequest{
115 // Option: dao.NoVote,
116 // ProposalID: dao.ProposalID(0),
117 // })
118 dao.MustVoteOnProposalSimple(cross(cur), 0, "NO")
119
120 urequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), ""), "Prop #0 - New T2 Member Proposal"))
121 // urequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), ""), "Author: "+m1.String()))
122
123 urequire.AbortsWithMessage(t, cur, "proposal didn't reach supermajority yet: 66.66", func() {
124 dao.ExecuteProposal(cross(cur), dao.ProposalID(0))
125 })
126
127 testing.SetOriginCaller(m1111)
128 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
129
130 accepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(0))
131 urequire.Equal(t, false, accepted)
132
133 urequire.Equal(t, true, contains(dao.Render(cross(cur), "0"), "**PROPOSAL HAS BEEN DENIED**"))
134 urequire.Equal(t, true, contains(dao.Render(cross(cur), "0"), "NO PERCENT: 81.25%"))
135}
136
137func TestExecutorCreationRealm(cur realm, t *testing.T) {
138 loadMembers(cur)
139
140 // Test that executor creation realm is captured correctly
141 testing.SetOriginCaller(m1)
142 testing.SetRealm(testing.NewCodeRealm("gno.land/r/template/contract"))
143
144 // Create executor in the template contract realm
145 executor := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, "Test executor from template")
146
147 proposalRequest := dao.NewProposalRequest(
148 "Test Proposal",
149 "This proposal tests executor creation realm tracking",
150 executor,
151 )
152
153 // Create proposal from user realm (user can call DAO directly)
154 testing.SetRealm(testing.NewUserRealm(m1))
155 pid := dao.MustCreateProposal(cross(cur), proposalRequest)
156
157 // Get the proposal
158 prop := dao.MustGetProposal(pid)
159
160 // Verify the author is m1
161 urequire.Equal(t, m1, prop.Author())
162
163 // Verify the executor creation realm is captured correctly
164 urequire.Equal(t, "gno.land/r/template/contract", prop.ExecutorCreationRealm())
165
166 // Check that it's displayed in the individual proposal render output
167 individualRendered := dao.Render(cross(cur), pid.String())
168 urequire.Equal(t, true, contains(individualRendered, "Executor created in: gno.land/r/template/contract"))
169 urequire.Equal(t, true, contains(individualRendered, "Test executor from template"))
170
171 // Also verify the main content is there
172 urequire.Equal(t, true, contains(individualRendered, "Test Proposal"))
173 urequire.Equal(t, true, contains(individualRendered, "This proposal tests executor creation realm tracking"))
174}
175
176func TestProposalPagination(cur realm, t *testing.T) {
177 loadMembers(cur)
178 portfolio := "### This is my portfolio:\n\n- THINGS"
179
180 testing.SetOriginCaller(m1)
181 testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
182
183 nm1 := testutils.TestAddress("nm1")
184
185 var pid dao.ProposalID
186
187 proposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)
188
189 testing.SetOriginCaller(m1)
190 testing.SetRealm(testing.NewUserRealm(m1))
191 pid = dao.MustCreateProposal(cross(cur), proposalRequest)
192
193 // TODO: tests keep the same vm state: https://github.com/gnolang/gno/issues/1982
194 urequire.Equal(t, 2, int(pid))
195
196 testing.SetRealm(testing.NewUserRealm(m1))
197 pid = dao.MustCreateProposal(cross(cur), proposalRequest)
198 urequire.Equal(t, 3, int(pid))
199
200 testing.SetRealm(testing.NewUserRealm(m1))
201 pid = dao.MustCreateProposal(cross(cur), proposalRequest)
202 urequire.Equal(t, 4, int(pid))
203
204 testing.SetRealm(testing.NewUserRealm(m1))
205 pid = dao.MustCreateProposal(cross(cur), proposalRequest)
206 urequire.Equal(t, 5, int(pid))
207
208 testing.SetRealm(testing.NewUserRealm(m1))
209 pid = dao.MustCreateProposal(cross(cur), proposalRequest)
210 urequire.Equal(t, 6, int(pid))
211
212 testing.SetRealm(testing.NewUserRealm(m1))
213 pid = dao.MustCreateProposal(cross(cur), proposalRequest)
214 urequire.Equal(t, 7, int(pid))
215
216 fmt.Println(dao.Render(cross(cur), ""))
217 urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #7 - New T2 Member Proposal](/r/gov/dao:7)"))
218 urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #6 - New T2 Member Proposal](/r/gov/dao:6)"))
219 urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #5 - New T2 Member Proposal](/r/gov/dao:5)"))
220 urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #4 - New T2 Member Proposal](/r/gov/dao:4)"))
221 urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #3 - New T2 Member Proposal](/r/gov/dao:3)"))
222
223 urequire.Equal(t, true, contains(dao.Render(cross(cur), "?page=2"), "### [Prop #2 - New T2 Member Proposal](/r/gov/dao:2)"))
224 urequire.Equal(t, true, contains(dao.Render(cross(cur), "?page=2"), "### [Prop #1 - Test Proposal](/r/gov/dao:1)"))
225 urequire.Equal(t, true, contains(dao.Render(cross(cur), "?page=2"), "### [Prop #0 - New T2 Member Proposal](/r/gov/dao:0)"))
226}
227
228func TestUpgradeDaoImplementation(cur realm, t *testing.T) {
229 loadMembers(cur)
230
231 testing.SetOriginCaller(noMember)
232 testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
233
234 urequire.PanicsWithMessage(t, cur, "proposer is not a member", func() {
235 NewUpgradeDaoImplRequest(cur, govDAO, "gno.land/r/gov/dao/v4/impl", "Something happened and we have to fix it.")
236 })
237
238 testing.SetOriginCaller(m1)
239 testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
240
241 preq := NewUpgradeDaoImplRequest(cur, govDAO, "gno.land/r/gov/dao/v4/impl", "Something happened and we have to fix it.")
242
243 testing.SetOriginCaller(m1)
244 testing.SetRealm(testing.NewUserRealm(m1))
245 pid := dao.MustCreateProposal(cross(cur), preq)
246 urequire.Equal(t, int(pid), 8)
247
248 // m1 votes yes because that member is interested on it
249 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
250
251 testing.SetOriginCaller(m11)
252
253 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
254
255 testing.SetOriginCaller(m2)
256
257 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
258
259 testing.SetOriginCaller(m3)
260
261 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
262
263 testing.SetOriginCaller(m111)
264
265 // Same effect as:
266 // dao.MustVoteOnProposal(dao.VoteRequest{
267 // Option: dao.YesVote,
268 // ProposalID: dao.ProposalID(pid),
269 // })
270 dao.MustVoteOnProposalSimple(cross(cur), int64(pid), "YES")
271
272 urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "**Proposal is open for votes**"))
273 urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "68.42105263157895%"))
274 urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "0%"))
275
276 accepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(pid))
277 urequire.Equal(t, true, accepted)
278 urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "**PROPOSAL HAS BEEN ACCEPTED**"))
279 urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "YES PERCENT: 68.42105263157895%"))
280}
281
282func TestAbstainVote(cur realm, t *testing.T) {
283 loadMembers(cur)
284
285 portfolio := "# This is my portfolio:\n\n- THINGS"
286
287 testing.SetOriginCaller(m1)
288 testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
289
290 nm1 := testutils.TestAddress("nm1")
291 proposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)
292
293 testing.SetOriginCaller(m1)
294 testing.SetRealm(testing.NewUserRealm(m1))
295 pid := dao.MustCreateProposal(cross(cur), proposalRequest)
296
297 // m1 votes abstain
298 dao.MustVoteOnProposalSimple(cross(cur), int64(pid), "ABSTAIN")
299
300 // Other members vote YES to reach supermajority
301 testing.SetOriginCaller(m11)
302 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
303
304 testing.SetOriginCaller(m111)
305 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
306
307 testing.SetOriginCaller(m1111)
308 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
309
310 testing.SetOriginCaller(m2)
311 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
312
313 testing.SetOriginCaller(m3)
314 dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
315
316 // Verify render shows correct percentages
317 urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "YES PERCENT: 81.25%"))
318 urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "NO PERCENT: 0%"))
319 urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "ABSTAIN PERCENT: 18.75%"))
320
321 // Supermajority reached despite one abstain voter
322 accepted := dao.ExecuteProposal(cross(cur), pid)
323 urequire.Equal(t, true, accepted)
324 urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "**PROPOSAL HAS BEEN ACCEPTED**"))
325
326 // Abstain voter appears in vote list
327 urequire.Equal(t, true, contains(dao.Render(cross(cur), fmt.Sprintf("%v/votes", int64(pid))), "ABSTAIN"))
328}
329
330func contains(s, substr string) bool {
331 return strings.Index(s, substr) >= 0
332}