record.gno
5.46 Kb ยท 195 lines
1package commondao
2
3import (
4 "errors"
5 "math"
6 "std"
7
8 "gno.land/p/nt/avl"
9)
10
11// ErrVoteExists indicates that a user already voted.
12var ErrVoteExists = errors.New("user already voted")
13
14type (
15 // VoteIterFn defines a callback to iterate votes.
16 VoteIterFn func(Vote) (stop bool)
17
18 // VotesCountIterFn defines a callback to iterate voted choices.
19 VotesCountIterFn func(_ VoteChoice, voteCount int) (stop bool)
20
21 // Vote defines a single vote.
22 Vote struct {
23 // Address is the address of the user that this vote belons to.
24 Address std.Address
25
26 // Choice contains the voted choice.
27 Choice VoteChoice
28
29 // Reason contains an optional reason for the vote.
30 Reason string
31
32 // Context can store any custom voting values related to the vote.
33 //
34 // Warning: When using context be careful if references/pointers are
35 // assigned to it because they could potentially be accessed anywhere,
36 // which could lead to unwanted indirect modifications.
37 Context any
38 }
39)
40
41// ReadonlyVotingRecord defines an read only voting record.
42type ReadonlyVotingRecord struct {
43 votes avl.Tree // string(address) -> Vote
44 count avl.Tree // string(choice) -> int
45}
46
47// Size returns the total number of votes that record contains.
48func (r ReadonlyVotingRecord) Size() int {
49 return r.votes.Size()
50}
51
52// Iterate iterates voting record votes.
53func (r ReadonlyVotingRecord) Iterate(offset, count int, reverse bool, fn VoteIterFn) bool {
54 cb := func(_ string, v any) bool { return fn(v.(Vote)) }
55 if reverse {
56 return r.votes.ReverseIterateByOffset(offset, count, cb)
57 }
58 return r.votes.IterateByOffset(offset, count, cb)
59}
60
61// IterateVotesCount iterates voted choices with the amount of votes submited for each.
62func (r ReadonlyVotingRecord) IterateVotesCount(fn VotesCountIterFn) bool {
63 return r.count.Iterate("", "", func(k string, v any) bool {
64 return fn(VoteChoice(k), v.(int))
65 })
66}
67
68// VoteCount returns the number of votes for a single voting choice.
69func (r ReadonlyVotingRecord) VoteCount(c VoteChoice) int {
70 if v, found := r.count.Get(string(c)); found {
71 return v.(int)
72 }
73 return 0
74}
75
76// HasVoted checks if an account already voted.
77func (r ReadonlyVotingRecord) HasVoted(user std.Address) bool {
78 return r.votes.Has(user.String())
79}
80
81// GetVote returns a vote.
82func (r ReadonlyVotingRecord) GetVote(user std.Address) (_ Vote, found bool) {
83 if v, found := r.votes.Get(user.String()); found {
84 return v.(Vote), true
85 }
86 return Vote{}, false
87}
88
89// VotingRecord stores accounts that voted and vote choices.
90type VotingRecord struct {
91 ReadonlyVotingRecord
92}
93
94// Readonly returns a read only voting record.
95func (r VotingRecord) Readonly() ReadonlyVotingRecord {
96 return r.ReadonlyVotingRecord
97}
98
99// AddVote adds a vote to the voting record.
100// If a vote for the same user already exists is overwritten.
101func (r *VotingRecord) AddVote(vote Vote) (updated bool) {
102 // Get previous member vote if it exists
103 v, _ := r.votes.Get(vote.Address.String())
104
105 // When a previous vote exists update counter for the previous choice
106 updated = r.votes.Set(vote.Address.String(), vote)
107 if updated {
108 prev := v.(Vote)
109 r.count.Set(string(prev.Choice), r.VoteCount(prev.Choice)-1)
110 }
111
112 r.count.Set(string(vote.Choice), r.VoteCount(vote.Choice)+1)
113 return
114}
115
116// FindMostVotedChoice returns the most voted choice.
117// ChoiceNone is returned when there is a tie between different
118// voting choices or when the voting record has are no votes.
119func FindMostVotedChoice(r ReadonlyVotingRecord) VoteChoice {
120 var (
121 choice VoteChoice
122 currentCount, prevCount int
123 )
124
125 r.IterateVotesCount(func(c VoteChoice, count int) bool {
126 if currentCount <= count {
127 choice = c
128 prevCount = currentCount
129 currentCount = count
130 }
131 return false
132 })
133
134 if prevCount < currentCount {
135 return choice
136 }
137 return ChoiceNone
138}
139
140// SelectChoiceByAbsoluteMajority select the vote choice by absolute majority.
141// Vote choice is a majority when chosen by more than half of the votes.
142// Absolute majority considers abstentions when counting votes.
143func SelectChoiceByAbsoluteMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {
144 choice := FindMostVotedChoice(r)
145 if choice != ChoiceNone && r.VoteCount(choice) > int(membersCount/2) {
146 return choice, true
147 }
148 return ChoiceNone, false
149}
150
151// SelectChoiceBySuperMajority select the vote choice by super majority using a 2/3s threshold.
152// Abstentions are considered when calculating the super majority choice.
153func SelectChoiceBySuperMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {
154 if membersCount < 3 {
155 return ChoiceNone, false
156 }
157
158 choice := FindMostVotedChoice(r)
159 if choice != ChoiceNone && r.VoteCount(choice) >= int(math.Ceil((2*float64(membersCount))/3)) {
160 return choice, true
161 }
162 return ChoiceNone, false
163}
164
165// SelectChoiceByPlurality selects the vote choice by plurality.
166// The choice will be considered a majority if it has votes and if there is no other
167// choice with the same number of votes. A tie won't be considered majority.
168func SelectChoiceByPlurality(r ReadonlyVotingRecord) (VoteChoice, bool) {
169 var (
170 choice VoteChoice
171 currentCount int
172 isMajority bool
173 )
174
175 r.IterateVotesCount(func(c VoteChoice, count int) bool {
176 // Don't consider explicit abstentions or invalid votes
177 if c == ChoiceAbstain || c == ChoiceNone {
178 return false
179 }
180
181 if currentCount < count {
182 choice = c
183 currentCount = count
184 isMajority = true
185 } else if currentCount == count {
186 isMajority = false
187 }
188 return false
189 })
190
191 if isMajority {
192 return choice, true
193 }
194 return ChoiceNone, false
195}