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}