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}