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.

dispute.gno

12.45 Kb · 431 lines
  1package kourt
  2import (
  3	"strconv"
  4	governor "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/governor/v0"
  5	grc20votes "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/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("kourtv2: there is no answer to dispute")
 30	}
 31	if who == cs.answerer {
 32		panic("kourtv2: the answerer cannot dispute their own answer")
 33	}
 34	if cs.disputeOpen {
 35		panic("kourtv2: a dispute is already open on this claim")
 36	}
 37	if cs.verdictAt != 0 {
 38		panic("kourtv2: 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("kourtv2: the dispute window has passed; the answer settles undisputed")
 45		}
 46	} else if escrowPassed(cs) {
 47		panic("kourtv2: 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("kourtv2: no open dispute on this claim")
 73	}
 74	if isParticipant(cs, who) {
 75		panic("kourtv2: a participant may not vote on their own claim's verdict")
 76	}
 77	held := voteCap(c, who)
 78	if held <= 0 {
 79		panic("kourtv2: 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("kourtv2: 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("kourtv2: 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("kourtv2: no dispute to resolve")
111	}
112	if c.gov.State(cs.proposalID) == "active" {
113		panic("kourtv2: 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("kourtv2: already final")
281	}
282	if cs.disputeOpen {
283		panic("kourtv2: a dispute is active; cannot finalize")
284	}
285	if cs.provisional < 0 {
286		panic("kourtv2: no provisional verdict to finalize")
287	}
288	now := heightNow()
289	if !escrowPassed(cs) {
290		panic("kourtv2: 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("kourtv2: 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}