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}