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

boardmod.gno

11.92 Kb · 421 lines
  1package kourtv3
  2import "strconv"
  3const (
  4	freezeMaxBlocks = int64(17_280) / 2
  5	freezeGapBlocks = freezeMaxBlocks
  6)
  7func mustBoardModInvariants() {
  8	if freezeMaxBlocks >= 17_280 {
  9		panic("kourtv3: a board freeze must expire inside the nomination window")
 10	}
 11	if freezeGapBlocks < freezeMaxBlocks {
 12		panic("kourtv3: the gap between freezes must be at least one freeze long, " +
 13			"or the ceiling is a formality")
 14	}
 15}
 16func init() { mustBoardModInvariants() }
 17func speechM(cm *courtMod) int {
 18	m := cm.m
 19	if m < 2 {
 20		m = 2
 21	}
 22	if m > cm.n {
 23		m = cm.n
 24	}
 25	return m
 26}
 27func SpeechThreshold(courtSlug string) int {
 28	c := mustCourt(courtSlug)
 29	if c.mod == nil {
 30		return 0
 31	}
 32	return speechM(c.mod)
 33}
 34func boardSanctionEpochOf(c *Court) int64 {
 35	if c.mod == nil {
 36		return 0
 37	}
 38	return c.mod.boardSanctionEpoch
 39}
 40func boardFreezeActive(r *standingRow) bool {
 41	passed, known := pastDeadline(r.frozenUntilTime, 0)
 42	return (known && !passed) || (!known && heightNow() < r.frozenUntil)
 43}
 44func boardFreezeGapOpen(r *standingRow) bool {
 45	passed, known := pastDeadline(r.frozenGapUntilTime, 0)
 46	return (known && !passed) || (!known && heightNow() < r.frozenGapUntil)
 47}
 48func boardFrozen(c *Court, who address) bool {
 49	r := lookupStanding(c, who)
 50	if r == nil || r.frozenUntil == 0 {
 51		return false
 52	}
 53	if r.frozenEpoch != boardSanctionEpochOf(c) {
 54		return false
 55	}
 56	return boardFreezeActive(r)
 57}
 58func boardFrozenAt(c *Court, who address) (at, atHeight int64) {
 59	if !boardFrozen(c, who) {
 60		return 0, 0
 61	}
 62	r := lookupStanding(c, who)
 63	return r.frozenUntilTime, r.frozenUntil
 64}
 65func BoardFrozenUntil(courtSlug string, who address) (at, atHeight int64) {
 66	return boardFrozenAt(mustCourt(courtSlug), who)
 67}
 68func claimFreezeActive(clm *claimMod) bool {
 69	passed, known := pastDeadline(clm.boardFrozenUntilTime, 0)
 70	return (known && !passed) || (!known && heightNow() < clm.boardFrozenUntil)
 71}
 72func claimFreezeGapOpen(clm *claimMod) bool {
 73	passed, known := pastDeadline(clm.boardFrozenGapUntilTime, 0)
 74	return (known && !passed) || (!known && heightNow() < clm.boardFrozenGapUntil)
 75}
 76func claimBoardFrozen(c *Court, claimID uint64) bool {
 77	if c.mod == nil {
 78		return false
 79	}
 80	clm := lookupClaimMod(c.mod, claimID)
 81	if clm == nil || clm.boardFrozenUntil == 0 {
 82		return false
 83	}
 84	if clm.boardFrozenEpoch != c.mod.boardSanctionEpoch {
 85		return false
 86	}
 87	return claimFreezeActive(clm)
 88}
 89func claimBoardFrozenAt(c *Court, claimID uint64) (at, atHeight int64) {
 90	if !claimBoardFrozen(c, claimID) {
 91		return 0, 0
 92	}
 93	clm := lookupClaimMod(c.mod, claimID)
 94	return clm.boardFrozenUntilTime, clm.boardFrozenUntil
 95}
 96func ClaimBoardFrozenUntil(courtSlug string, claimID uint64) (at, atHeight int64) {
 97	return claimBoardFrozenAt(mustCourt(courtSlug), claimID)
 98}
 99func HideBoardRow(cur realm, courtSlug string, claimID, rowID uint64, code string) {
100	if !cur.IsCurrent() {
101		panic(errStaleRealm)
102	}
103	who := cur.Previous().Address()
104	c := mustCourt(courtSlug)
105	cm := ensureMod(c)
106	requireActiveMod(cm, who)
107	mustBoardCode(code)
108	cs := mustClaim(c, claimID)
109	r := mustBoardRow(cs, rowID)
110	if r.hiddenByMod {
111		panic("kourtv3: that comment is already hidden")
112	}
113	fire, _ := approveAction(cm.pending,
114		"boardhide:"+strconv.FormatUint(claimID, 10)+":"+strconv.FormatUint(rowID, 10),
115		who, code, speechM(cm))
116	if !fire {
117		return
118	}
119	r.hiddenByMod = true
120	scoreIndexDrop(cs, r)
121	ensureClaimMod(c, cm, claimID).
122		appendLog(who, boardActCode("board-hide", rowID, code), "")
123	emitModAct(c.id, claimID, "board-hide", who)
124}
125func UnhideBoardRow(cur realm, courtSlug string, claimID, rowID uint64) {
126	if !cur.IsCurrent() {
127		panic(errStaleRealm)
128	}
129	who := cur.Previous().Address()
130	c := mustCourt(courtSlug)
131	cm := ensureMod(c)
132	requireMod(cm, who)
133	cs := mustClaim(c, claimID)
134	r := mustBoardRow(cs, rowID)
135	if !r.hiddenByMod {
136		panic("kourtv3: that comment is not hidden")
137	}
138	fire, _ := approveAction(cm.pending,
139		"boardunhide:"+strconv.FormatUint(claimID, 10)+":"+strconv.FormatUint(rowID, 10),
140		who, "", speechM(cm))
141	if !fire {
142		return
143	}
144	r.hiddenByMod = false
145	scoreIndexPut(cs, r)
146	ensureClaimMod(c, cm, claimID).
147		appendLog(who, boardActCode("board-unhide", rowID, ""), "")
148	emitModAct(c.id, claimID, "board-unhide", who)
149}
150func SlashStanding(cur realm, courtSlug string, who address, levels int64, code string) {
151	if !cur.IsCurrent() {
152		panic(errStaleRealm)
153	}
154	actor := cur.Previous().Address()
155	if levels < 1 || levels > 3 {
156		panic("kourtv3: a slash is 1, 2 or 3 whole levels")
157	}
158	c := mustCourt(courtSlug)
159	cm := ensureMod(c)
160	requireActiveMod(cm, actor)
161	mustBoardCode(code)
162	if who == "" {
163		panic("kourtv3: no such address")
164	}
165	fire, voted := approveAction(cm.pending,
166		"slash:"+who.String()+":"+strconv.FormatInt(levels, 10),
167		actor, code, speechM(cm))
168	if !fire {
169		return
170	}
171	r := getStanding(c, who)
172	target := postLevel(c, who) - levels
173	if target < 0 {
174		target = 0
175	}
176	if want := levelFloor(c, target); r.score > want {
177		r.score = want
178	}
179	if target == 0 {
180		r.passHeld = false
181	}
182	cm.appendCourtLog(actor, "slash:"+strconv.FormatInt(levels, 10), voted)
183	emitModAct(c.id, 0, "slash", actor)
184}
185func levelFloor(c *Court, lv int64) int64 {
186	if lv <= 0 {
187		return 0
188	}
189	l := ladderFor(c)
190	sup, _ := boardSupply(c)
191	switch lv {
192	case 1:
193		return sup * l.t1Bps / 10_000
194	case 2:
195		return sup * l.t2Bps / 10_000
196	}
197	return sup * l.t3Bps / 10_000
198}
199func RestoreStanding(cur realm, courtSlug string, who address) int64 {
200	if !cur.IsCurrent() {
201		panic(errStaleRealm)
202	}
203	actor := cur.Previous().Address()
204	c := mustCourt(courtSlug)
205	cm := ensureMod(c)
206	requireMod(cm, actor)
207	r := lookupStanding(c, who)
208	if r == nil || r.score >= r.highWater {
209		panic("kourtv3: that address has nothing to restore")
210	}
211	fire, _ := approveAction(cm.pending, "restore:"+who.String(), actor, "",
212		speechM(cm))
213	if !fire {
214		return 0
215	}
216	r.score = r.highWater
217	cm.appendCourtLog(actor, "restore", "")
218	emitModAct(c.id, 0, "restore", actor)
219	return r.score
220}
221func FreezeBoard(cur realm, courtSlug string, who address, blocks int64, code string) {
222	if !cur.IsCurrent() {
223		panic(errStaleRealm)
224	}
225	actor := cur.Previous().Address()
226	if blocks < 1 || blocks > freezeMaxBlocks {
227		panic("kourtv3: a board freeze is at most freezeMaxBlocks long")
228	}
229	c := mustCourt(courtSlug)
230	cm := ensureMod(c)
231	requireActiveMod(cm, actor)
232	mustBoardCode(code)
233	if who == "" {
234		panic("kourtv3: no such address")
235	}
236	now := heightNow()
237	if r := lookupStanding(c, who); r != nil && boardFreezeGapOpen(r) {
238		panic("kourtv3: this address was frozen too recently; the ceiling is not a formality")
239	}
240	fire, voted := approveAction(cm.pending, "freeze:"+who.String(), actor, code,
241		speechM(cm))
242	if !fire {
243		return
244	}
245	r := getStanding(c, who)
246	r.frozenUntil = now + blocks
247	r.frozenUntilTime = nowTime() + blocksToSecs(blocks)
248	r.frozenEpoch = boardSanctionEpochOf(c)
249	r.frozenGapUntil = now + freezeMaxBlocks + freezeGapBlocks
250	r.frozenGapUntilTime = nowTime() + blocksToSecs(freezeMaxBlocks+freezeGapBlocks)
251	cm.appendCourtLog(actor, "freeze:"+strconv.FormatInt(blocks, 10), voted)
252	emitModAct(c.id, 0, "freeze", actor)
253}
254func UnfreezeBoard(cur realm, courtSlug string, who address) {
255	if !cur.IsCurrent() {
256		panic(errStaleRealm)
257	}
258	actor := cur.Previous().Address()
259	c := mustCourt(courtSlug)
260	cm := ensureMod(c)
261	requireMod(cm, actor)
262	if !boardFrozen(c, who) {
263		panic("kourtv3: that address is not frozen")
264	}
265	fire, _ := approveAction(cm.pending, "unfreeze:"+who.String(), actor, "",
266		speechM(cm))
267	if !fire {
268		return
269	}
270	lifted := lookupStanding(c, who)
271	lifted.frozenUntil = 0
272	lifted.frozenUntilTime = 0
273	cm.appendCourtLog(actor, "unfreeze", "")
274	emitModAct(c.id, 0, "unfreeze", actor)
275}
276func FreezeClaimBoard(cur realm, courtSlug string, claimID uint64, blocks int64, code string) {
277	if !cur.IsCurrent() {
278		panic(errStaleRealm)
279	}
280	actor := cur.Previous().Address()
281	if blocks < 1 || blocks > freezeMaxBlocks {
282		panic("kourtv3: a board freeze is at most freezeMaxBlocks long")
283	}
284	c := mustCourt(courtSlug)
285	cm := ensureMod(c)
286	requireActiveMod(cm, actor)
287	mustBoardCode(code)
288	_ = mustClaim(c, claimID)
289	now := heightNow()
290	if prev := lookupClaimMod(cm, claimID); prev != nil && claimFreezeGapOpen(prev) {
291		panic("kourtv3: this board was frozen too recently; the ceiling is not a formality")
292	}
293	fire, _ := approveAction(cm.pending,
294		"freezeboard:"+strconv.FormatUint(claimID, 10), actor, code, speechM(cm))
295	if !fire {
296		return
297	}
298	clm := ensureClaimMod(c, cm, claimID)
299	clm.boardFrozenUntil = now + blocks
300	clm.boardFrozenUntilTime = nowTime() + blocksToSecs(blocks)
301	clm.boardFrozenEpoch = cm.boardSanctionEpoch
302	clm.boardFrozenGapUntil = now + freezeMaxBlocks + freezeGapBlocks
303	clm.boardFrozenGapUntilTime = nowTime() + blocksToSecs(freezeMaxBlocks+freezeGapBlocks)
304	clm.appendLog(actor, "freezeboard:"+strconv.FormatInt(blocks, 10)+":"+code, "")
305	emitModAct(c.id, claimID, "freezeboard", actor)
306}
307func ThawClaimBoard(cur realm, courtSlug string, claimID uint64) {
308	if !cur.IsCurrent() {
309		panic(errStaleRealm)
310	}
311	actor := cur.Previous().Address()
312	c := mustCourt(courtSlug)
313	cm := ensureMod(c)
314	requireMod(cm, actor)
315	if !claimBoardFrozen(c, claimID) {
316		panic("kourtv3: that board is not frozen")
317	}
318	fire, _ := approveAction(cm.pending,
319		"thawboard:"+strconv.FormatUint(claimID, 10), actor, "", speechM(cm))
320	if !fire {
321		return
322	}
323	thawed := lookupClaimMod(cm, claimID)
324	thawed.boardFrozenUntil = 0
325	thawed.boardFrozenUntilTime = 0
326	ensureClaimMod(c, cm, claimID).appendLog(actor, "thawboard", "")
327	emitModAct(c.id, claimID, "thawboard", actor)
328}
329func GlobalClearBoardBits(cur realm, courtSlug string) int64 {
330	if !cur.IsCurrent() {
331		panic(errStaleRealm)
332	}
333	who := cur.Previous().Address()
334	if !isGlobalMod(who) {
335		panic("kourtv3: only a global DAO member may clear board bits (recovery)")
336	}
337	c := mustCourt(courtSlug)
338	cm := ensureMod(c)
339	cm.boardSanctionEpoch++
340	cm.appendCourtLog(who, "global-clear-board-bits", "")
341	emitGlobalAct(c.id, 0, "clear-board-bits", who)
342	return cm.boardSanctionEpoch
343}
344func GlobalRestoreStanding(cur realm, courtSlug string, who address) int64 {
345	if !cur.IsCurrent() {
346		panic(errStaleRealm)
347	}
348	actor := cur.Previous().Address()
349	if !isGlobalMod(actor) {
350		panic("kourtv3: only a global DAO member may restore standing (recovery)")
351	}
352	c := mustCourt(courtSlug)
353	r := lookupStanding(c, who)
354	if r == nil || r.score >= r.highWater {
355		panic("kourtv3: that address has nothing to restore")
356	}
357	r.score = r.highWater
358	ensureMod(c).appendCourtLog(actor, "global-restore-standing", "")
359	emitGlobalAct(c.id, 0, "restore-standing", actor)
360	return r.score
361}
362func mustBoardWritable(c *Court, cs *claimState, who address) {
363	if claimBoardFrozen(c, cs.id) {
364		panic("kourtv3: this claim's board is frozen by moderators")
365	}
366	if boardFrozen(c, who) {
367		panic("kourtv3: you are frozen out of this court's boards")
368	}
369}
370func PurgeCourtLogRow(cur realm, courtSlug string, rowID uint64, categoryCode string) {
371	if !cur.IsCurrent() {
372		panic(errStaleRealm)
373	}
374	who := cur.Previous().Address()
375	d := ensureGlobalDAO()
376	if !d.members.Has(who.String()) {
377		panic("kourtv3: only a global DAO member may purge")
378	}
379	mustCategoryCode(categoryCode)
380	c := mustCourt(courtSlug)
381	cm := ensureMod(c)
382	idx := -1
383	for i := range cm.log {
384		if cm.log[i].rowID == rowID {
385			idx = i
386			break
387		}
388	}
389	if idx < 0 {
390		panic("kourtv3: no such court log row")
391	}
392	if cm.log[idx].purged {
393		return
394	}
395	fire, code := approveAction(d.pending,
396		"purgecourtrow:"+c.id+":"+strconv.FormatUint(rowID, 10),
397		who, categoryCode, d.purgeM)
398	if !fire {
399		return
400	}
401	cm.log[idx].purged = true
402	cm.log[idx].reason = ""
403	cm.appendCourtLog(who, "purgecourtrow:"+code, "")
404	emitPurge(c.id, 0, code, who)
405}
406func GlobalClearCourtParams(cur realm, courtSlug string) {
407	if !cur.IsCurrent() {
408		panic(errStaleRealm)
409	}
410	who := cur.Previous().Address()
411	if !isGlobalMod(who) {
412		panic("kourtv3: only a global DAO member may clear court parameters (recovery)")
413	}
414	c := mustCourt(courtSlug)
415	if c.ladder == nil && c.creditRates == nil {
416		panic("kourtv3: this court has no parameter overrides")
417	}
418	c.ladder, c.creditRates = nil, nil
419	ensureMod(c).appendCourtLog(who, "global-clear-params", "")
420	emitGlobalAct(c.id, 0, "clear-court-params", who)
421}