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

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}