modvote.gno
11.23 Kb · 421 lines
1package kourtv3
2import (
3 "chain"
4 "strconv"
5 grc20votes "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/grc20votes/v0"
6 bptree "gno.land/p/nt/bptree/v0"
7)
8const (
9 nominationWindow = int64(17_280)
10 maxBallotLines = 64
11 electionBondBps = int64(50)
12)
13func mustElectionInvariants() {
14 if electionBondBps <= 0 {
15 panic("kourtv3: the election bond must be positive")
16 }
17 if electionBondBps >= quorumSupplyBps {
18 panic("kourtv3: the election bond must stay below the quorum floor — else affording the ballot is harder than winning the vote (incumbency lock by price)")
19 }
20 if nominationWindow < 17_280 {
21 panic("kourtv3: the nomination window must be at least a day")
22 }
23}
24func init() { mustElectionInvariants() }
25type modCandidate struct {
26 id uint64
27 members []address
28 m int
29 by address
30 at int64
31}
32type ballotLine struct {
33 candID uint64
34 weight int64
35 nomAt int64
36 bond int64
37 poster address
38}
39type election struct {
40 seq uint64
41 epoch uint32
42 openedAt int64
43 nominateEnd int64
44 voteEnd int64
45 nominateEndTime int64
46 voteEndTime int64
47 floor int64
48 bond int64
49 lines *bptree.BPTree
50 retainW int64
51 nominators *bptree.BPTree
52 voters *bptree.BPTree
53 approvals *bptree.BPTree
54 turnout int64
55 resolved bool
56}
57func electionFloor(c *Court, at uint32) int64 {
58 f := mulDiv128(votableAt(c, at), quorumSupplyBps, grc20votes.Bps)
59 if f < 1 {
60 f = 1
61 }
62 return f
63}
64func votableAt(c *Court, at uint32) int64 {
65 v := c.coin.PastTotal(at) - c.coin.PastVotes(c.escrow, at)
66 if v < 0 {
67 v = 0
68 }
69 return v
70}
71func electionBond(c *Court, at uint32) int64 {
72 b := mulDiv128(votableAt(c, at), electionBondBps, grc20votes.Bps)
73 if b < carrotClampMinCC {
74 b = carrotClampMinCC
75 }
76 if f := electionFloor(c, at); b > f {
77 b = f
78 }
79 return b
80}
81func RegisterModCandidate(cur realm, courtSlug string, m int, members ...address) uint64 {
82 if !cur.IsCurrent() {
83 panic(errStaleRealm)
84 }
85 who := cur.Previous().Address()
86 c := mustCourt(courtSlug)
87 cm := ensureMod(c)
88 canon := canonicalMembers(members)
89 if len(canon) == 0 {
90 panic("kourtv3: a candidate set may not be empty")
91 }
92 if m < 1 || m > len(canon) {
93 panic("kourtv3: the m-of-n threshold must be in 1..n")
94 }
95 if cm.candidates == nil {
96 cm.candidates = bptree.NewBPTree32()
97 }
98 cm.candSeq++
99 id := cm.candSeq
100 cm.candidates.Set(beClaimKey(id), &modCandidate{
101 id: id, members: canon, m: m, by: who, at: heightNow(),
102 })
103 return id
104}
105func canonicalMembers(in []address) []address {
106 if len(in) > maxModSetSize {
107 panic("kourtv3: a moderator set is at most 32 members")
108 }
109 out := []address{}
110 for _, a := range in {
111 if a == "" {
112 panic("kourtv3: a moderator address may not be zero")
113 }
114 if !a.IsValid() {
115 panic("kourtv3: not a valid address for a moderator set")
116 }
117 dup := false
118 for _, b := range out {
119 if b == a {
120 dup = true
121 break
122 }
123 }
124 if dup {
125 continue
126 }
127 pos := len(out)
128 out = append(out, a)
129 for pos > 0 && out[pos-1].String() > a.String() {
130 out[pos] = out[pos-1]
131 pos--
132 }
133 out[pos] = a
134 }
135 return out
136}
137func (cm *courtMod) mustCandidate(id uint64) *modCandidate {
138 if cm.candidates == nil {
139 panic("kourtv3: no such candidate")
140 }
141 v := cm.candidates.Get(beClaimKey(id))
142 if v == nil {
143 panic("kourtv3: no such candidate")
144 }
145 return v.(*modCandidate)
146}
147func electionCooldownOpen(cm *courtMod) bool {
148 passed, known := pastDeadline(cm.electionCooldownUntilTime, 0)
149 return (known && !passed) || (!known && heightNow() < cm.electionCooldownUntil)
150}
151func nominationClosed(e *election) bool {
152 passed, known := pastDeadline(e.nominateEndTime, 0)
153 return (known && passed) || (!known && heightNow() >= e.nominateEnd)
154}
155func votingClosed(e *election) bool {
156 passed, known := pastDeadline(e.voteEndTime, 0)
157 return (known && passed) || (!known && heightNow() >= e.voteEnd)
158}
159func OpenElection(cur realm, courtSlug string, candidateID uint64) uint64 {
160 if !cur.IsCurrent() {
161 panic(errStaleRealm)
162 }
163 who := cur.Previous().Address()
164 c := mustCourt(courtSlug)
165 cm := ensureMod(c)
166 if cm.election != nil && !cm.election.resolved {
167 panic("kourtv3: an election is already open for this court")
168 }
169 now := heightNow()
170 if electionCooldownOpen(cm) {
171 panic("kourtv3: this court is in its post-election cooldown")
172 }
173 cand := cm.mustCandidate(candidateID)
174 at := c.coin.Epoch() - 1
175 cm.electionSeq++
176 e := &election{
177 seq: cm.electionSeq,
178 epoch: at,
179 openedAt: now,
180 nominateEnd: now + nominationWindow,
181 voteEnd: now + nominationWindow + c.params.votingBlocks,
182 nominateEndTime: nowTime() + blocksToSecs(nominationWindow),
183 voteEndTime: nowTime() + blocksToSecs(nominationWindow+c.params.votingBlocks),
184 floor: electionFloor(c, at),
185 bond: electionBond(c, at),
186 lines: bptree.NewBPTree32(),
187 nominators: bptree.NewBPTree32(),
188 voters: bptree.NewBPTree32(),
189 approvals: bptree.NewBPTree32(),
190 }
191 cm.election = e
192 addNomination(c, cm, e, cand, who)
193 return e.seq
194}
195func NominateCandidate(cur realm, courtSlug string, candidateID uint64) {
196 if !cur.IsCurrent() {
197 panic(errStaleRealm)
198 }
199 who := cur.Previous().Address()
200 c := mustCourt(courtSlug)
201 cm := ensureMod(c)
202 e := cm.mustOpenElection()
203 if nominationClosed(e) {
204 panic("kourtv3: the nomination window has closed")
205 }
206 cand := cm.mustCandidate(candidateID)
207 addNomination(c, cm, e, cand, who)
208}
209func addNomination(c *Court, cm *courtMod, e *election, cand *modCandidate, who address) {
210 if e.lines.Size() >= maxBallotLines {
211 panic("kourtv3: this ballot is full; open the next election instead")
212 }
213 if e.nominators.Has(who.String()) {
214 panic("kourtv3: one nomination per address per election")
215 }
216 if e.lines.Has(beClaimKey(cand.id)) {
217 panic("kourtv3: that candidate is already on the ballot")
218 }
219 mustSpendable(c, who, e.bond)
220 c.coin.Transfer(who, c.escrow, e.bond)
221 e.nominators.Set(who.String(), true)
222 e.lines.Set(beClaimKey(cand.id), &ballotLine{
223 candID: cand.id, nomAt: heightNow(), bond: e.bond, poster: who,
224 })
225}
226func (cm *courtMod) mustOpenElection() *election {
227 if cm.election == nil || cm.election.resolved {
228 panic("kourtv3: no election is open")
229 }
230 return cm.election
231}
232func ApproveCandidate(cur realm, courtSlug string, candidateID uint64) {
233 if !cur.IsCurrent() {
234 panic(errStaleRealm)
235 }
236 approve(cur.Previous().Address(), courtSlug, candidateID, false)
237}
238func ApproveRetain(cur realm, courtSlug string) {
239 if !cur.IsCurrent() {
240 panic(errStaleRealm)
241 }
242 approve(cur.Previous().Address(), courtSlug, 0, true)
243}
244func approve(who address, courtSlug string, candidateID uint64, retain bool) {
245 c := mustCourt(courtSlug)
246 cm := ensureMod(c)
247 e := cm.mustOpenElection()
248 if !nominationClosed(e) {
249 panic("kourtv3: voting opens when the nomination window closes")
250 }
251 if votingClosed(e) {
252 panic("kourtv3: voting has closed")
253 }
254 var w int64
255 if prior := e.voters.Get(who.String()); prior != nil {
256 w = prior.(int64)
257 } else {
258 var snapshot int64
259 w, snapshot = votingWeight(c, who, e.epoch)
260 if snapshot <= 0 {
261 panic("kourtv3: no voting weight at the pinned epoch")
262 }
263 if w <= 0 {
264 panic("kourtv3: you no longer hold the coin you would vote with")
265 }
266 }
267 key := beClaimKey(candidateID) + who.String()
268 if retain {
269 key = "retain" + who.String()
270 }
271 if e.approvals.Has(key) {
272 panic("kourtv3: already approved that line")
273 }
274 if !retain {
275 v := e.lines.Get(beClaimKey(candidateID))
276 if v == nil {
277 panic("kourtv3: that candidate is not on the ballot")
278 }
279 line := v.(*ballotLine)
280 line.weight += w
281 } else {
282 e.retainW += w
283 }
284 e.approvals.Set(key, true)
285 if !e.voters.Has(who.String()) {
286 e.voters.Set(who.String(), w)
287 e.turnout += w
288 lockVote(c, who, voteLockElection, int64(e.seq), 0, w)
289 }
290}
291func ResolveElection(cur realm, courtSlug string) {
292 if !cur.IsCurrent() {
293 panic(errStaleRealm)
294 }
295 c := mustCourt(courtSlug)
296 cm := ensureMod(c)
297 e := cm.mustOpenElection()
298 now := heightNow()
299 if !votingClosed(e) {
300 panic("kourtv3: the voting window has not closed")
301 }
302 e.resolved = true
303 maxW := int64(0)
304 e.lines.Iterate("", "", func(_ string, v any) bool {
305 if l, ok := v.(*ballotLine); ok && l.weight > maxW {
306 maxW = l.weight
307 }
308 return false
309 })
310 install := maxW > 0 && e.turnout >= e.floor && maxW-e.retainW >= e.floor
311 var winner *ballotLine
312 if install {
313 e.lines.Iterate("", "", func(_ string, v any) bool {
314 l, ok := v.(*ballotLine)
315 if !ok || l.weight < maxW-e.floor || l.weight-e.retainW < e.floor {
316 return false
317 }
318 if winner == nil || l.nomAt < winner.nomAt ||
319 (l.nomAt == winner.nomAt && l.candID < winner.candID) {
320 winner = l
321 }
322 return false
323 })
324 }
325 e.lines.Iterate("", "", func(_ string, v any) bool {
326 l, ok := v.(*ballotLine)
327 if !ok || l.bond <= 0 {
328 return false
329 }
330 if winner != nil && l.candID == winner.candID {
331 c.coin.Transfer(c.escrow, l.poster, l.bond)
332 } else {
333 half := l.bond / 2
334 if half > 0 {
335 c.coin.Transfer(c.escrow, l.poster, half)
336 }
337 if burn := l.bond - half; burn > 0 {
338 c.coin.Burn(c.escrow, burn)
339 }
340 }
341 l.bond = 0
342 return false
343 })
344 if winner != nil {
345 cand := cm.mustCandidate(winner.candID)
346 installModSet(c, cm, cand, now, true)
347 chain.Emit("ModSetInstalled",
348 "court", c.id, "by", "election",
349 "candidate", strconv.FormatUint(cand.id, 10),
350 "height", strconv.FormatInt(now, 10),
351 )
352 return
353 }
354 cm.electionCooldownUntil = now + decideWindowBlocks
355 cm.electionCooldownUntilTime = nowTime() + blocksToSecs(decideWindowBlocks)
356}
357func installModSet(c *Court, cm *courtMod, cand *modCandidate, at int64, byElection bool) {
358 fresh := bptree.NewBPTree32()
359 for _, a := range cand.members {
360 fresh.Set(a.String(), true)
361 }
362 cm.members = fresh
363 cm.n = len(cand.members)
364 cm.m = cand.m
365 clearPendingOnMembershipChange(&cm.pending)
366 cm.setActHeight = at
367 cm.installedByMeta = !byElection
368 if currentSetID(cm) != cm.suspendedSetID || cm.m > cm.suspendedM {
369 cm.suspended = false
370 cm.suspendedSetID = ""
371 cm.suspendedM = 0
372 }
373 if byElection {
374 cm.lastElectionAt = at
375 cm.creatorUnseated = true
376 }
377}
378func mustModRead(courtSlug string) *courtMod {
379 c := mustCourt(courtSlug)
380 if c.mod == nil {
381 panic("kourtv3: this court has no moderation state yet")
382 }
383 return c.mod
384}
385func ElectionOpen(courtSlug string) bool {
386 c := mustCourt(courtSlug)
387 return c.mod != nil && c.mod.election != nil && !c.mod.election.resolved
388}
389func ElectionWindows(courtSlug string) (int64, int64) {
390 e := mustModRead(courtSlug).mustOpenElection()
391 return e.nominateEnd, e.voteEnd
392}
393func ElectionFloorOf(courtSlug string) int64 {
394 return mustModRead(courtSlug).mustOpenElection().floor
395}
396func ElectionBondOf(courtSlug string) int64 {
397 return mustModRead(courtSlug).mustOpenElection().bond
398}
399func ElectionTally(courtSlug string, candidateID uint64) (candW, retainW, turnout int64) {
400 e := mustModRead(courtSlug).mustOpenElection()
401 if v := e.lines.Get(beClaimKey(candidateID)); v != nil {
402 candW = v.(*ballotLine).weight
403 }
404 return candW, e.retainW, e.turnout
405}
406func CandidateMembers(courtSlug string, candidateID uint64) []address {
407 cand := mustModRead(courtSlug).mustCandidate(candidateID)
408 out := make([]address, len(cand.members))
409 copy(out, cand.members)
410 return out
411}
412func CandidateThreshold(courtSlug string, candidateID uint64) int {
413 return mustModRead(courtSlug).mustCandidate(candidateID).m
414}
415func ElectionCooldownUntil(courtSlug string) int64 {
416 c := mustCourt(courtSlug)
417 if c.mod == nil {
418 return 0
419 }
420 return c.mod.electionCooldownUntil
421}