dispute.gno
12.46 Kb · 431 lines
1package kourtv3
2import (
3 "strconv"
4 governor "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/governor/v0"
5 grc20votes "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/grc20votes/v0"
6 bptree "gno.land/p/nt/bptree/v0"
7)
8const disputeKindName = "dispute"
9const quorumSupplyBps = int64(500)
10const finalizeGraceBlocks = int64(120_960)
11type disputeKind struct{}
12func (disputeKind) Name() string { return disputeKindName }
13func (disputeKind) Describe(payload string) string {
14 return "resolve a disputed claim by a token vote (see the court page for the claim)"
15}
16func (disputeKind) Check(payload string) error { return nil }
17func (disputeKind) Do(_ int, rlm realm, payload string) error { return nil }
18func disputeRules(p Params) governor.Rules {
19 return governor.NewRules(5000, p.disputeThresholdBps, p.votingBlocks, 1, p.graceBlocks, 0)
20}
21func OpenDispute(cur realm, courtSlug string, claimID uint64) int64 {
22 if !cur.IsCurrent() {
23 panic(errStaleRealm)
24 }
25 who := cur.Previous().Address()
26 c := mustCourt(courtSlug)
27 cs := mustClaim(c, claimID)
28 if cs.frozenAt == 0 {
29 panic("kourtv3: there is no answer to dispute")
30 }
31 if who == cs.answerer {
32 panic("kourtv3: the answerer cannot dispute their own answer")
33 }
34 if cs.disputeOpen {
35 panic("kourtv3: a dispute is already open on this claim")
36 }
37 if cs.verdictAt != 0 {
38 panic("kourtv3: the verdict is final")
39 }
40 now := heightNow()
41 if cs.provisional < 0 {
42 if passed, known := pastDeadline(cs.answeredAtTime, settleSecs); (known && passed) ||
43 (!known && now >= cs.answerHeight+settleDelay) {
44 panic("kourtv3: the dispute window has passed; the answer settles undisputed")
45 }
46 } else if escrowPassed(cs) {
47 panic("kourtv3: the escrow window has passed; Finalize the verdict")
48 }
49 touch(c)
50 bond := cs.disputeBond0()
51 mustSpendable(c, who, bond)
52 c.coin.Transfer(who, c.escrow, bond)
53 floor := quorumFloor(c, cs.xBarFrozen)
54 cs.round++
55 payload := strconv.FormatUint(claimID, 10) + "|" + strconv.FormatInt(cs.round, 10)
56 pid := c.gov.ProposeWithQuorum(c.escrow, disputeKindName, payload, "dispute", floor)
57 cs.proposalID = pid
58 cs.disputeOpen = true
59 cs.disputeOpenedTime = nowTime()
60 cs.disputeBond = bond
61 cs.disputer = who
62 return pid
63}
64func VoteDispute(cur realm, courtSlug string, claimID uint64, choice string) {
65 if !cur.IsCurrent() {
66 panic(errStaleRealm)
67 }
68 who := cur.Previous().Address()
69 c := mustCourt(courtSlug)
70 cs := mustClaim(c, claimID)
71 if !cs.disputeOpen {
72 panic("kourtv3: no open dispute on this claim")
73 }
74 if isParticipant(cs, who) {
75 panic("kourtv3: a participant may not vote on their own claim's verdict")
76 }
77 held := voteCap(c, who)
78 if held <= 0 {
79 panic("kourtv3: you hold none of this court's coin, and voting weight is " +
80 "the lesser of what you held when the round opened and what you hold now")
81 }
82 govChoice := choice
83 switch choice {
84 case voteYes, voteNo:
85 case voteSpam:
86 govChoice = govAbstain
87 default:
88 panic("kourtv3: a dispute vote is \"yes\" to overturn, \"no\" to uphold, " +
89 "or \"spam\" to say the claim was not worth judging")
90 }
91 c.gov.VoteWithCap(who, cs.proposalID, govChoice, held)
92 if cs.voted == nil {
93 cs.voted = bptree.NewBPTree32()
94 }
95 cs.voted.Set("d"+beClaimKey(uint64(cs.proposalID))+string(who), choice)
96 _, dw, ok := c.gov.VoteOf(cs.proposalID, who)
97 if !ok {
98 panic("kourtv3: the governor did not record the vote it just accepted")
99 }
100 cs.voted.Set("dw"+beClaimKey(uint64(cs.proposalID))+string(who), dw)
101 lockVote(c, who, voteLockDispute, int64(claimID), cs.proposalID, dw)
102}
103func ResolveDispute(cur realm, courtSlug string, claimID uint64) {
104 if !cur.IsCurrent() {
105 panic(errStaleRealm)
106 }
107 c := mustCourt(courtSlug)
108 cs := mustClaim(c, claimID)
109 if !cs.disputeOpen {
110 panic("kourtv3: no dispute to resolve")
111 }
112 if c.gov.State(cs.proposalID) == "active" {
113 panic("kourtv3: the dispute vote is still open")
114 }
115 touch(c)
116 yes, no, spamW, _ := c.gov.Tally(cs.proposalID)
117 cast := yes + no + spamW
118 floor := c.gov.QuorumFloor(cs.proposalID)
119 firstResolution := cs.provisional < 0
120 switch {
121 case cast < floor:
122 half := cs.disputeBond / 2
123 if half > 0 {
124 c.coin.Transfer(c.escrow, cs.disputer, half)
125 }
126 if rest := cs.disputeBond - half; rest > 0 {
127 c.coin.Burn(c.escrow, rest)
128 }
129 cs.disputeBond = 0
130 if firstResolution {
131 cs.provisional = cs.answer
132 }
133 cs.failedRounds++
134 if cs.failedRounds >= maxFailedRounds {
135 provCloseClaim(c, cs)
136 }
137 case spamDiscards(spamW, cast):
138 spamDiscardClaim(c, cs, spamW, cast)
139 case yes > 0 && yes*grc20votes.Bps >= (yes+no)*c.params.disputeThresholdBps:
140 posted := cs.disputeBond
141 burned := cs.answerBond
142 if burned > 0 {
143 c.coin.Burn(c.escrow, burned)
144 cs.answerBond = 0
145 cs.overturnBy = cs.disputer
146 cs.overturnBond = posted
147 }
148 if posted > 0 {
149 c.coin.Transfer(c.escrow, cs.disputer, posted)
150 cs.disputeBond = 0
151 }
152 if comp := compAmount(posted, burned); comp > 0 {
153 enqueueSenior(c, cs.disputer, comp, "comp")
154 }
155 cs.provisional = int8(oppositeSide(int(cs.answer)))
156 cs.failedRounds = 0
157 cs.decidedRounds++
158 cs.decidedPID, cs.carrotChoice, cs.carrotDenom = cs.proposalID, "yes", yes
159 cs.spamW, cs.spamTotalW = spamW, cast
160 default:
161 burned := cs.disputeBond
162 if burned > 0 {
163 c.coin.Burn(c.escrow, burned)
164 cs.disputeBond = 0
165 }
166 if comp := compAmount(cs.answerBond0, burned); comp > 0 {
167 enqueueSenior(c, cs.answerer, comp, "comp")
168 }
169 if yes >= credWeightFloor(c, cs.proposalID)/4 {
170 cs.credEligible = true
171 }
172 cs.provisional = cs.answer
173 cs.failedRounds = 0
174 cs.decidedRounds++
175 cs.decidedPID, cs.carrotChoice, cs.carrotDenom = cs.proposalID, "no", no
176 cs.spamW, cs.spamTotalW = spamW, cast
177 }
178 if firstResolution && !cs.provClose {
179 w := escrowWindow(c, cs)
180 if cs.failedRounds > 0 {
181 if lw := ladderWindow(c.params); lw > w {
182 w = lw
183 }
184 }
185 cs.escrowUntil = heightNow() + w
186 cs.escrowUntilAt = nowTime() + blocksToSecs(w)
187 }
188 cs.disputeOpen = false
189}
190func (cs *claimState) disputeBond0() int64 {
191 base := cs.xBarFrozen * disputeBondXBps / 10000
192 if arm := cs.answerBond0 * disputeBondOfAnswerBps / 10000; arm < base {
193 base = arm
194 }
195 if base < 1 {
196 base = 1
197 }
198 shift := cs.failedRounds
199 if shift > maxFailedRounds-1 {
200 shift = maxFailedRounds - 1
201 }
202 return mustMul(base, int64(1)<<uint(shift))
203}
204func compAmount(ownBond, burned int64) int64 {
205 comp := mustMul(compOwnX, ownBond)
206 if arm := burned * compOfBurnBps / 10000; arm < comp {
207 comp = arm
208 }
209 if comp < 0 {
210 comp = 0
211 }
212 return comp
213}
214func spamDiscards(spamW, cast int64) bool { return spamW*2 > cast }
215func spamBurnsDeposit(spamW, cast int64) bool { return spamW*3 >= cast*2 }
216func spamDiscardClaim(c *Court, cs *claimState, spamW, cast int64) {
217 cs.spamClosed = true
218 cs.provClose = true
219 cs.verdictAt = heightNow()
220 cs.verdictAtTime = nowTime()
221 cs.route = "spam"
222 cs.spamW, cs.spamTotalW = spamW, cast
223 cs.decidedPID, cs.carrotChoice, cs.carrotDenom = cs.proposalID, voteSpam, spamW
224 if cs.answerBond > 0 {
225 settleAnswerBond(c, cs)
226 }
227 if cs.disputeBond > 0 {
228 c.coin.Transfer(c.escrow, cs.disputer, cs.disputeBond)
229 cs.disputeBond = 0
230 }
231 burnDeposit := spamBurnsDeposit(spamW, cast)
232 if cs.deposit > 0 {
233 if burnDeposit {
234 c.coin.Burn(c.escrow, cs.deposit)
235 } else {
236 c.coin.Transfer(c.escrow, cs.author, cs.deposit)
237 }
238 c.deposit -= cs.deposit
239 cs.deposit = 0
240 }
241 if cs.fee > 0 {
242 c.coin.Burn(c.escrow, cs.fee)
243 c.deposit -= cs.fee
244 cs.fee = 0
245 }
246 releasePriority(c, cs.answerer, cs.id)
247 stripExit(c, cs)
248 releaseMetaLatchIfMeta(c, cs)
249}
250func provCloseClaim(c *Court, cs *claimState) {
251 cs.provClose = true
252 cs.verdictAt = heightNow()
253 cs.verdictAtTime = nowTime()
254 cs.route = "closed"
255 if cs.answerBond > 0 {
256 settleAnswerBond(c, cs)
257 }
258 if cs.deposit > 0 {
259 c.coin.Transfer(c.escrow, cs.author, cs.deposit)
260 c.deposit -= cs.deposit
261 cs.deposit = 0
262 }
263 if cs.fee > 0 {
264 c.coin.Transfer(c.escrow, cs.author, cs.fee)
265 c.deposit -= cs.fee
266 cs.fee = 0
267 }
268 releasePriority(c, cs.answerer, cs.id)
269 stripExit(c, cs)
270 releaseMetaLatchIfMeta(c, cs)
271}
272func Finalize(cur realm, courtSlug string, claimID uint64) {
273 if !cur.IsCurrent() {
274 panic(errStaleRealm)
275 }
276 who := cur.Previous().Address()
277 c := mustCourt(courtSlug)
278 cs := mustClaim(c, claimID)
279 if cs.verdictAt != 0 {
280 panic("kourtv3: already final")
281 }
282 if cs.disputeOpen {
283 panic("kourtv3: a dispute is active; cannot finalize")
284 }
285 if cs.provisional < 0 {
286 panic("kourtv3: no provisional verdict to finalize")
287 }
288 now := heightNow()
289 if !escrowPassed(cs) {
290 panic("kourtv3: the escrow window has not passed")
291 }
292 if passed, known := pastDeadline(cs.escrowUntilAt, finalizeGraceSecs); ((known && !passed) ||
293 (!known && now < cs.escrowUntil+finalizeGraceBlocks)) && !isParticipant(cs, who) {
294 panic("kourtv3: Finalize is participant-only for its first week")
295 }
296 touch(c)
297 cs.verdictAt = now
298 cs.verdictAtTime = nowTime()
299 cs.route = "vote"
300 trimClaimHistory(c, cs, uint32(cs.frozenAt/epochBlocks)+1)
301 if cs.answerBond > 0 {
302 settleAnswerBond(c, cs)
303 }
304 if cs.provisional != cs.answer {
305 resetOverturned(c, cs.answerer)
306 } else if cs.decidedRounds > 0 && cs.credEligible {
307 creditUpheld(c, cs.answerer)
308 }
309 creditDisputeResolved(c, cs)
310 releasePriority(c, cs.answerer, claimID)
311}
312func VotableSupply(courtSlug string) (raw, escrowed, votable int64) {
313 c := mustCourt(courtSlug)
314 at := c.coin.Epoch()
315 if at <= 1 {
316 return 0, 0, 0
317 }
318 at--
319 raw = c.coin.PastTotal(at)
320 escrowed = c.coin.PastVotes(c.escrow, at)
321 votable = raw - escrowed
322 if votable < 0 {
323 votable = 0
324 }
325 return raw, escrowed, votable
326}
327func QuorumFloorOf(courtSlug string, claimID uint64) int64 {
328 c := mustCourt(courtSlug)
329 cs := mustClaim(c, claimID)
330 if cs.disputeOpen {
331 return c.gov.QuorumFloor(cs.proposalID)
332 }
333 return quorumFloor(c, cs.xBarFrozen)
334}
335func isParticipant(cs *claimState, who address) bool {
336 if who == cs.author || who == cs.answerer {
337 return true
338 }
339 return cs.stakers.Get(posKey(who, sideYES)) != nil ||
340 cs.stakers.Get(posKey(who, sideNO)) != nil
341}
342func quorumFloor(c *Court, xbar int64) int64 {
343 at := c.coin.Epoch() - 1
344 supply := c.coin.PastTotal(at)
345 votable := supply - c.coin.PastVotes(c.escrow, at)
346 if votable < 0 {
347 votable = 0
348 }
349 floor := xbar
350 if third := votable / 3; third < floor {
351 floor = third
352 }
353 if floor < 1 {
354 floor = 1
355 }
356 return floor
357}
358func oppositeSide(side int) int {
359 if side == sideYES {
360 return sideNO
361 }
362 return sideYES
363}
364func escrowWindow(c *Court, cs *claimState) int64 {
365 extraDays := mulDiv128(cs.xBarFrozen, c.crv.Price(c.minted), 500_000_000)
366 room := (c.params.escrowMaxBlocks - c.params.escrowMinBlocks) / oneDayBlocks
367 if extraDays >= room {
368 return c.params.escrowMaxBlocks
369 }
370 return c.params.escrowMinBlocks + extraDays*oneDayBlocks
371}
372const (
373 voteYes = "yes"
374 voteNo = "no"
375 voteSpam = "spam"
376 govAbstain = "abstain"
377)
378const oneDayBlocks = int64(24) * epochBlocks
379func DisputeOpen(courtSlug string, claimID uint64) bool {
380 return mustClaim(mustCourt(courtSlug), claimID).disputeOpen
381}
382func DisputeProposal(courtSlug string, claimID uint64) int64 {
383 return mustClaim(mustCourt(courtSlug), claimID).proposalID
384}
385func Provisional(courtSlug string, claimID uint64) int {
386 return int(mustClaim(mustCourt(courtSlug), claimID).provisional)
387}
388func FailedRounds(courtSlug string, claimID uint64) int64 {
389 return mustClaim(mustCourt(courtSlug), claimID).failedRounds
390}
391func ProvClose(courtSlug string, claimID uint64) bool {
392 return mustClaim(mustCourt(courtSlug), claimID).provClose
393}
394func EscrowUntil(courtSlug string, claimID uint64) int64 {
395 return mustClaim(mustCourt(courtSlug), claimID).escrowUntil
396}
397func DisputeVoteCloses(courtSlug string, claimID uint64) int64 {
398 c := mustCourt(courtSlug)
399 cs := mustClaim(c, claimID)
400 if !cs.disputeOpen || cs.proposalID == 0 {
401 return 0
402 }
403 _, closes, _, _ := c.gov.Timings(cs.proposalID)
404 return closes
405}
406func DisputeBondNext(courtSlug string, claimID uint64) int64 {
407 cs := mustClaim(mustCourt(courtSlug), claimID)
408 if cs.frozenAt == 0 {
409 return 0
410 }
411 return cs.disputeBond0()
412}
413func escrowPassed(cs *claimState) bool {
414 passed, known := pastDeadline(cs.escrowUntilAt, 0)
415 return (known && passed) || (!known && heightNow() >= cs.escrowUntil)
416}
417func Reopenable(courtSlug string, claimID uint64) bool {
418 cs := mustClaim(mustCourt(courtSlug), claimID)
419 return cs.provisional >= 0 && cs.verdictAt == 0 && !cs.disputeOpen &&
420 !escrowPassed(cs)
421}
422func ladderWindow(p Params) int64 {
423 return (maxFailedRounds-2)*p.votingBlocks + (maxFailedRounds-1)*p.graceBlocks + 1
424}
425func credWeightFloor(c *Court, pid int64) int64 {
426 engaged, _ := c.gov.Snapshot(pid)
427 return mulDiv128(engaged, quorumSupplyBps, grc20votes.Bps)
428}
429func credWeightFloorAt(c *Court, at uint32) int64 {
430 return mulDiv128(c.coin.EngagedTotal(at), quorumSupplyBps, grc20votes.Bps)
431}