Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}