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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

modvote.gno

11.23 Kb · 421 lines
  1package kourt
  2import (
  3	"chain"
  4	"strconv"
  5	grc20votes "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/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("kourtv2: the election bond must be positive")
 16	}
 17	if electionBondBps >= quorumSupplyBps {
 18		panic("kourtv2: 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("kourtv2: 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("kourtv2: a candidate set may not be empty")
 91	}
 92	if m < 1 || m > len(canon) {
 93		panic("kourtv2: 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("kourtv2: a moderator set is at most 32 members")
108	}
109	out := []address{}
110	for _, a := range in {
111		if a == "" {
112			panic("kourtv2: a moderator address may not be zero")
113		}
114		if !a.IsValid() {
115			panic("kourtv2: 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("kourtv2: no such candidate")
140	}
141	v := cm.candidates.Get(beClaimKey(id))
142	if v == nil {
143		panic("kourtv2: 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("kourtv2: an election is already open for this court")
168	}
169	now := heightNow()
170	if electionCooldownOpen(cm) {
171		panic("kourtv2: 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("kourtv2: 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("kourtv2: this ballot is full; open the next election instead")
212	}
213	if e.nominators.Has(who.String()) {
214		panic("kourtv2: one nomination per address per election")
215	}
216	if e.lines.Has(beClaimKey(cand.id)) {
217		panic("kourtv2: 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("kourtv2: 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("kourtv2: voting opens when the nomination window closes")
250	}
251	if votingClosed(e) {
252		panic("kourtv2: 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("kourtv2: no voting weight at the pinned epoch")
262		}
263		if w <= 0 {
264			panic("kourtv2: 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("kourtv2: already approved that line")
273	}
274	if !retain {
275		v := e.lines.Get(beClaimKey(candidateID))
276		if v == nil {
277			panic("kourtv2: 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("kourtv2: 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("kourtv2: 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}