Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions 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}