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

ballot.gno

6.54 Kb · 249 lines
  1// Package ballot is a Gno port of the classic Solidity "Ballot" voting
  2// contract from the Solidity docs. A chairperson seeds a fixed list of
  3// proposals and grants specific addresses the right to vote; each voter can
  4// either cast a direct vote or delegate their weight to another voter, and
  5// delegation follows a chain so a delegate who later delegates further still
  6// carries every accumulated weight. The winning proposal is whichever has
  7// the most accumulated weight at query time.
  8package ballot
  9
 10import (
 11	"errors"
 12	"strconv"
 13	"strings"
 14
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18// proposal is one of the fixed choices set up at Init time.
 19type proposal struct {
 20	name  string
 21	votes uint64
 22}
 23
 24// voterInfo tracks one address's voting rights and delegation state.
 25type voterInfo struct {
 26	weight   uint64  // 0 means "no right to vote"
 27	voted    bool    // true once the weight has been cast (directly or delegated away)
 28	delegate address // set only when voted == true and this voter delegated
 29	vote     int     // proposal index this voter's weight ended up on, valid once tallied
 30}
 31
 32// Package-level persistent state.
 33var (
 34	chairperson address
 35	initialized bool
 36	proposals   []proposal
 37	voters      *avl.Tree // address string -> *voterInfo
 38)
 39
 40var (
 41	errAlreadyInit    = errors.New("ballot: already initialized")
 42	errNotInit        = errors.New("ballot: not initialized yet")
 43	errNotChairperson = errors.New("ballot: only the chairperson may do this")
 44	errNoProposals    = errors.New("ballot: at least one proposal name is required")
 45	errInvalidAddr    = errors.New("ballot: invalid address")
 46	errAlreadyRight   = errors.New("ballot: voter already has the right to vote")
 47	errAlreadyVoted   = errors.New("ballot: voter already voted")
 48	errNoRight        = errors.New("ballot: caller has no right to vote")
 49	errSelfDelegate   = errors.New("ballot: self-delegation is not allowed")
 50	errDelegateLoop   = errors.New("ballot: found a loop in the delegation chain")
 51	errDelegateNoRight = errors.New("ballot: delegate has no right to vote")
 52	errBadProposal    = errors.New("ballot: invalid proposal index")
 53)
 54
 55func init() {
 56	voters = avl.NewTree()
 57}
 58
 59func requireInit() {
 60	if !initialized {
 61		panic(errNotInit)
 62	}
 63}
 64
 65func getOrCreateVoter(addr address) *voterInfo {
 66	if v := voters.Get(addr.String()); v != nil {
 67		return v.(*voterInfo)
 68	}
 69	v := &voterInfo{}
 70	voters.Set(addr.String(), v)
 71	return v
 72}
 73
 74// Init configures the ballot exactly once: the caller becomes the
 75// chairperson (and is immediately given the right to vote), and
 76// proposalNamesCSV — a comma-separated list — seeds the fixed set of
 77// proposals. It panics if called twice or with no usable proposal names.
 78func Init(cur realm, proposalNamesCSV string) {
 79	if initialized {
 80		panic(errAlreadyInit)
 81	}
 82
 83	for _, raw := range strings.Split(proposalNamesCSV, ",") {
 84		name := strings.TrimSpace(raw)
 85		if name == "" {
 86			continue
 87		}
 88		proposals = append(proposals, proposal{name: name})
 89	}
 90	if len(proposals) == 0 {
 91		panic(errNoProposals)
 92	}
 93
 94	chairperson = cur.Previous().Address()
 95	voters.Set(chairperson.String(), &voterInfo{weight: 1})
 96	initialized = true
 97}
 98
 99// GiveRightToVote grants voterAddr a voting weight of 1. Only the
100// chairperson may call this, and only for voters who haven't voted yet.
101func GiveRightToVote(cur realm, voterAddr string) {
102	requireInit()
103	if cur.Previous().Address() != chairperson {
104		panic(errNotChairperson)
105	}
106
107	addr := address(voterAddr)
108	if !addr.IsValid() {
109		panic(errInvalidAddr)
110	}
111
112	v := getOrCreateVoter(addr)
113	if v.voted {
114		panic(errAlreadyVoted)
115	}
116	if v.weight != 0 {
117		panic(errAlreadyRight)
118	}
119	v.weight = 1
120}
121
122// Delegate hands the caller's voting weight to toAddr. If toAddr has
123// already voted, the weight is added directly to that proposal's tally;
124// otherwise it accumulates on toAddr's own weight, ready to be cast (or
125// delegated further) later. Mirrors Solidity's delegation-chain-following
126// with a loop guard.
127func Delegate(cur realm, toAddr string) {
128	requireInit()
129
130	sender := cur.Previous().Address()
131	from := getOrCreateVoter(sender)
132	if from.weight == 0 {
133		panic(errNoRight)
134	}
135	if from.voted {
136		panic(errAlreadyVoted)
137	}
138
139	to := address(toAddr)
140	if !to.IsValid() {
141		panic(errInvalidAddr)
142	}
143	if to == sender {
144		panic(errSelfDelegate)
145	}
146
147	var zero address
148	maxSteps := voters.Size() + 1
149	for steps := 0; ; steps++ {
150		v := voters.Get(to.String())
151		if v == nil {
152			break
153		}
154		next := v.(*voterInfo).delegate
155		if next == zero {
156			break
157		}
158		to = next
159		if to == sender {
160			panic(errDelegateLoop)
161		}
162		if steps > maxSteps {
163			panic(errDelegateLoop)
164		}
165	}
166
167	delegateVoter := getOrCreateVoter(to)
168	if delegateVoter.weight == 0 {
169		panic(errDelegateNoRight)
170	}
171
172	from.voted = true
173	from.delegate = to
174
175	if delegateVoter.voted {
176		proposals[delegateVoter.vote].votes += from.weight
177	} else {
178		delegateVoter.weight += from.weight
179	}
180}
181
182// Vote casts the caller's full weight for proposals[proposalIndex].
183func Vote(cur realm, proposalIndex int) {
184	requireInit()
185
186	sender := cur.Previous().Address()
187	v := getOrCreateVoter(sender)
188	if v.weight == 0 {
189		panic(errNoRight)
190	}
191	if v.voted {
192		panic(errAlreadyVoted)
193	}
194	if proposalIndex < 0 || proposalIndex >= len(proposals) {
195		panic(errBadProposal)
196	}
197
198	v.voted = true
199	v.vote = proposalIndex
200	proposals[proposalIndex].votes += v.weight
201}
202
203// WinningProposal returns the index of the proposal with the most
204// accumulated weight, breaking ties in favor of the lowest index.
205func WinningProposal() int {
206	winner := 0
207	best := uint64(0)
208	for i, p := range proposals {
209		if p.votes > best {
210			best = p.votes
211			winner = i
212		}
213	}
214	return winner
215}
216
217// WinnerName returns the name of the current winning proposal, or "" if
218// the ballot hasn't been initialized.
219func WinnerName() string {
220	if len(proposals) == 0 {
221		return ""
222	}
223	return proposals[WinningProposal()].name
224}
225
226func Render(path string) string {
227	var b strings.Builder
228	b.WriteString("# Ballot\n\n")
229
230	if !initialized {
231		b.WriteString("Not initialized yet. The first caller to run `Init` becomes the " +
232			"chairperson and seeds the proposal list, e.g.:\n\n")
233		b.WriteString("```\nInit(\"Alice,Bob,Carol\")\n```\n")
234		return b.String()
235	}
236
237	b.WriteString("Chairperson: `" + chairperson.String() + "`\n\n")
238	b.WriteString("| # | Proposal | Votes |\n")
239	b.WriteString("|---|---|---|\n")
240	for i, p := range proposals {
241		b.WriteString("| " + strconv.Itoa(i) + " | " + p.name + " | " +
242			strconv.FormatUint(p.votes, 10) + " |\n")
243	}
244
245	b.WriteString("\nCurrent winner: **" + WinnerName() + "** (index " +
246		strconv.Itoa(WinningProposal()) + ")\n\n")
247	b.WriteString("Registered voters: " + strconv.Itoa(voters.Size()) + "\n")
248	return b.String()
249}