Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

pado.gno

16.86 Kb · 693 lines
  1// Package pado is a launchpad template with no trade tax.
  2//
  3// One deployment is one realm and one WUGNOT purse. A creator copies this
  4// package into their own namespace with MsgAddPackage (gno.land/r/<creator>/pado).
  5// That purse is not padv3 and not any other creator's pad. This package cannot
  6// spawn those realms itself.
  7//
  8// Inside one realm, each token has its own raise target and its own quote
  9// reserve. Sell and SwapSell pay only from that token's reserve. The bank
 10// balance is still a single number; the books are what keep launches apart.
 11//
 12// Create locks raiseUgnot. Buy and Sell take no fee. At the target, Graduate
 13// moves that token's quote into an internal pool in this same realm. There is
 14// no Gnoswap list and no zDex list, and the owner cannot withdraw the raise.
 15//
 16// Payment: the user wugnot.Transfer's to this realm, then calls Buy or SwapBuy
 17// with the same amount. A partial fill leaves the rest as that caller's prepaid
 18// credit (ClaimWugnot). Pass the amount you transferred. Coins that sit here
 19// without a Buy can be locked by the next Buy caller, same as padv3.
 20package pado
 21
 22import (
 23	"chain"
 24	"chain/runtime"
 25	"chain/runtime/unsafe"
 26	"strconv"
 27	"strings"
 28
 29	"gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/pado/ammmath"
 30	"gno.land/p/nt/avl/v0"
 31	"gno.land/p/nt/grc20/v0"
 32	"gno.land/p/nt/seqid/v0"
 33	"gno.land/r/gnoland/wugnot"
 34)
 35
 36var (
 37	launches    avl.Tree // id -> *launch
 38	bySymbol    avl.Tree // symbol -> id
 39	nextID      seqid.ID
 40	nextTokenID seqid.ID
 41
 42	padAddr address
 43	owner   address
 44
 45	prepaid      avl.Tree // caller -> int64 claimable WUGNOT
 46	totalPrepaid int64
 47
 48	// testSkipBanker is set only by resetState in tests. Production stays false.
 49	testSkipBanker bool
 50	testBank       int64
 51)
 52
 53func init(cur realm) {
 54	padAddr = cur.Address()
 55	caller := unsafe.OriginCaller()
 56	if caller.IsValid() {
 57		owner = caller
 58	}
 59}
 60
 61// Init sets the owner when deploy did not capture one. One shot.
 62func Init(cur realm) {
 63	if !cur.Previous().IsUserCall() {
 64		panic("pado: must be EOA MsgCall")
 65	}
 66	if owner.IsValid() {
 67		panic("pado: owner already set")
 68	}
 69	owner = cur.Previous().Address()
 70	chain.Emit("Owner", "owner", owner.String())
 71}
 72
 73// TransferAdmin rotates the launchpad owner. The raise stays in the realm.
 74func TransferAdmin(cur realm, next address) {
 75	requireOwner(cur)
 76	if !next.IsValid() {
 77		panic("pado: bad owner")
 78	}
 79	owner = next
 80	chain.Emit("Owner", "owner", owner.String())
 81}
 82
 83// Create opens one token. raiseUgnot is the quote this curve locks before
 84// graduate, in ugnot. Only the realm owner may call. The value is fixed here.
 85func Create(cur realm, name, symbol, uri string, raiseUgnot int64) string {
 86	requireOwner(cur)
 87	name = strings.TrimSpace(name)
 88	symbol = strings.TrimSpace(symbol)
 89	if len(uri) > MaxURILen {
 90		panic("pado: uri too long")
 91	}
 92	if strings.Contains(uri, "\n") || strings.Contains(uri, "\r") {
 93		panic("pado: bad uri")
 94	}
 95	if bySymbol.Has(symbol) {
 96		panic("pado: symbol taken")
 97	}
 98	vu0, target := seedForRaise(raiseUgnot)
 99	id := formatID(nextID.Next())
100	token, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), cur)
101	l := &launch{
102		id:             id,
103		name:           name,
104		symbol:         symbol,
105		uri:            uri,
106		creator:        cur.Previous().Address(),
107		status:         StatusCurve,
108		created:        runtime.ChainHeight(),
109		requestedRaise: raiseUgnot,
110		raiseUgnot:     target,
111		virtualUgnot0:  vu0,
112		virtualUgnot:   vu0,
113		virtualToken:   VirtualToken0,
114		token:          token,
115		ledger:         ledger,
116	}
117	launches.Set(id, l)
118	bySymbol.Set(symbol, id)
119	chain.Emit("Create",
120		"id", id,
121		"symbol", symbol,
122		"creator", l.creator.String(),
123		"requested", strconv.FormatInt(raiseUgnot, 10),
124		"raise", strconv.FormatInt(target, 10),
125		"virtualUgnot0", strconv.FormatInt(vu0, 10),
126	)
127	return id
128}
129
130// Buy spends up to amount WUGNOT on the curve and mints tokens to the caller.
131// No fee. A fill that reaches the raise graduates inside this call.
132func Buy(cur realm, id string, amount, minTokensOut int64) int64 {
133	payer := requireEOA(cur)
134	l := mustLaunch(id)
135	if amount <= 0 {
136		panic("pado: amount must be positive")
137	}
138	if minTokensOut < 0 {
139		panic("pado: minOut negative")
140	}
141	creditOrigin(cur)
142	lockPrepaid(payer, amount)
143
144	if l.status != StatusCurve {
145		panic("pado: not on curve (use SwapBuy)")
146	}
147	need := l.raiseUgnot - l.raisedUgnot
148	left := CurveSupply - l.realSold
149	if need <= 0 || left <= 0 {
150		if ready(l) {
151			graduate(l)
152		}
153		return 0
154	}
155
156	used := amount
157	if used > need {
158		used = need
159	}
160	maxIn := ammmath.MaxNetInForTokenOut(l.virtualUgnot, l.virtualToken, left)
161	if maxIn <= 0 {
162		panic("pado: no curve capacity")
163	}
164	if used > maxIn {
165		used = maxIn
166	}
167	tokens, _, newVT := ammmath.BuyTokens(l.virtualUgnot, l.virtualToken, used)
168	for tokens > left && used > 1 {
169		used--
170		tokens, _, newVT = ammmath.BuyTokens(l.virtualUgnot, l.virtualToken, used)
171	}
172	if tokens <= 0 || tokens > left {
173		panic("pado: cannot fill curve")
174	}
175	if minTokensOut > 0 && tokens < minTokensOut {
176		panic("pado: tokens below minOut")
177	}
178	checkSnipe(l, payer, tokens)
179
180	subPrepaid(payer, used)
181	l.raisedUgnot += used
182	l.virtualUgnot = l.virtualUgnot0 + l.raisedUgnot
183	l.virtualToken = newVT
184	l.realSold += tokens
185	mint(l, payer, tokens)
186	chain.Emit("Buy",
187		"id", id,
188		"buyer", payer.String(),
189		"ugnot", strconv.FormatInt(used, 10),
190		"tokens", strconv.FormatInt(tokens, 10),
191	)
192	if ready(l) {
193		graduate(l)
194	}
195	return tokens
196}
197
198// Sell burns curve tokens and returns the quote this launch holds. No fee.
199// Payout is capped at this launch's raised quote, so a rounding step cannot
200// spend another launch's WUGNOT or the virtual seed.
201func Sell(cur realm, id string, tokensIn, minUgnotOut int64) int64 {
202	seller := requireEOA(cur)
203	l := mustLaunch(id)
204	if l.status != StatusCurve {
205		panic("pado: not on curve (use SwapSell)")
206	}
207	if tokensIn <= 0 {
208		panic("pado: tokensIn must be positive")
209	}
210	if minUgnotOut < 0 {
211		panic("pado: minOut negative")
212	}
213	if bal(l, seller) < tokensIn {
214		panic("pado: insufficient token balance")
215	}
216	quote, _, newVT := ammmath.SellTokens(l.virtualUgnot, l.virtualToken, tokensIn)
217	if quote > l.raisedUgnot {
218		quote = l.raisedUgnot
219	}
220	if quote <= 0 {
221		panic("pado: no quote out")
222	}
223	if minUgnotOut > 0 && quote < minUgnotOut {
224		panic("pado: ugnot below minOut")
225	}
226	l.raisedUgnot -= quote
227	l.virtualUgnot = l.virtualUgnot0 + l.raisedUgnot
228	l.virtualToken = newVT
229	l.realSold -= tokensIn
230	if l.realSold < 0 {
231		l.realSold = 0
232	}
233	burn(l, seller, tokensIn)
234	payOut(cur, seller, quote)
235	chain.Emit("Sell",
236		"id", id,
237		"seller", seller.String(),
238		"tokens", strconv.FormatInt(tokensIn, 10),
239		"ugnot", strconv.FormatInt(quote, 10),
240	)
241	return quote
242}
243
244// Graduate moves a filled curve into this realm's internal pool.
245// The quote stays on this token's books. Anyone may call it once ready.
246func Graduate(cur realm, id string) {
247	requireEOA(cur)
248	l := mustLaunch(id)
249	if l.status != StatusCurve {
250		panic("pado: already graduated")
251	}
252	if !ready(l) {
253		panic("pado: not ready to graduate")
254	}
255	graduate(l)
256}
257
258// SwapBuy buys from the post-graduate pool. No fee.
259func SwapBuy(cur realm, id string, amount, minTokensOut int64) int64 {
260	buyer := requireEOA(cur)
261	l := mustLaunch(id)
262	if l.status != StatusGraduated {
263		panic("pado: not graduated")
264	}
265	if amount <= 0 {
266		panic("pado: amount must be positive")
267	}
268	if minTokensOut < 0 {
269		panic("pado: minOut negative")
270	}
271	creditOrigin(cur)
272	lockPrepaid(buyer, amount)
273	tokens, newU, newT := ammmath.PoolSwapUgnotForToken(l.poolUgnot, l.poolToken, amount, 0)
274	if minTokensOut > 0 && tokens < minTokensOut {
275		panic("pado: tokens below minOut")
276	}
277	subPrepaid(buyer, amount)
278	l.poolUgnot = newU
279	l.poolToken = newT
280	mint(l, buyer, tokens)
281	chain.Emit("SwapBuy",
282		"id", id,
283		"buyer", buyer.String(),
284		"ugnot", strconv.FormatInt(amount, 10),
285		"tokens", strconv.FormatInt(tokens, 10),
286	)
287	return tokens
288}
289
290// SwapSell sells into the post-graduate pool. No fee.
291// Pays only from this launch's pool quote.
292func SwapSell(cur realm, id string, tokensIn, minUgnotOut int64) int64 {
293	seller := requireEOA(cur)
294	l := mustLaunch(id)
295	if l.status != StatusGraduated {
296		panic("pado: not graduated")
297	}
298	if tokensIn <= 0 {
299		panic("pado: tokensIn must be positive")
300	}
301	if minUgnotOut < 0 {
302		panic("pado: minOut negative")
303	}
304	if bal(l, seller) < tokensIn {
305		panic("pado: insufficient token balance")
306	}
307	quote, newU, newT := ammmath.PoolSwapTokenForUgnot(l.poolUgnot, l.poolToken, tokensIn)
308	if quote > l.poolUgnot {
309		panic("pado: pool quote")
310	}
311	if minUgnotOut > 0 && quote < minUgnotOut {
312		panic("pado: ugnot below minOut")
313	}
314	l.poolUgnot = newU
315	l.poolToken = newT
316	burn(l, seller, tokensIn)
317	payOut(cur, seller, quote)
318	chain.Emit("SwapSell",
319		"id", id,
320		"seller", seller.String(),
321		"tokens", strconv.FormatInt(tokensIn, 10),
322		"ugnot", strconv.FormatInt(quote, 10),
323	)
324	return quote
325}
326
327// ClaimWugnot withdraws the caller's prepaid remainder. It does not withdraw
328// a launch's raised quote or pool.
329func ClaimWugnot(cur realm) int64 {
330	user := requireEOA(cur)
331	amt := prepaidOf(user)
332	if amt <= 0 {
333		return 0
334	}
335	subPrepaid(user, amt)
336	payOut(cur, user, amt)
337	chain.Emit("ClaimWugnot", "user", user.String(), "amount", strconv.FormatInt(amt, 10))
338	return amt
339}
340
341// Transfer moves this realm's token. The quote reserve does not move.
342func Transfer(cur realm, id string, to address, amount int64) {
343	from := requireEOA(cur)
344	l := mustLaunch(id)
345	if err := l.ledger.Transfer(from, to, amount); err != nil {
346		panic(err.Error())
347	}
348	chain.Emit("Transfer", "id", id, "from", from.String(), "to", to.String(), "amount", strconv.FormatInt(amount, 10))
349}
350
351// Approve lets spender TransferFrom the caller's tokens.
352func Approve(cur realm, id string, spender address, amount int64) {
353	caller := requireEOA(cur)
354	l := mustLaunch(id)
355	if err := l.ledger.Approve(caller, spender, amount); err != nil {
356		panic(err.Error())
357	}
358}
359
360// TransferFrom spends the caller's allowance.
361func TransferFrom(cur realm, id string, from, to address, amount int64) {
362	spender := requireEOA(cur)
363	l := mustLaunch(id)
364	if err := l.ledger.TransferFrom(from, spender, to, amount); err != nil {
365		panic(err.Error())
366	}
367	chain.Emit("TransferFrom", "id", id, "from", from.String(), "to", to.String(), "amount", strconv.FormatInt(amount, 10))
368}
369
370func Owner() address     { return owner }
371func RealmAddr() address { return padAddr }
372func RaiseOf(id string) int64 {
373	return mustLaunch(id).raiseUgnot
374}
375func RequestedRaiseOf(id string) int64 {
376	return mustLaunch(id).requestedRaise
377}
378func RaisedOf(id string) int64 { return mustLaunch(id).raisedUgnot }
379func StatusOf(id string) int   { return mustLaunch(id).status }
380func SoldOf(id string) int64   { return mustLaunch(id).realSold }
381func VirtualOf(id string) (vu0, vu, vt int64) {
382	l := mustLaunch(id)
383	return l.virtualUgnot0, l.virtualUgnot, l.virtualToken
384}
385func PoolOf(id string) (ugnot, tokens int64) {
386	l := mustLaunch(id)
387	return l.poolUgnot, l.poolToken
388}
389func BalanceOf(id string, addr address) int64 {
390	return bal(mustLaunch(id), addr)
391}
392func Allowance(id string, who, spender address) int64 {
393	return mustLaunch(id).token.Allowance(who, spender)
394}
395func PrepaidOf(addr address) int64 { return prepaidOf(addr) }
396func ResolveSymbol(symbol string) string {
397	v := bySymbol.Get(symbol)
398	if v == nil {
399		return ""
400	}
401	id, ok := v.(string)
402	if !ok {
403		panic("pado: bad symbol index")
404	}
405	return id
406}
407
408// launch is one token book. token and ledger stay unexported so other
409// packages cannot mint.
410type launch struct {
411	id             string
412	name           string
413	symbol         string
414	uri            string
415	creator        address
416	status         int
417	created        int64
418	requestedRaise int64
419	raiseUgnot     int64
420	virtualUgnot0  int64
421	virtualUgnot   int64
422	virtualToken   int64
423	raisedUgnot    int64
424	realSold       int64
425	poolUgnot      int64
426	poolToken      int64
427	leftover       int64
428	token          *grc20.Token
429	ledger         *grc20.PrivateLedger
430	snipe          avl.Tree
431}
432
433func seedForRaise(raise int64) (vu0, target int64) {
434	if raise < MinRaiseUgnot || raise > MaxRaiseUgnot {
435		panic("pado: raise out of range")
436	}
437	span := VirtualToken0 - CurveSupply
438	vu0 = ammmath.MulDiv(raise, span, CurveSupply)
439	if vu0 <= 0 {
440		panic("pado: raise too small for curve")
441	}
442	target = ammmath.MulDiv(vu0, CurveSupply, span)
443	if target <= 0 || target > raise {
444		panic("pado: raise not reachable")
445	}
446	return vu0, target
447}
448
449func ready(l *launch) bool {
450	if l.raisedUgnot >= l.raiseUgnot && l.raiseUgnot > 0 {
451		return true
452	}
453	return l.realSold >= CurveSupply && l.raisedUgnot > 0
454}
455
456func graduate(l *launch) {
457	if l.status != StatusCurve {
458		return
459	}
460	poolU := l.raisedUgnot
461	if poolU <= 0 {
462		panic("pado: empty pool quote")
463	}
464	remaining := TotalSupply - l.realSold
465	if remaining <= 0 {
466		panic("pado: no tokens for pool")
467	}
468	poolT := remaining
469	if l.virtualUgnot > 0 && l.virtualToken > 0 {
470		needed := ammmath.MulDiv(poolU, l.virtualToken, l.virtualUgnot)
471		if needed > 0 && needed < remaining {
472			poolT = needed
473		}
474	}
475	l.poolUgnot = poolU
476	l.poolToken = poolT
477	l.leftover = remaining - poolT
478	l.raisedUgnot = 0
479	l.virtualUgnot = 0
480	l.virtualToken = 0
481	l.status = StatusGraduated
482	chain.Emit("Graduated",
483		"id", l.id,
484		"poolUgnot", strconv.FormatInt(poolU, 10),
485		"poolToken", strconv.FormatInt(poolT, 10),
486		"leftover", strconv.FormatInt(l.leftover, 10),
487	)
488}
489
490func requireEOA(cur realm) address {
491	if !cur.Previous().IsUserCall() {
492		panic("pado: must be EOA MsgCall")
493	}
494	caller := cur.Previous().Address()
495	if !caller.IsValid() {
496		panic("pado: bad caller")
497	}
498	return caller
499}
500
501func requireOwner(cur realm) {
502	caller := requireEOA(cur)
503	if !owner.IsValid() || caller != owner {
504		panic("pado: owner only")
505	}
506}
507
508func mustLaunch(id string) *launch {
509	v := launches.Get(id)
510	if v == nil {
511		panic("pado: unknown launch")
512	}
513	l, ok := v.(*launch)
514	if !ok || l == nil {
515		panic("pado: bad launch")
516	}
517	return l
518}
519
520func bal(l *launch, addr address) int64 {
521	if l.token == nil {
522		panic("pado: missing token")
523	}
524	return l.token.BalanceOf(addr)
525}
526
527func mint(l *launch, addr address, amount int64) {
528	if l.ledger == nil {
529		panic("pado: missing ledger")
530	}
531	if err := l.ledger.Mint(addr, amount); err != nil {
532		panic(err.Error())
533	}
534}
535
536func burn(l *launch, addr address, amount int64) {
537	if err := l.ledger.Burn(addr, amount); err != nil {
538		panic(err.Error())
539	}
540}
541
542func checkSnipe(l *launch, buyer address, tokens int64) {
543	if runtime.ChainHeight()-l.created >= AntiSnipeHeights {
544		return
545	}
546	maxTok := TotalSupply * AntiSnipeMaxBuyBPS / 10000
547	prev := snipeOf(l, buyer)
548	if prev+tokens > maxTok {
549		panic("pado: anti-snipe max buy")
550	}
551	l.snipe.Set(buyer.String(), prev+tokens)
552}
553
554func snipeOf(l *launch, buyer address) int64 {
555	v := l.snipe.Get(buyer.String())
556	if v == nil {
557		return 0
558	}
559	n, ok := v.(int64)
560	if !ok {
561		panic("pado: bad snipe book")
562	}
563	return n
564}
565
566func reservedWugnot() int64 {
567	n := totalPrepaid
568	launches.Iterate("", "", func(_ string, value any) bool {
569		l, ok := value.(*launch)
570		if !ok || l == nil {
571			panic("pado: bad launch")
572		}
573		if l.status == StatusCurve {
574			n += l.raisedUgnot
575		}
576		if l.status == StatusGraduated {
577			n += l.poolUgnot
578		}
579		return false
580	})
581	return n
582}
583
584func bankBal() int64 {
585	if testSkipBanker {
586		return testBank
587	}
588	return wugnot.BalanceOf(padAddr)
589}
590
591func freeWugnot() int64 {
592	have := bankBal()
593	res := reservedWugnot()
594	if have < res {
595		panic("pado: reserve exceeds balance")
596	}
597	return have - res
598}
599
600// creditOrigin attributes this tx's ugnot to the caller in tests, where
601// OriginSend stands in for a prior wugnot.Transfer.
602func creditOrigin(cur realm) {
603	if !testSkipBanker {
604		return
605	}
606	sent := unsafe.OriginSend().AmountOf(DenomUgnot)
607	if sent < 0 {
608		panic("pado: bad origin send")
609	}
610	if sent == 0 {
611		return
612	}
613	addPrepaid(cur.Previous().Address(), sent)
614	testBank += sent
615}
616
617func lockPrepaid(user address, amount int64) {
618	have := prepaidOf(user)
619	if have >= amount {
620		return
621	}
622	need := amount - have
623	if freeWugnot() < need {
624		panic("pado: transfer WUGNOT to this realm, then buy")
625	}
626	addPrepaid(user, need)
627}
628
629func prepaidOf(addr address) int64 {
630	if !addr.IsValid() {
631		return 0
632	}
633	v := prepaid.Get(addr.String())
634	if v == nil {
635		return 0
636	}
637	n, ok := v.(int64)
638	if !ok {
639		panic("pado: bad prepaid book")
640	}
641	return n
642}
643
644func addPrepaid(addr address, amount int64) {
645	if amount <= 0 {
646		return
647	}
648	next := prepaidOf(addr) + amount
649	prepaid.Set(addr.String(), next)
650	totalPrepaid += amount
651}
652
653func subPrepaid(addr address, amount int64) {
654	if amount <= 0 {
655		return
656	}
657	have := prepaidOf(addr)
658	if have < amount {
659		panic("pado: prepaid underfunded")
660	}
661	next := have - amount
662	if next == 0 {
663		prepaid.Remove(addr.String())
664	} else {
665		prepaid.Set(addr.String(), next)
666	}
667	totalPrepaid -= amount
668}
669
670func payOut(cur realm, to address, amount int64) {
671	if amount <= 0 {
672		return
673	}
674	if freeWugnot() < amount {
675		panic("pado: insufficient free WUGNOT")
676	}
677	if testSkipBanker {
678		testBank -= amount
679		if testBank < 0 {
680			panic("pado: negative test bank")
681		}
682		return
683	}
684	wugnot.Transfer(cross(cur), to, amount)
685}
686
687func formatID(n seqid.ID) string {
688	s := strconv.FormatUint(uint64(n), 10)
689	for len(s) < 7 {
690		s = "0" + s
691	}
692	return s
693}