proposal.gno
8.99 Kb ยท 403 lines
1package gnopendao9
2
3import (
4 "time"
5 "errors"
6 "chain/banker"
7 "chain/runtime"
8 "chain"
9 "strconv"
10
11 "gno.land/p/nt/commondao"
12 "gno.land/p/nt/ufmt"
13)
14
15const (
16 PROPOSAL_TYPE_APPROVE_COLLECTION = "approve_collection"
17 PROPOSAL_TYPE_REMOVE_COLLECTION = "remove_collection"
18 PROPOSAL_TYPE_UPDATE_FEES = "update_fees"
19 PROPOSAL_TYPE_WITHDRAW_TREASURY = "withdraw_treasury"
20 PROPOSAL_TYPE_FORCE_CANCEL_LISTING = "force_cancel_listing"
21
22 VOTING_PERIOD = 10 * time.Minute // 7 * 24 * time.Hour // 10 minutes for testing
23 QUORUM = 0 // 0% of members must vote
24)
25
26// CollectionProposal - Proposal to approve or remove a collection
27type CollectionProposal struct {
28 proposalType string
29 collectionAddr address
30 collectionName string
31 reason string
32 approved bool
33 executed bool
34}
35
36func newCollectionProposal(propType string, addr address, name string, reason string) *CollectionProposal {
37 return &CollectionProposal{
38 proposalType: propType,
39 collectionAddr: addr,
40 collectionName: name,
41 reason: reason,
42 approved: false,
43 executed: false,
44 }
45}
46
47func (p *CollectionProposal) Title() string {
48 if p.proposalType == PROPOSAL_TYPE_APPROVE_COLLECTION {
49 return "Approve Collection: " + p.collectionName
50 }
51 return "Remove Collection: " + p.collectionName
52}
53
54func (p *CollectionProposal) Body() string {
55 return ufmt.Sprintf(
56 "Type: %s\nCollection: %s\nAddress: %s\nReason: %s\n\nVote YES to approve, NO to reject",
57 p.proposalType,
58 p.collectionName,
59 p.collectionAddr.String(),
60 p.reason,
61 )
62}
63
64func (p *CollectionProposal) VotingPeriod() time.Duration {
65 return VOTING_PERIOD
66}
67
68func (p *CollectionProposal) Tally(
69 votes commondao.ReadonlyVotingRecord,
70 members commondao.MemberSet,
71) (bool, error) {
72 yesVotes := 0
73 noVotes := 0
74 totalVotes := 0
75
76 votes.Iterate(0, votes.Size(), false, func(v commondao.Vote) bool {
77 if string(v.Choice) == "yes" {
78 yesVotes++
79 } else if string(v.Choice) == "no" {
80 noVotes++
81 }
82 totalVotes++
83 return false
84 })
85
86 // Check quorum
87 totalMembers := members.Size()
88 quorumRequired := (totalMembers * QUORUM) / 100
89
90 if totalVotes < quorumRequired {
91 p.approved = false
92 return false, nil
93 }
94
95 // Simple majority
96 p.approved = yesVotes > noVotes
97 return p.approved, nil
98}
99
100func (p *CollectionProposal) IsApproved() bool {
101 return p.approved
102}
103
104func (p *CollectionProposal) IsExecuted() bool {
105 return p.executed
106}
107
108func (p *CollectionProposal) Execute(realm) error {
109 // Do the actual action
110 if p.proposalType == PROPOSAL_TYPE_APPROVE_COLLECTION {
111 approvedCollections.Set(p.collectionAddr.String(), true)
112 } else if p.proposalType == PROPOSAL_TYPE_REMOVE_COLLECTION {
113 approvedCollections.Remove(p.collectionAddr.String())
114 }
115
116 p.executed = true
117 return nil
118}
119
120func (p *CollectionProposal) GetCollectionAddr() address {
121 return p.collectionAddr
122}
123
124// FeesProposal - Proposal to update marketplace fees
125type FeesProposal struct {
126 newFee int64
127 reason string
128 approved bool
129 executed bool
130}
131
132func newFeesProposal(newFee int64, reason string) *FeesProposal {
133 return &FeesProposal{
134 newFee: newFee,
135 reason: reason,
136 approved: false,
137 executed: false,
138 }
139}
140
141func (p *FeesProposal) Title() string {
142 return ufmt.Sprintf("Update Marketplace Fee to %d basis points", p.newFee)
143}
144
145func (p *FeesProposal) Body() string {
146 currentFeePercent := float64(marketplaceFee) / 100.0
147 newFeePercent := float64(p.newFee) / 100.0
148
149 return ufmt.Sprintf(
150 "Current Fee: %.2f%%\nProposed Fee: %.2f%%\nReason: %s\n\nVote YES to approve, NO to reject",
151 currentFeePercent,
152 newFeePercent,
153 p.reason,
154 )
155}
156
157func (p *FeesProposal) VotingPeriod() time.Duration {
158 return VOTING_PERIOD
159}
160
161func (p *FeesProposal) Tally(
162 votes commondao.ReadonlyVotingRecord,
163 members commondao.MemberSet,
164) (bool, error) {
165 yesVotes := 0
166 noVotes := 0
167 totalVotes := 0
168
169 votes.Iterate(0, votes.Size(), false, func(v commondao.Vote) bool {
170 if string(v.Choice) == "yes" {
171 yesVotes++
172 } else if string(v.Choice) == "no" {
173 noVotes++
174 }
175 totalVotes++
176 return false
177 })
178
179 // Check quorum
180 totalMembers := members.Size()
181 quorumRequired := (totalMembers * QUORUM) / 100
182
183 if totalVotes < quorumRequired {
184 p.approved = false
185 return false, nil
186 }
187
188 // Simple majority
189 p.approved = yesVotes > noVotes
190 return p.approved, nil
191}
192
193func (p *FeesProposal) IsApproved() bool {
194 return p.approved
195}
196
197func (p *FeesProposal) IsExecuted() bool {
198 return p.executed
199}
200
201func (p *FeesProposal) Execute(realm) error {
202 // Do the actual action
203 marketplaceFee = p.newFee
204
205 p.executed = true
206 return nil
207}
208
209func (p *FeesProposal) GetNewFee() int64 {
210 return p.newFee
211}
212
213// TreasuryProposal - Proposal to withdraw funds from treasury
214type TreasuryProposal struct {
215 amount int64
216 recipient address
217 reason string
218 approved bool
219 executed bool
220}
221
222func NewTreasuryProposal(amount int64, recipient address, reason string) *TreasuryProposal {
223 return &TreasuryProposal{
224 amount: amount,
225 recipient: recipient,
226 reason: reason,
227 approved: false,
228 executed: false,
229 }
230}
231
232func (p *TreasuryProposal) Title() string {
233 return ufmt.Sprintf("Withdraw %d ugnot from Treasury", p.amount)
234}
235
236func (p *TreasuryProposal) Body() string {
237 currentBalance := GetBalance()
238
239 return ufmt.Sprintf(
240 "Withdraw: %s\nRecipient: %s\nCurrent Treasury: %s\nReason: %s\n\nVote YES to approve, NO to reject",
241 formatPrice(p.amount),
242 p.recipient.String(),
243 formatPrice(currentBalance),
244 p.reason,
245 )
246}
247
248func (p *TreasuryProposal) VotingPeriod() time.Duration {
249 return VOTING_PERIOD
250}
251
252func (p *TreasuryProposal) Tally(
253 votes commondao.ReadonlyVotingRecord,
254 members commondao.MemberSet,
255) (bool, error) {
256 yesVotes := 0
257 noVotes := 0
258 totalVotes := 0
259
260 votes.Iterate(0, votes.Size(), false, func(v commondao.Vote) bool {
261 if string(v.Choice) == "yes" || string(v.Choice) == "YES" {
262 yesVotes++
263 } else if string(v.Choice) == "no" || string(v.Choice) == "NO" {
264 noVotes++
265 }
266 totalVotes++
267 return false
268 })
269
270 // Check quorum
271 totalMembers := members.Size()
272 quorumRequired := (totalMembers * QUORUM) / 100
273
274 if totalVotes < quorumRequired {
275 p.approved = false
276 return false, nil
277 }
278
279 // Simple majority
280 p.approved = yesVotes > noVotes
281 return p.approved, nil
282}
283
284func (p *TreasuryProposal) Execute(realm) error {
285 // Verify sufficient balance
286 currentBalance := GetBalance()
287 if currentBalance < p.amount {
288 return errors.New("insufficient treasury balance")
289 }
290
291 // Send funds
292 bnkr := banker.NewBanker(banker.BankerTypeRealmSend)
293 realmAddr := runtime.CurrentRealm().Address()
294
295 coins := chain.Coins{chain.Coin{"ugnot", p.amount}}
296 bnkr.SendCoins(realmAddr, p.recipient, coins)
297
298 p.executed = true
299 return nil
300}
301
302func (p *TreasuryProposal) GetAmount() int64 {
303 return p.amount
304}
305
306func (p *TreasuryProposal) GetRecipient() address {
307 return p.recipient
308}
309
310// ForceCancelListingProposal - Proposal to force cancel a problematic listing
311type ForceCancelListingProposal struct {
312 listingId int
313 reason string
314 approved bool
315 executed bool
316}
317
318func NewForceCancelListingProposal(listingId int, reason string) *ForceCancelListingProposal {
319 return &ForceCancelListingProposal{
320 listingId: listingId,
321 reason: reason,
322 approved: false,
323 executed: false,
324 }
325}
326
327func (p *ForceCancelListingProposal) Title() string {
328 return ufmt.Sprintf("Force Cancel Listing #%d", p.listingId)
329}
330
331func (p *ForceCancelListingProposal) Body() string {
332 listing := getListing(p.listingId)
333 if listing == nil {
334 return ufmt.Sprintf("Listing #%d\nStatus: NOT FOUND\nReason: %s\n\nVote YES to approve, NO to reject", p.listingId, p.reason)
335 }
336
337 return ufmt.Sprintf(
338 "Listing ID: %d\nToken ID: %s\nSeller: %s\nPrice: %s\nReason for cancellation: %s\n\nVote YES to force cancel, NO to reject",
339 p.listingId,
340 listing.TokenId.String(),
341 listing.Seller.String(),
342 formatPrice(listing.Price),
343 p.reason,
344 )
345}
346
347func (p *ForceCancelListingProposal) VotingPeriod() time.Duration {
348 return VOTING_PERIOD
349}
350
351func (p *ForceCancelListingProposal) Tally(
352 votes commondao.ReadonlyVotingRecord,
353 members commondao.MemberSet,
354) (bool, error) {
355 yesVotes := 0
356 noVotes := 0
357 totalVotes := 0
358
359 votes.Iterate(0, votes.Size(), false, func(v commondao.Vote) bool {
360 if string(v.Choice) == "yes" || string(v.Choice) == "YES" {
361 yesVotes++
362 } else if string(v.Choice) == "no" || string(v.Choice) == "NO" {
363 noVotes++
364 }
365 totalVotes++
366 return false
367 })
368
369 // Check quorum
370 totalMembers := members.Size()
371 quorumRequired := (totalMembers * QUORUM) / 100
372
373 if totalVotes < quorumRequired {
374 p.approved = false
375 return false, nil
376 }
377
378 // Simple majority
379 p.approved = yesVotes > noVotes
380 return p.approved, nil
381}
382
383func (p *ForceCancelListingProposal) Execute(realm) error {
384 listing := getListing(p.listingId)
385 if listing == nil {
386 return errors.New("listing not found")
387 }
388
389 if !listing.Active {
390 return errors.New("listing already inactive")
391 }
392
393 // Force cancel the listing
394 listing.Active = false
395 listings.Set(strconv.Itoa(p.listingId), listing)
396
397 p.executed = true
398 return nil
399}
400
401func (p *ForceCancelListingProposal) GetListingId() int {
402 return p.listingId
403}