record.gno
5.28 Kb · 189 lines
1package commondao
2
3import (
4 "gno.land/p/nt/addrset/v0"
5 "gno.land/p/nt/bptree/v0"
6)
7
8type (
9 // VoteIterFn defines a callback to iterate votes.
10 VoteIterFn func(Vote) (stop bool)
11
12 // VotesCountIterFn defines a callback to iterate voted choices.
13 VotesCountIterFn func(_ VoteChoice, voteCount int) (stop bool)
14
15 // Vote defines a single vote. Its fields are unexported so instances
16 // cannot be forged or reshaped outside the package: votes enter a
17 // record only through CommonDAO.Vote's gates.
18 Vote struct {
19 addr address
20 choice VoteChoice
21 reason string
22 }
23)
24
25// NewVote creates a vote, validating the address and choice. It exists
26// so external code can build records to independently re-verify a tally
27// with TallyDefault; votes reach a DAO's own records only through
28// CommonDAO.Vote.
29func NewVote(addr address, choice VoteChoice, reason string) (Vote, error) {
30 if !addr.IsValid() {
31 return Vote{}, ErrInvalidVoterAddress
32 }
33
34 if choice != ChoiceYes && choice != ChoiceNo && choice != ChoiceAbstain {
35 return Vote{}, ErrInvalidVoteChoice
36 }
37
38 return Vote{addr: addr, choice: choice, reason: reason}, nil
39}
40
41// Address returns the address of the account that submitted the vote.
42func (v Vote) Address() address {
43 return v.addr
44}
45
46// Choice returns the voted choice.
47func (v Vote) Choice() VoteChoice {
48 return v.choice
49}
50
51// Reason returns the optional reason for the vote.
52func (v Vote) Reason() string {
53 return v.reason
54}
55
56// ReadonlyVotingRecord defines a read only voting record. The copy
57// captures the live record's tree roots by value, so a held value can go
58// stale in surprising ways: fetch it, read it, and re-fetch rather than
59// holding it across votes.
60type ReadonlyVotingRecord struct {
61 votes bptree.BPTree // string(address) -> Vote
62 count bptree.BPTree // string(choice) -> int
63}
64
65// Size returns the total number of votes that record contains.
66func (r ReadonlyVotingRecord) Size() int {
67 return r.votes.Size()
68}
69
70// Iterate iterates voting record votes.
71func (r ReadonlyVotingRecord) Iterate(offset, count int, reverse bool, fn VoteIterFn) bool {
72 cb := func(_ string, v any) bool { return fn(v.(Vote)) }
73 if reverse {
74 return r.votes.ReverseIterateByOffset(offset, count, cb)
75 }
76 return r.votes.IterateByOffset(offset, count, cb)
77}
78
79// IterateVotesCount iterates voted choices with the amount of votes submitted for each.
80func (r ReadonlyVotingRecord) IterateVotesCount(fn VotesCountIterFn) bool {
81 return r.count.Iterate("", "", func(k string, v any) bool {
82 return fn(VoteChoice(k), v.(int))
83 })
84}
85
86// VoteCount returns the number of votes for a single voting choice.
87func (r ReadonlyVotingRecord) VoteCount(c VoteChoice) int {
88 if v := r.count.Get(string(c)); v != nil {
89 return v.(int)
90 }
91 return 0
92}
93
94// HasVoted checks if an account already voted.
95func (r ReadonlyVotingRecord) HasVoted(user address) bool {
96 return r.votes.Has(user.String())
97}
98
99// GetVote returns a vote.
100func (r ReadonlyVotingRecord) GetVote(user address) (_ Vote, found bool) {
101 if v := r.votes.Get(user.String()); v != nil {
102 return v.(Vote), true
103 }
104 return Vote{}, false
105}
106
107// VotingRecord stores accounts that voted and vote choices.
108type VotingRecord struct {
109 ReadonlyVotingRecord
110}
111
112// Readonly returns a read only voting record.
113func (r VotingRecord) Readonly() ReadonlyVotingRecord {
114 return r.ReadonlyVotingRecord
115}
116
117// AddVote adds a vote to the voting record.
118// If a vote for the same user already exists is overwritten.
119func (r *VotingRecord) AddVote(vote Vote) (updated bool) {
120 // Get previous member vote if it exists
121 v := r.votes.Get(vote.addr.String())
122
123 // When a previous vote exists update counter for the previous choice
124 updated = r.votes.Set(vote.addr.String(), vote)
125 if updated {
126 prev := v.(Vote)
127 r.count.Set(string(prev.choice), r.VoteCount(prev.choice)-1)
128 }
129
130 r.count.Set(string(vote.choice), r.VoteCount(vote.choice)+1)
131 return
132}
133
134// TallyDefault applies the constitution's default Council voting rules
135// over a proposal's electorate.
136//
137// Only votes cast by electorate members are counted. The tally denominator
138// D is the electorate size minus the number of ABSTAIN votes: abstaining
139// shrinks the denominator (deference), while not voting counts against
140// passage (silence is opposition). With integer math:
141//
142// D = |electorate| - abstains
143// supermajority: passed ⇔ D > 0 && 3*yes >= 2*D
144// simple majority: passed ⇔ D > 0 && 2*yes > D
145// both: dismissed ⇔ 2*no > D
146//
147// Passing is checked before dismissal; within one electorate both can never
148// hold at once (yes+no <= D makes each pair contradictory). When D is zero
149// or negative (an empty electorate, or every member abstained) the outcome
150// stays pending: nothing can pass with zero YES votes.
151func TallyDefault(r ReadonlyVotingRecord, electorate *addrset.ReadonlySet, t Threshold) Outcome {
152 var yes, no, abstain int
153 r.Iterate(0, r.Size(), false, func(v Vote) bool {
154 if !electorate.Has(v.addr) {
155 return false
156 }
157
158 switch v.choice {
159 case ChoiceYes:
160 yes++
161 case ChoiceNo:
162 no++
163 case ChoiceAbstain:
164 abstain++
165 }
166 return false
167 })
168
169 d := electorate.Size() - abstain
170 if d <= 0 {
171 return OutcomePending
172 }
173
174 switch t {
175 case ThresholdSimpleMajority:
176 if 2*yes > d {
177 return OutcomePassed
178 }
179 default:
180 if 3*yes >= 2*d {
181 return OutcomePassed
182 }
183 }
184
185 if 2*no > d {
186 return OutcomeDismissed
187 }
188 return OutcomePending
189}