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.29 Kb · 469 lines
  1package kourt
  2import (
  3	"strings"
  4	checkpoint "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/checkpoint/v0"
  5	curve "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/curve/v0"
  6	governor "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/governor/v0"
  7	grc20votes "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/grc20votes/v0"
  8	bptree "gno.land/p/nt/bptree/v0"
  9)
 10const errStaleRealm = "kourtv2: 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("kourtv2: curveCap × emission growth bound exceeds the tally-safe supply ceiling")
 60	}
 61	if metaLifetimeEmission := metaWeeklyBudget * 151; curveCap > maxSupply-metaLifetimeEmission {
 62		panic("kourtv2: curveCap + meta's lifetime emission exceeds the tally-safe supply ceiling")
 63	}
 64	if tierFloorBps*tierRefFloorBps != tierParBps*ordAnswerXFloorBps {
 65		panic("kourtv2: 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("kourtv2: 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("kourtv2: the multiplier clamps are out of order")
 74	}
 75	if tierRefFloorBps <= ordAnswerXFloorBps {
 76		panic("kourtv2: the reference floor must exceed the answerability floor")
 77	}
 78	if tierEMAN <= 0 {
 79		panic("kourtv2: the running average's memory must be positive")
 80	}
 81	parPoints := tierParBps / 100
 82	if bondFloorConvBps < (parPoints+splitCarrot)*100 {
 83		panic("kourtv2: 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("kourtv2: dispute bond arm breaks the round-1 honest-dispute bar")
 87	}
 88	if budgetWeeklyBps*52 > 2000 {
 89		panic("kourtv2: the weekly budget exceeds the 20%/yr inflation ceiling")
 90	}
 91	if splitWinners+splitAuthor+splitAnswerer != 93 {
 92		panic("kourtv2: 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("kourtv2: 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("kourtv2: the flat slash arm leaves the base answer bond no settle-refund margin")
106	}
107	if maxAnswerBondBps() != maxBondBps {
108		panic("kourtv2: 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("kourtv2: the " + l.name + " answerability floor prices filing above winning the vote — incumbency lock by price")
121		}
122		if 50*l.deposit < filing {
123			panic("kourtv2: 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("kourtv2: 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("kourtv2: the overturn threshold must be in [5001, 10000]")
158	}
159	if p.votingBlocks < 1 || p.graceBlocks < 1 {
160		panic("kourtv2: vote and grace windows must be positive")
161	}
162	if p.minAnswerX < 1 {
163		panic("kourtv2: minAnswerX must be positive")
164	}
165	if p.minClaimDepositCC < 1 {
166		panic("kourtv2: the claim deposit must never be zero")
167	}
168	if p.escrowMinBlocks < 1 || p.escrowMaxBlocks < p.escrowMinBlocks {
169		panic("kourtv2: the escrow window is malformed")
170	}
171	if ladderWindow(p) > p.escrowMaxBlocks {
172		panic("kourtv2: the escrow ceiling cannot fit the failed-round ladder a defaulted verdict must walk")
173	}
174	if p.answerBondCapCC < 0 {
175		panic("kourtv2: answerBondCapCC must be non-negative")
176	}
177	if p.stakeOpenDelayBlocks < 0 || p.stakeOpenDelayBlocks > maxStakeOpenDelay {
178		panic("kourtv2: 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	burnedGNOT int64
231	burnKey    string
232	createdSeq uint64
233	burnH checkpoint.Series
234	priceH checkpoint.Series
235	absBudget int64
236	assocBond int64
237	ladder *ladder
238	creditRates *creditRates
239	standings *bptree.BPTree
240}
241var courts = bptree.NewBPTree32()
242func StartCourt(cur realm, slug, name string) string {
243	if !cur.IsCurrent() {
244		panic(errStaleRealm)
245	}
246	takeCourtCreationBurn(cur)
247	return startCourtUser(cur.Previous().Address(), cur.Address(), slug, name, 0)
248}
249func StartCourtP(cur realm, slug, name string, stakeOpenDelay int64) string {
250	if !cur.IsCurrent() {
251		panic(errStaleRealm)
252	}
253	takeCourtCreationBurn(cur)
254	return startCourtUser(cur.Previous().Address(), cur.Address(), slug, name, stakeOpenDelay)
255}
256func startCourtUser(admin, escrowAddr address, slug, name string, delay int64) string {
257	if slug == metaSlug {
258		panic("kourtv2: the 'meta' slug is reserved")
259	}
260	return startCourt(admin, escrowAddr, slug, name, delay)
261}
262func startCourt(admin, escrowAddr address, slug, name string, delay int64) string {
263	mustSlug(slug)
264	mustCourtName(name)
265	if courts.Has(slug) {
266		panic("kourtv2: that slug is taken")
267	}
268	p := defaultParams()
269	p.stakeOpenDelayBlocks = delay
270	coin := grc20votes.NewLedgerWithClock(name, courtSymbol(slug), coinDecimals, epochBlocks, realmClock{})
271	c := &Court{
272		id:             slug,
273		name:           name,
274		tier:           tierListed,
275		admin:          admin,
276		coin:           coin,
277		gov:            governor.New(coin, coin),
278		crv:            curve.New(curveDenom, curveCap),
279		escrow:         escrowAddr,
280		claims:         bptree.NewBPTree32(),
281		queue:          bptree.NewBPTree32(),
282		records:        bptree.NewBPTree32(),
283		standings:      bptree.NewBPTree32(),
284		locked:         bptree.NewBPTree32(),
285		voteLocks:      bptree.NewBPTree32(),
286		stakeIdx:       bptree.NewBPTree32(),
287		params:         p.mustSane(),
288		createdAt:      heightNow(),
289		curBudgetBpsFP: budgetBpsFPStart,
290	}
291	c.lastAccrual = c.createdAt
292	if slug == metaSlug {
293		c.absBudget = metaWeeklyBudget
294	}
295	c.gov.Adopt(disputeKind{}, disputeRules(c.params))
296	courts.Set(slug, c)
297	indexNewCourt(c)
298	if directoryAdmin == "" {
299		directoryAdmin = admin
300	}
301	return slug
302}
303const maxCourtDescLen = 240
304func SetCourtDesc(cur realm, courtSlug, desc string) {
305	if !cur.IsCurrent() {
306		panic(errStaleRealm)
307	}
308	who := cur.Previous().Address()
309	c := mustCourt(courtSlug)
310	requireFolderMod(c, who)
311	mustCourtDesc(desc)
312	c.desc = desc
313	emitModAct(c.id, 0, "court-desc", who)
314}
315func SetCourtImage(cur realm, courtSlug, media string) {
316	if !cur.IsCurrent() {
317		panic(errStaleRealm)
318	}
319	who := cur.Previous().Address()
320	c := mustCourt(courtSlug)
321	requireFolderMod(c, who)
322	items := parseMediaArg(media)
323	if len(items) != 1 {
324		panic("kourtv2: a court carries exactly one image")
325	}
326	if items[0].kind != mediaKindImage {
327		panic("kourtv2: a court's image is an image, not a video link")
328	}
329	c.image = &items[0]
330	emitModAct(c.id, 0, "court-image", who)
331}
332func ClearCourtImage(cur realm, courtSlug string) {
333	if !cur.IsCurrent() {
334		panic(errStaleRealm)
335	}
336	who := cur.Previous().Address()
337	c := mustCourt(courtSlug)
338	requireFolderMod(c, who)
339	c.image = nil
340	emitModAct(c.id, 0, "court-image-clear", who)
341}
342func CourtImage(courtSlug string) string {
343	c := mustCourt(courtSlug)
344	if c.image == nil {
345		return "[]"
346	}
347	return encodeMedia([]mediaItem{*c.image})
348}
349func CourtImages(slugs string) string {
350	out := strings.Builder{}
351	sent := 0
352	for _, sl := range strings.Split(slugs, ",") {
353		if sent >= seriesPageMax {
354			break
355		}
356		sl = strings.TrimSpace(sl)
357		if sl == "" {
358			continue
359		}
360		v := courts.Get(sl)
361		if v == nil {
362			continue
363		}
364		c := v.(*Court)
365		if c.image == nil {
366			continue
367		}
368		out.WriteString(sl + "\t" + encodeMedia([]mediaItem{*c.image}) + "\n")
369		sent++
370	}
371	return out.String()
372}
373func mustCourtDesc(desc string) {
374	if runeLen(desc) > maxCourtDescLen {
375		panic("kourtv2: a court description is at most 240 characters")
376	}
377	for i := 0; i < len(desc); i++ {
378		if desc[i] == '\n' || desc[i] == '\r' {
379			panic("kourtv2: a court description is a single paragraph")
380		}
381	}
382}
383func mustCourtName(name string) {
384	if runeLen(name) == 0 || runeLen(name) > maxCourtNameLen {
385		panic("kourtv2: a court name is 1..100 characters")
386	}
387}
388func CourtDesc(courtSlug string) string {
389	return courtDescFor(mustCourt(courtSlug))
390}
391func CourtStakeOpenDelay(courtSlug string) int64 {
392	return mustCourt(courtSlug).params.stakeOpenDelayBlocks
393}
394func CourtCount() int { return courts.Size() }
395func Exists(slug string) bool { return courts.Has(slug) }
396func CourtName(slug string) string { return mustCourt(slug).name }
397func CourtSymbol(slug string) string { return mustCourt(slug).coin.Symbol() }
398func CoinDecimals() int { return coinDecimals }
399func CoinSupply(slug string) int64   { return mustCourt(slug).coin.TotalSupply() }
400func CoinPrice(slug string) int64 { return mustCourt(slug).crv.Price(mustCourt(slug).minted) }
401func CourtEscrow(slug string) address { return mustCourt(slug).escrow }
402func EmittedTotal(slug string) int64 { return mustCourt(slug).emittedTotal }
403func effMinAnswerX(c *Court) int64 {
404	if c.id == metaSlug {
405		return supplyFloor(c, answerXFloorBps, c.params.minAnswerX)
406	}
407	return supplyFloor(c, ordAnswerXFloorBps, c.params.minAnswerX)
408}
409func effMinDeposit(c *Court) int64 {
410	if c.id == metaSlug {
411		return supplyFloor(c, depositFloorBps, c.params.minClaimDepositCC)
412	}
413	return supplyFloor(c, ordDepositFloorBps, c.params.minClaimDepositCC)
414}
415func maxAnswerBondBps() int64 {
416	maxRateBpsFP := 255 * (r0WeeklyBps*1_000_000 + budgetBpsFPStart) / 100
417	maxMidGrossBps := maxRateBpsFP * (deadClaimTimeout / periodBlocks) / 1_000_000
418	if arm := bondFloorConvBps * maxMidGrossBps / 10000; arm > answerBondBps {
419		return arm
420	}
421	return answerBondBps
422}
423func supplyFloor(c *Court, bps, dust int64) int64 {
424	var supply int64
425	if at := c.coin.Epoch(); at > 1 {
426		supply = c.coin.PastTotal(at - 1)
427	}
428	v := mulDiv128(supply, bps, grc20votes.Bps)
429	if v < dust {
430		v = dust
431	}
432	if lid := mulDiv128(supply, quorumSupplyBps, grc20votes.Bps) *
433		(grc20votes.Bps - 1) / (grc20votes.Bps + maxAnswerBondBps()); lid > 0 && v > lid {
434		v = lid
435	}
436	if v < 1 {
437		v = 1
438	}
439	return v
440}
441func mustCourt(slug string) *Court {
442	v := courts.Get(slug)
443	if v == nil {
444		panic("kourtv2: no such court")
445	}
446	return v.(*Court)
447}
448func courtSymbol(slug string) string { return strings.ToUpper(slug) }
449var reservedSlugs = []string{
450	"kourt", "court", "gno", "gnot", "ugnot",
451	"btc", "eth", "usdc", "usdt",
452}
453func mustSlug(slug string) {
454	if len(slug) == 0 || len(slug) > maxSlugLen {
455		panic("kourtv2: a slug is 1..11 characters")
456	}
457	for _, r := range reservedSlugs {
458		if slug == r {
459			panic("kourtv2: that slug is reserved")
460		}
461	}
462	for i := 0; i < len(slug); i++ {
463		ch := slug[i]
464		ok := (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
465		if !ok {
466			panic("kourtv2: a slug is lowercase letters and digits only")
467		}
468	}
469}