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

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}