1// Package ballot is a Gno port of the classic Solidity "Ballot" voting2// contract from the Solidity docs. A chairperson seeds a fixed list of3// proposals and grants specific addresses the right to vote; each voter can4// either cast a direct vote or delegate their weight to another voter, and5// delegation follows a chain so a delegate who later delegates further still6// carries every accumulated weight. The winning proposal is whichever has7// the most accumulated weight at query time.8package ballot910import (11 "errors"12 "strconv"13 "strings"1415 "gno.land/p/nt/avl/v0"16)1718// proposal is one of the fixed choices set up at Init time.19type proposal struct {20 name string21 votes uint6422}2324// 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 delegated29 vote int // proposal index this voter's weight ended up on, valid once tallied30}3132// Package-level persistent state.33var (34 chairperson address35 initialized bool36 proposals []proposal37 voters *avl.Tree // address string -> *voterInfo38)3940var (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)5455func init() {56 voters = avl.NewTree()57}5859func requireInit() {60 if !initialized {61 panic(errNotInit)62 }63}6465func 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 v72}7374// Init configures the ballot exactly once: the caller becomes the75// chairperson (and is immediately given the right to vote), and76// proposalNamesCSV — a comma-separated list — seeds the fixed set of77// 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 }8283 for _, raw := range strings.Split(proposalNamesCSV, ",") {84 name := strings.TrimSpace(raw)85 if name == "" {86 continue87 }88 proposals = append(proposals, proposal{name: name})89 }90 if len(proposals) == 0 {91 panic(errNoProposals)92 }9394 chairperson = cur.Previous().Address()95 voters.Set(chairperson.String(), &voterInfo{weight: 1})96 initialized = true97}9899// GiveRightToVote grants voterAddr a voting weight of 1. Only the100// 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 }106107 addr := address(voterAddr)108 if !addr.IsValid() {109 panic(errInvalidAddr)110 }111112 v := getOrCreateVoter(addr)113 if v.voted {114 panic(errAlreadyVoted)115 }116 if v.weight != 0 {117 panic(errAlreadyRight)118 }119 v.weight = 1120}121122// Delegate hands the caller's voting weight to toAddr. If toAddr has123// 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 (or125// delegated further) later. Mirrors Solidity's delegation-chain-following126// with a loop guard.127func Delegate(cur realm, toAddr string) {128 requireInit()129130 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 }138139 to := address(toAddr)140 if !to.IsValid() {141 panic(errInvalidAddr)142 }143 if to == sender {144 panic(errSelfDelegate)145 }146147 var zero address148 maxSteps := voters.Size() + 1149 for steps := 0; ; steps++ {150 v := voters.Get(to.String())151 if v == nil {152 break153 }154 next := v.(*voterInfo).delegate155 if next == zero {156 break157 }158 to = next159 if to == sender {160 panic(errDelegateLoop)161 }162 if steps > maxSteps {163 panic(errDelegateLoop)164 }165 }166167 delegateVoter := getOrCreateVoter(to)168 if delegateVoter.weight == 0 {169 panic(errDelegateNoRight)170 }171172 from.voted = true173 from.delegate = to174175 if delegateVoter.voted {176 proposals[delegateVoter.vote].votes += from.weight177 } else {178 delegateVoter.weight += from.weight179 }180}181182// Vote casts the caller's full weight for proposals[proposalIndex].183func Vote(cur realm, proposalIndex int) {184 requireInit()185186 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 }197198 v.voted = true199 v.vote = proposalIndex200 proposals[proposalIndex].votes += v.weight201}202203// WinningProposal returns the index of the proposal with the most204// accumulated weight, breaking ties in favor of the lowest index.205func WinningProposal() int {206 winner := 0207 best := uint64(0)208 for i, p := range proposals {209 if p.votes > best {210 best = p.votes211 winner = i212 }213 }214 return winner215}216217// WinnerName returns the name of the current winning proposal, or "" if218// the ballot hasn't been initialized.219func WinnerName() string {220 if len(proposals) == 0 {221 return ""222 }223 return proposals[WinningProposal()].name224}225226func Render(path string) string {227 var b strings.Builder228 b.WriteString("# Ballot\n\n")229230 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 }236237 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 }244245 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}250Delegate(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, toAddr string)
GiveRightToVote(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, voterAddr string)
Init(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, proposalNamesCSV string)
Render(path string) string
Vote(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, proposalIndex int)
WinnerName() string
WinningProposal() int
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.