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

court.gno

14.72 Kb · 482 lines
  1package kourtv3
  2import (
  3	"strings"
  4	checkpoint "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/checkpoint/v0"
  5	curve "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/curve/v0"
  6	governor "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/governor/v0"
  7	grc20votes "gno.land/p/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/grc20votes/v0"
  8	bptree "gno.land/p/nt/bptree/v0"
  9)
 10const errStaleRealm = "kourtv3: stale realm; call through a crossing function"
 11const (
 12	coinDecimals = 6
 13	epochBlocks  = int64(720)
 14	curveCap   = (int64(9223372036854775807) / grc20votes.Bps) / 2
 15	curveDenom = int64(1_000_000_000)
 16)
 17const (
 18	periodBlocks = int64(120_960)
 19	budgetWeeklyBps = int64(38)
 20	stepDownPeriods = int64(104)
 21	rMaxPeriods     = int64(4)
 22	tierRefFloorBps = int64(40)
 23	tierFloorBps    = int64(2500)
 24	tierCeilBps     = int64(20000)
 25	tierRefDustCC   = int64(1_000_000)
 26	tierEMAN = int64(16)
 27	tierParBps = int64(10000)
 28	splitWinners, splitAuthor, splitAnswerer, splitCarrot = int64(80), int64(8), int64(5), int64(7)
 29)
 30const (
 31	answerBondBps = int64(600)
 32	disputeBondXBps, disputeBondOfAnswerBps = int64(2000), int64(4000)
 33	maxFailedRounds                         = 3
 34	compOwnX, compOfBurnBps = int64(2), int64(8000)
 35	decideWindowBlocks = int64(120_960)
 36	carrotClampXBps  = int64(200)
 37	carrotClampMinCC = int64(1_000_000)
 38	bondFloorFlatBps = int64(450)
 39	bondFloorConvBps = int64(10700)
 40	depositFeeBps    = int64(1000)
 41	priorityNetRecord    = 3
 42	priorityColdStartN   = 3
 43	priorityWindowBlocks = int64(17_280)
 44	settleDelay      = int64(51_840)
 45	deadClaimTimeout = int64(1_451_520)
 46	metaSlug = "meta"
 47	metaWeeklyBudget = int64(100_000_000)
 48	answerXFloorBps = int64(50)
 49	depositFloorBps = int64(2)
 50	ordAnswerXFloorBps = int64(10)
 51	ordDepositFloorBps = int64(1)
 52	maxCourtNameLen = 100
 53	maxSlugLen        = 11
 54	maxStakeOpenDelay = int64(17_280)
 55)
 56func mustInvariants() {
 57	maxSupply := int64(9223372036854775807) / grc20votes.Bps
 58	if curveCap > (maxSupply/1785)*1000 {
 59		panic("kourtv3: curveCap × emission growth bound exceeds the tally-safe supply ceiling")
 60	}
 61	if metaLifetimeEmission := metaWeeklyBudget * 151; curveCap > maxSupply-metaLifetimeEmission {
 62		panic("kourtv3: curveCap + meta's lifetime emission exceeds the tally-safe supply ceiling")
 63	}
 64	if tierFloorBps*tierRefFloorBps != tierParBps*ordAnswerXFloorBps {
 65		panic("kourtv3: the multiplier's floor no longer meets the answerability floor — " +
 66			"a band of answerable claims would have a rate their size cannot move")
 67	}
 68	if tierCeilBps != 2*tierParBps {
 69		panic("kourtv3: the multiplier ceiling is no longer the old HIGH band — " +
 70			"this raises the per-claim draw the emission bounds were fitted to")
 71	}
 72	if tierFloorBps <= 0 || tierFloorBps >= tierParBps || tierParBps >= tierCeilBps {
 73		panic("kourtv3: the multiplier clamps are out of order")
 74	}
 75	if tierRefFloorBps <= ordAnswerXFloorBps {
 76		panic("kourtv3: the reference floor must exceed the answerability floor")
 77	}
 78	if tierEMAN <= 0 {
 79		panic("kourtv3: the running average's memory must be positive")
 80	}
 81	parPoints := tierParBps / 100
 82	if bondFloorConvBps < (parPoints+splitCarrot)*100 {
 83		panic("kourtv3: the slash multiple does not cover the mid draw plus the carrot — the draw cap would cut the published MID rate")
 84	}
 85	if 2*disputeBondOfAnswerBps > compOfBurnBps {
 86		panic("kourtv3: dispute bond arm breaks the round-1 honest-dispute bar")
 87	}
 88	if budgetWeeklyBps*52 > 2000 {
 89		panic("kourtv3: the weekly budget exceeds the 20%/yr inflation ceiling")
 90	}
 91	if splitWinners+splitAuthor+splitAnswerer != 93 {
 92		panic("kourtv3: the tier-scaled split must sum to 93 points")
 93	}
 94	maxRateBpsFP := 255 * (r0WeeklyBps*1_000_000 + budgetBpsFPStart) / 100
 95	maxTcWeeks := deadClaimTimeout / periodBlocks
 96	maxMidGrossBps := maxRateBpsFP * maxTcWeeks / 1_000_000
 97	if bondFloorConvBps*maxMidGrossBps/10000 <= answerBondBps {
 98		panic("kourtv3: the draw arm never reaches the base bond — the collateralization floor is inert and the max-keying prices nothing")
 99	}
100	maxBondBps := bondFloorConvBps * maxMidGrossBps / 10000
101	if maxBondBps < answerBondBps {
102		maxBondBps = answerBondBps
103	}
104	if bondFloorFlatBps*4/3 > answerBondBps {
105		panic("kourtv3: the flat slash arm leaves the base answer bond no settle-refund margin")
106	}
107	if maxAnswerBondBps() != maxBondBps {
108		panic("kourtv3: maxAnswerBondBps drifted from the deploy-time worst-case bond")
109	}
110	for _, l := range []struct {
111		name    string
112		floor   int64
113		deposit int64
114	}{
115		{"meta", answerXFloorBps, depositFloorBps},
116		{"ordinary", ordAnswerXFloorBps, ordDepositFloorBps},
117	} {
118		filing := l.floor * (grc20votes.Bps + maxBondBps) / grc20votes.Bps
119		if filing >= quorumSupplyBps {
120			panic("kourtv3: the " + l.name + " answerability floor prices filing above winning the vote — incumbency lock by price")
121		}
122		if 50*l.deposit < filing {
123			panic("kourtv3: a 50-row flood is cheaper than the self-answer escape from it (§5.3), " + l.name + " lane")
124		}
125		if l.deposit >= l.floor {
126			panic("kourtv3: the " + l.name + " deposit floor exceeds its answerability floor")
127		}
128	}
129}
130func init() { mustInvariants() }
131type Params struct {
132	disputeThresholdBps  int64
133	votingBlocks         int64
134	graceBlocks          int64
135	minAnswerX           int64
136	answerBondCapCC      int64
137	minClaimDepositCC    int64
138	escrowMinBlocks      int64
139	escrowMaxBlocks      int64
140	stakeOpenDelayBlocks int64
141}
142func defaultParams() Params {
143	return Params{
144		disputeThresholdBps:  5001,
145		votingBlocks:         120_960,
146		graceBlocks:          17_280,
147		minAnswerX:           1_000_000,
148		answerBondCapCC:      0,
149		minClaimDepositCC:    1_000_000,
150		escrowMinBlocks:      120_960,
151		escrowMaxBlocks:      362_880,
152		stakeOpenDelayBlocks: 0,
153	}
154}
155func (p Params) mustSane() Params {
156	if p.disputeThresholdBps < 5001 || p.disputeThresholdBps > 10000 {
157		panic("kourtv3: the overturn threshold must be in [5001, 10000]")
158	}
159	if p.votingBlocks < 1 || p.graceBlocks < 1 {
160		panic("kourtv3: vote and grace windows must be positive")
161	}
162	if p.minAnswerX < 1 {
163		panic("kourtv3: minAnswerX must be positive")
164	}
165	if p.minClaimDepositCC < 1 {
166		panic("kourtv3: the claim deposit must never be zero")
167	}
168	if p.escrowMinBlocks < 1 || p.escrowMaxBlocks < p.escrowMinBlocks {
169		panic("kourtv3: the escrow window is malformed")
170	}
171	if ladderWindow(p) > p.escrowMaxBlocks {
172		panic("kourtv3: the escrow ceiling cannot fit the failed-round ladder a defaulted verdict must walk")
173	}
174	if p.answerBondCapCC < 0 {
175		panic("kourtv3: answerBondCapCC must be non-negative")
176	}
177	if p.stakeOpenDelayBlocks < 0 || p.stakeOpenDelayBlocks > maxStakeOpenDelay {
178		panic("kourtv3: the polish window must be in [0, 24h]")
179	}
180	return p
181}
182type Court struct {
183	xBarAcc int64
184	id   string
185	name string
186	desc string
187	image *mediaItem
188	tier  uint8
189	admin address
190	coin *grc20votes.Ledger
191	gov  *governor.Governor
192	crv  curve.Curve
193	escrow address
194	minted int64
195	claims  *bptree.BPTree
196	nextID  uint64
197	params  Params
198	deposit int64
199	cumAccrual      int64
200	accrualRem      int64
201	curPeriodBudget int64
202	curBudgetBpsFP  int64
203	dEffBpsFP       int64
204	rateAccFP       int64
205	emaMinted       int64
206	emittedAtRoll   int64
207	lastAccrual     int64
208	periodIndex     int64
209	createdAt       int64
210	reservedTail    int64
211	juniorReserved  int64
212	seniorOwed      int64
213	seniorPaid      int64
214	emittedTotal    int64
215	queue           *bptree.BPTree
216	queueSeq        uint64
217	records         *bptree.BPTree
218	assocOut          *bptree.BPTree
219	assocIn           *bptree.BPTree
220	supOf           *bptree.BPTree
221	supBy           *bptree.BPTree
222	locked         *bptree.BPTree
223	voteLocks      *bptree.BPTree
224	stakeIdx       *bptree.BPTree
225	qualifiedCount int
226	mod *courtMod
227	csArch *checkpoint.Archive
228	stripIdx   *bptree.BPTree
229	pendingIdx *bptree.BPTree
230	oneWay bool
231	reserve      int64
232	redeemedGNOT int64
233	paidIn       int64
234	burnedGNOT int64
235	burnKey    string
236	createdSeq uint64
237	burnH checkpoint.Series
238	priceH checkpoint.Series
239	absBudget int64
240	assocBond int64
241	ladder *ladder
242	creditRates *creditRates
243	standings *bptree.BPTree
244}
245var courts = bptree.NewBPTree32()
246func StartCourt(cur realm, slug, name string) string {
247	if !cur.IsCurrent() {
248		panic(errStaleRealm)
249	}
250	takeCourtCreationBurn(cur)
251	return startCourtUser(cur.Previous().Address(), cur.Address(), slug, name, 0, false)
252}
253func StartOneWayCourt(cur realm, slug, name string) string {
254	if !cur.IsCurrent() {
255		panic(errStaleRealm)
256	}
257	takeCourtCreationBurn(cur)
258	return startCourtUser(cur.Previous().Address(), cur.Address(), slug, name, 0, true)
259}
260func StartCourtP(cur realm, slug, name string, stakeOpenDelay int64) string {
261	if !cur.IsCurrent() {
262		panic(errStaleRealm)
263	}
264	takeCourtCreationBurn(cur)
265	return startCourtUser(cur.Previous().Address(), cur.Address(), slug, name, stakeOpenDelay, false)
266}
267func startCourtUser(admin, escrowAddr address, slug, name string, delay int64, oneWay bool) string {
268	if slug == metaSlug {
269		panic("kourtv3: the 'meta' slug is reserved")
270	}
271	return startCourt(admin, escrowAddr, slug, name, delay, oneWay)
272}
273func startCourt(admin, escrowAddr address, slug, name string, delay int64, oneWay bool) string {
274	mustSlug(slug)
275	mustCourtName(name)
276	if courts.Has(slug) {
277		panic("kourtv3: that slug is taken")
278	}
279	p := defaultParams()
280	p.stakeOpenDelayBlocks = delay
281	coin := grc20votes.NewLedgerWithClock(name, courtSymbol(slug), coinDecimals, epochBlocks, realmClock{})
282	c := &Court{
283		id:             slug,
284		name:           name,
285		tier:           tierListed,
286		admin:          admin,
287		coin:           coin,
288		gov:            governor.New(coin, coin),
289		crv:            curve.New(curveDenom, curveCap),
290		escrow:         escrowAddr,
291		oneWay:         oneWay,
292		claims:         bptree.NewBPTree32(),
293		queue:          bptree.NewBPTree32(),
294		records:        bptree.NewBPTree32(),
295		standings:      bptree.NewBPTree32(),
296		locked:         bptree.NewBPTree32(),
297		voteLocks:      bptree.NewBPTree32(),
298		stakeIdx:       bptree.NewBPTree32(),
299		params:         p.mustSane(),
300		createdAt:      heightNow(),
301		curBudgetBpsFP: budgetBpsFPStart,
302	}
303	c.lastAccrual = c.createdAt
304	if slug == metaSlug {
305		c.absBudget = metaWeeklyBudget
306	}
307	c.gov.Adopt(disputeKind{}, disputeRules(c.params))
308	courts.Set(slug, c)
309	indexNewCourt(c)
310	if directoryAdmin == "" {
311		directoryAdmin = admin
312	}
313	return slug
314}
315const maxCourtDescLen = 240
316func SetCourtDesc(cur realm, courtSlug, desc string) {
317	if !cur.IsCurrent() {
318		panic(errStaleRealm)
319	}
320	who := cur.Previous().Address()
321	c := mustCourt(courtSlug)
322	requireFolderMod(c, who)
323	mustCourtDesc(desc)
324	c.desc = desc
325	emitModAct(c.id, 0, "court-desc", who)
326}
327func SetCourtImage(cur realm, courtSlug, media string) {
328	if !cur.IsCurrent() {
329		panic(errStaleRealm)
330	}
331	who := cur.Previous().Address()
332	c := mustCourt(courtSlug)
333	requireFolderMod(c, who)
334	items := parseMediaArg(media)
335	if len(items) != 1 {
336		panic("kourtv3: a court carries exactly one image")
337	}
338	if items[0].kind != mediaKindImage {
339		panic("kourtv3: a court's image is an image, not a video link")
340	}
341	c.image = &items[0]
342	emitModAct(c.id, 0, "court-image", who)
343}
344func ClearCourtImage(cur realm, courtSlug string) {
345	if !cur.IsCurrent() {
346		panic(errStaleRealm)
347	}
348	who := cur.Previous().Address()
349	c := mustCourt(courtSlug)
350	requireFolderMod(c, who)
351	c.image = nil
352	emitModAct(c.id, 0, "court-image-clear", who)
353}
354func CourtImage(courtSlug string) string {
355	c := mustCourt(courtSlug)
356	if c.image == nil {
357		return "[]"
358	}
359	return encodeMedia([]mediaItem{*c.image})
360}
361func CourtImages(slugs string) string {
362	out := strings.Builder{}
363	sent := 0
364	for _, sl := range strings.Split(slugs, ",") {
365		if sent >= seriesPageMax {
366			break
367		}
368		sl = strings.TrimSpace(sl)
369		if sl == "" {
370			continue
371		}
372		v := courts.Get(sl)
373		if v == nil {
374			continue
375		}
376		c := v.(*Court)
377		if c.image == nil {
378			continue
379		}
380		out.WriteString(sl + "\t" + encodeMedia([]mediaItem{*c.image}) + "\n")
381		sent++
382	}
383	return out.String()
384}
385func mustCourtDesc(desc string) {
386	if runeLen(desc) > maxCourtDescLen {
387		panic("kourtv3: a court description is at most 240 characters")
388	}
389	for i := 0; i < len(desc); i++ {
390		if desc[i] == '\n' || desc[i] == '\r' {
391			panic("kourtv3: a court description is a single paragraph")
392		}
393	}
394}
395func mustCourtName(name string) {
396	if runeLen(name) == 0 || runeLen(name) > maxCourtNameLen {
397		panic("kourtv3: a court name is 1..100 characters")
398	}
399}
400func CourtDesc(courtSlug string) string {
401	return courtDescFor(mustCourt(courtSlug))
402}
403func CourtStakeOpenDelay(courtSlug string) int64 {
404	return mustCourt(courtSlug).params.stakeOpenDelayBlocks
405}
406func CourtCount() int { return courts.Size() }
407func Exists(slug string) bool { return courts.Has(slug) }
408func CourtName(slug string) string { return mustCourt(slug).name }
409func CourtSymbol(slug string) string { return mustCourt(slug).coin.Symbol() }
410func CoinDecimals() int { return coinDecimals }
411func CoinSupply(slug string) int64   { return mustCourt(slug).coin.TotalSupply() }
412func CoinPrice(slug string) int64 { return mustCourt(slug).crv.Price(mustCourt(slug).minted) }
413func CourtOneWay(slug string) bool { return mustCourt(slug).oneWay }
414func CourtEscrow(slug string) address { return mustCourt(slug).escrow }
415func EmittedTotal(slug string) int64 { return mustCourt(slug).emittedTotal }
416func effMinAnswerX(c *Court) int64 {
417	if c.id == metaSlug {
418		return supplyFloor(c, answerXFloorBps, c.params.minAnswerX)
419	}
420	return supplyFloor(c, ordAnswerXFloorBps, c.params.minAnswerX)
421}
422func effMinDeposit(c *Court) int64 {
423	if c.id == metaSlug {
424		return supplyFloor(c, depositFloorBps, c.params.minClaimDepositCC)
425	}
426	return supplyFloor(c, ordDepositFloorBps, c.params.minClaimDepositCC)
427}
428func maxAnswerBondBps() int64 {
429	maxRateBpsFP := 255 * (r0WeeklyBps*1_000_000 + budgetBpsFPStart) / 100
430	maxMidGrossBps := maxRateBpsFP * (deadClaimTimeout / periodBlocks) / 1_000_000
431	if arm := bondFloorConvBps * maxMidGrossBps / 10000; arm > answerBondBps {
432		return arm
433	}
434	return answerBondBps
435}
436func supplyFloor(c *Court, bps, dust int64) int64 {
437	var supply int64
438	if at := c.coin.Epoch(); at > 1 {
439		supply = c.coin.PastTotal(at - 1)
440	}
441	v := mulDiv128(supply, bps, grc20votes.Bps)
442	if v < dust {
443		v = dust
444	}
445	if lid := mulDiv128(supply, quorumSupplyBps, grc20votes.Bps) *
446		(grc20votes.Bps - 1) / (grc20votes.Bps + maxAnswerBondBps()); lid > 0 && v > lid {
447		v = lid
448	}
449	if v < 1 {
450		v = 1
451	}
452	return v
453}
454func mustCourt(slug string) *Court {
455	v := courts.Get(slug)
456	if v == nil {
457		panic("kourtv3: no such court")
458	}
459	return v.(*Court)
460}
461func courtSymbol(slug string) string { return strings.ToUpper(slug) }
462var reservedSlugs = []string{
463	"kourt", "court", "gno", "gnot", "ugnot",
464	"btc", "eth", "usdc", "usdt",
465}
466func mustSlug(slug string) {
467	if len(slug) == 0 || len(slug) > maxSlugLen {
468		panic("kourtv3: a slug is 1..11 characters")
469	}
470	for _, r := range reservedSlugs {
471		if slug == r {
472			panic("kourtv3: that slug is reserved")
473		}
474	}
475	for i := 0; i < len(slug); i++ {
476		ch := slug[i]
477		ok := (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
478		if !ok {
479			panic("kourtv3: a slug is lowercase letters and digits only")
480		}
481	}
482}