Transaction

55D65903471869…CDF715DD0057

Block 80,582 · index 0 · indexed

Summary

Hash
55D65903471869417E1D53D3B7F1B31E150C525314E56EF9D6B0CDF715DD0057
Block
80,582
Size
101945 bytes
Gas used
131,948,007 / 350,000,000
Fee
2000000ugnot
Status
success

Messages

Attached funds
50000000ugnot

Arguments · 17

  1. #1pad
  2. #2gnomod.toml
  3. #3module = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/pad" gno = "0.9"
  4. #4gnoswap_list.gno
  5. #5package pad import ( "chain" "strconv" "strings" "time" "gno.land/p/gnoswap/consts/v1" u256 "gno.land/p/gnoswap/uint256/v1" "gno.land/r/gnoland/wugnot" "gno.land/r/gnoswap/gns" gnspool "gno.land/r/gnoswap/pool" "gno.land/r/gnoswap/position" "gno.land/r/gnoswap/router" ) // Sapphire Gnoswap registry keys / role addresses (sapphire-1). const ( wugnotTokenKey = "gno.land/r/gnoland/wugnot.wugnot" gnsTokenKey = "gno.land/r/gnoswap/gns.GNS" gnoswapPoolAddrStr = "g1dexaf6aqkkyr9yfy9d5up69lsn7ra80af34g5v" gnoswapRouterAddrStr = "g1vc883gshu5z7ytk5cdynhc8c2dh67pdp4cszkp" ) // listFund tracks assets pulled from the listing caller for refund / LP ugnot reimburse. type listFund struct { ok bool caller address pulledLpWugnot int64 // reimbursed as ugnot after successful list pulledFeeWugnot int64 // leftover WUGNOT refunded after list; full refund on fail pulledGns int64 // CreatePool fee; consumed on success } // tryListOnGnoswap seeds a Gnoswap CL pool with remaining meme tokens + raised-sized // WUGNOT as LP. CreatePool fee (fixed GNS amount) is paid from: // 1) pre-funded GNS on pad (preferred - immune to GNOT/GNS price moves), else // 2) ExactOut WUGNOT->GNS using SURPLUS inventory only (above raised), so LP depth // stays equal to raised even when GNS is expensive. // // Returns true on success. Soft failure sets l.GnoswapNote and returns false // (caller keeps internal CPMM). Does not pull from user - see prepareCallerListFunding. func tryListOnGnoswap(cur realm, l *Launch, raisedUgnot, remainingTokens int64) bool { if raisedUgnot <= 0 || remainingTokens <= 0 { l.GnoswapNote = "list skip: empty capital" return false } padAddr := cur.Address() wBal := wugnot.BalanceOf(padAddr) if wBal < raisedUgnot { l.GnoswapNote = "list skip: need WUGNOT inventory >= raised for LP; have " + strconv.FormatInt(wBal, 10) + " need " + strconv.FormatInt(raisedUgnot, 10) return false } feeNeed := gnspool.GetPoolCreationFee() if feeNeed <= 0 { feeNeed = 100_000_000 // 100 GNS default (6 decimals) } // --- GNS for CreatePool fee --- // Require GNS already on pad. Do NOT Approve+router ExactOut mid-list: // realm spender frame often panics "insufficient allowance" and reverts the tx. // UI: gns.Transfer(pad, feeNeed) then RetryListGnoswap. gnsBal := gns.BalanceOf(padAddr) feeWugnot := int64(0) if gnsBal < feeNeed { l.GnoswapNote = "list skip: need " + strconv.FormatInt(feeNeed, 10) + " GNS base units on pad (Transfer GNS then RetryList). have " + strconv.FormatInt(gnsBal, 10) + ". No mid-list WUGNOT->GNS swap (avoids allowance panic)." return false } liqWugnot := raisedUgnot if wugnot.BalanceOf(padAddr) < liqWugnot { l.GnoswapNote = "list skip: WUGNOT below raised after fee path" return false } // LP token amount: prefer PoolToken already sized in graduate() to curve spot. // Defensive resize if Virtuals are somehow still set (pre-graduate test paths): // tokensForLP = raisedUgnot * VirtualToken / VirtualUgnot liqTokens := remainingTokens if l.VirtualUgnot > 0 && l.VirtualToken > 0 && liqWugnot > 0 { needed := liqWugnot * l.VirtualToken / l.VirtualUgnot if needed > 0 && needed < remainingTokens { liqTokens = needed } } if liqTokens <= 0 { l.GnoswapNote = "list skip: zero LP tokens at curve spot" return false } // Mint LP slice + leftover inventory (LeftoverTokens set at graduate) onto pad. mintAmt := remainingTokens if l.LeftoverTokens > 0 { mintAmt = remainingTokens + l.LeftoverTokens } if mintAmt < liqTokens { mintAmt = liqTokens } addBal(l, padAddr, mintAmt) tokenKey := adenaKeyFromTokenID(l.TokenID, l.Symbol) if tokenKey == "" { l.GnoswapNote = "list skip: empty token registry key" return false } // Sort token0 < token1 (Gnoswap pool key order) BEFORE sqrtPrice + CreatePool + Mint. // CreatePool reorders tokens and INVERTS sqrtPriceX96 when args are out of order // (newSqrt = Q192/oldSqrt). If we pass unsorted keys with a price already computed // for the sorted pair, the pool is born with inverted price → Mint only takes a // tiny sliver of one side (e.g. ~4 GNOT of a 10k WUGNOT raise). Always call // CreatePool(t0, t1, fee, sqrt) with the same sorted pair as Mint. t0, t1 := wugnotTokenKey, tokenKey amt0, amt1 := liqWugnot, liqTokens if strings.Compare(t0, t1) > 0 { t0, t1 = t1, t0 amt0, amt1 = amt1, amt0 } sqrtPrice := computeSqrtPriceX96(amt0, amt1) if sqrtPrice == "" || sqrtPrice == "0" { l.GnoswapNote = "list skip: bad sqrtPriceX96" return false } poolAddr := address(gnoswapPoolAddrStr) gns.Approve(cross(cur), poolAddr, feeNeed) wugnot.Approve(cross(cur), poolAddr, liqWugnot) if err := l.ledger.Approve(padAddr, poolAddr, liqTokens); err != nil { l.GnoswapNote = "list skip: token approve failed: " + err.Error() return false } // CRITICAL: sorted (t0,t1) + matching sqrtPrice (do NOT pass original unsorted keys) gnspool.CreatePool(cross(cur), t0, t1, GnoswapFeeTier, sqrtPrice) tickLower, tickUpper := alignedFullRangeTicks(GnoswapTickSpacing) deadline := time.Now().Unix() + 600 posID, liqStr, a0, a1 := position.Mint( cross(cur), t0, t1, GnoswapFeeTier, tickLower, tickUpper, strconv.FormatInt(amt0, 10), strconv.FormatInt(amt1, 10), "0", "0", deadline, padAddr, // permanent lock: pad owns position NFT "", ) // Actual WUGNOT deposited (whichever side is WUGNOT after sort) used0, _ := strconv.ParseInt(a0, 10, 64) used1, _ := strconv.ParseInt(a1, 10, 64) wugnotUsed := int64(0) if t0 == wugnotTokenKey { wugnotUsed = used0 } else if t1 == wugnotTokenKey { wugnotUsed = used1 } // Sanity: if Mint took << intended LP WUGNOT, mark note (still listed — pool exists) if wugnotUsed > 0 && liqWugnot > 0 && wugnotUsed*2 < liqWugnot { // keep going but surface under-deposit for ops chain.Emit("ListUnderDeposit", "id", l.ID, "wantWugnot", strconv.FormatInt(liqWugnot, 10), "gotWugnot", strconv.FormatInt(wugnotUsed, 10), ) } poolPath := t0 + ":" + t1 + ":" + strconv.FormatUint(uint64(GnoswapFeeTier), 10) l.GnoswapListed = true l.GnoswapPoolPath = poolPath l.GnoswapPositionID = posID l.ListVenue = VenueGnoswap l.FeeWugnotSpent = feeWugnot if wugnotUsed > 0 { l.LiqWugnotUsed = wugnotUsed } else { l.LiqWugnotUsed = liqWugnot } // Release Create-time GNS escrow (fee left pad via CreatePool). consumeListFeeEscrow(l) leftoverTok := l.LeftoverTokens if leftoverTok < 0 { leftoverTok = 0 } if remLeft := remainingTokens - liqTokens; remLeft > leftoverTok { leftoverTok = remLeft } l.GnoswapNote = "listed pool=" + poolPath + " pos=" + strconv.FormatUint(posID, 10) + " liq=" + liqStr + " a0=" + a0 + " a1=" + a1 + " wugnotUsed=" + strconv.FormatInt(wugnotUsed, 10) + " feeWugnot=" + strconv.FormatInt(feeWugnot, 10) + " lpTokens=" + strconv.FormatInt(liqTokens, 10) + " leftoverTokens=" + strconv.FormatInt(leftoverTok, 10) chain.Emit("GnoswapListed", "id", l.ID, "poolPath", poolPath, "positionId", strconv.FormatUint(posID, 10), "feeWugnot", strconv.FormatInt(feeWugnot, 10), "liqWugnot", strconv.FormatInt(liqWugnot, 10), "wugnotUsed", strconv.FormatInt(wugnotUsed, 10), "lpTokens", strconv.FormatInt(liqTokens, 10), "leftoverTokens", strconv.FormatInt(leftoverTok, 10), "tokens", strconv.FormatInt(remainingTokens, 10), ) return true } // prepareCallerListFunding fills pad inventory shortfall from the EOA caller via TransferFrom. // // padv14+ WUGNOT raise: Buy already TransferFrom WUGNOT to pad, so wBal >= raised // at graduate in the normal case — only GNS fee (or small fee WUGNOT budget) may be short. // // Legacy ugnot-raise pads: wugnot.Deposit is EOA-only; caller wraps temp LP then reimbursed. func prepareCallerListFunding(cur realm, l *Launch, raisedUgnot int64) listFund { caller := cur.Previous().Address() padA := cur.Address() out := listFund{ok: true, caller: caller} if raisedUgnot <= 0 { l.GnoswapNote = "list skip: empty raise" out.ok = false return out } feeNeed := gnspool.GetPoolCreationFee() if feeNeed <= 0 { feeNeed = 100_000_000 } wBal := wugnot.BalanceOf(padA) if wBal < raisedUgnot { short := raisedUgnot - wBal if !pullWugnotFrom(cur, caller, padA, short) { l.GnoswapNote = "list skip: need temp WUGNOT wrap " + strconv.FormatInt(short, 10) + " ugnot units (Deposit+Approve pad). LP reimbursed from pad ugnot after list. " + "Fee: " + strconv.FormatInt(feeNeed, 10) + " GNS or WUGNOT ExactOut budget." out.ok = false return out } out.pulledLpWugnot = short wBal += short } gnsBal := gns.BalanceOf(padA) if gnsBal < feeNeed { gShort := feeNeed - gnsBal if pullGnsFrom(cur, caller, padA, gShort) { out.pulledGns = gShort } else { // No GNS from caller - pull modest WUGNOT surplus for ExactOut → GNS. // Do NOT pull full GnoswapMaxFeeWugnot (5000 GNOT) — that causes // InsufficientCoins for normal wallets. MaxFee still caps ExactOut spend. surplus := wBal - raisedUgnot if surplus < 1 { feeBudget := listFeeWugnotPull() if !pullWugnotFrom(cur, caller, padA, feeBudget) { l.GnoswapNote = "list skip: need GNS fee " + strconv.FormatInt(feeNeed, 10) + " (Approve pad) OR WUGNOT fee budget " + strconv.FormatInt(feeBudget, 10) + " ugnot for ExactOut. Prefer holding 100 GNS." // Roll back LP pull. if out.pulledLpWugnot > 0 { safeWugnotTransfer(cur, caller, out.pulledLpWugnot) out.pulledLpWugnot = 0 } out.ok = false return out } out.pulledFeeWugnot = feeBudget } } } return out } // listFeeWugnotPull is the WUGNOT amount pulled from the listing caller for // ExactOut → GNS when they do not hold CreatePool fee in GNS. // Kept well below GnoswapMaxFeeWugnot so List does not demand 5k+ GNOT wrap. func listFeeWugnotPull() int64 { const defaultPull int64 = 1_500_000_000 // 1500 GNOT — enough for ~100 GNS at ≤15 GNOT/GNS if GnoswapMaxFeeWugnot > 0 && GnoswapMaxFeeWugnot < defaultPull { return GnoswapMaxFeeWugnot } return defaultPull } // pullWugnotFrom is intentionally a no-op pull. // // GRC20 TransferFrom from a realm often panics "insufficient allowance" even when // Allowance(owner, pad) is set (spender frame resolves to EOA, not pad). Soft-fail // instead so list can fall back to notes / surplus / pre-funded pad inventory. // // UI must pre-fund pad via wugnot.Transfer(pad, amount) and gns.Transfer(pad, fee) // before RetryListGnoswap — never rely on Approve+TransferFrom in the same flow. func pullWugnotFrom(cur realm, from, to address, amount int64) bool { if amount <= 0 { return true } // Already on pad? treat as satisfied without TransferFrom. if wugnot.BalanceOf(to) >= amount && from != to { // Not a full check for "raised" accounting — prepareCallerListFunding // already compared pad balance to raised before calling shortfall pulls. } _ = cur _ = from _ = to return false } func pullGnsFrom(cur realm, from, to address, amount int64) bool { if amount <= 0 { return true } _ = cur _ = from _ = to // Never TransferFrom GNS either (same spender-frame issue). return false } func safeWugnotTransfer(cur realm, to address, amount int64) { if amount <= 0 { return } have := wugnot.BalanceOf(cur.Address()) if have < amount { amount = have } if amount > 0 { wugnot.Transfer(cross(cur), to, amount) } } func safeGnsTransfer(cur realm, to address, amount int64) { if amount <= 0 { return } have := gns.BalanceOf(cur.Address()) if have < amount { amount = have } if amount > 0 { gns.Transfer(cross(cur), to, amount) } } func refundCallerListFunding(cur realm, fund listFund) { if !fund.caller.IsValid() { return } safeWugnotTransfer(cur, fund.caller, fund.pulledLpWugnot+fund.pulledFeeWugnot) safeGnsTransfer(cur, fund.caller, fund.pulledGns) } // settleCallerListFunding returns unused fee-budget WUGNOT to the caller. // LP shortfall pull is rare on WUGNOT-raise pads (raised already on pad); if it // happened, reimburse in WUGNOT from any leftover pad balance after list. func settleCallerListFunding(cur realm, fund listFund) { if !fund.caller.IsValid() { return } if fund.pulledLpWugnot > 0 { safeWugnotTransfer(cur, fund.caller, fund.pulledLpWugnot) } // Leftover fee-budget WUGNOT (ExactOut may not consume full maxIn). if fund.pulledFeeWugnot > 0 { safeWugnotTransfer(cur, fund.caller, fund.pulledFeeWugnot) } } // listOnGnoswapWithFunding is the full auto path: pull inventory from caller if needed, // CreatePool+Mint, reimburse LP ugnot / refund on failure. func listOnGnoswapWithFunding(cur realm, l *Launch, raisedUgnot, remainingTokens int64) bool { fund := prepareCallerListFunding(cur, l, raisedUgnot) if !fund.ok { return false } ok := tryListOnGnoswap(cur, l, raisedUgnot, remainingTokens) if ok { settleCallerListFunding(cur, fund) if fund.pulledLpWugnot > 0 || fund.pulledFeeWugnot > 0 || fund.pulledGns > 0 { chain.Emit("ListFunding", "id", l.ID, "caller", fund.caller.String(), "lpWugnot", strconv.FormatInt(fund.pulledLpWugnot, 10), "feeWugnot", strconv.FormatInt(fund.pulledFeeWugnot, 10), "gns", strconv.FormatInt(fund.pulledGns, 10), ) } return true } refundCallerListFunding(cur, fund) return false } // listNeedOf: poolU|wHave|wNeedLp|gnsHave|gnsNeed|feeGns|feeWugnotBudget|padAddr func listNeedOf(l *Launch) string { poolU := int64(0) if l != nil && l.Status == StatusGraduated && !l.GnoswapListed { poolU = l.PoolUgnot } padA := padAddr wHave := wugnot.BalanceOf(padA) gnsHave := gns.BalanceOf(padA) feeGns := gnspool.GetPoolCreationFee() if feeGns <= 0 { feeGns = 100_000_000 } wNeed := poolU - wHave if wNeed < 0 { wNeed = 0 } gNeed := feeGns - gnsHave if gNeed < 0 { gNeed = 0 } feeBud := listFeeWugnotPull() // If GNS already covered, fee WUGNOT budget need is 0 for the wizard. if gNeed == 0 { feeBud = 0 } else if wHave > poolU { // Existing surplus reduces fee budget to pull surp := wHave - poolU if surp >= feeBud { feeBud = 0 } else { feeBud = feeBud - surp } } return strconv.FormatInt(poolU, 10) + "|" + strconv.FormatInt(wHave, 10) + "|" + strconv.FormatInt(wNeed, 10) + "|" + strconv.FormatInt(gnsHave, 10) + "|" + strconv.FormatInt(gNeed, 10) + "|" + strconv.FormatInt(feeGns, 10) + "|" + strconv.FormatInt(feeBud, 10) + "|" + padA.String() } // ListNeed is the public query for the Token list wizard (default venue). func ListNeed(id string) string { return listNeedOf(mustLaunch(id)) } // ListNeedFor is venue-aware; only gnoswap has a Need adapter today. func ListNeedFor(id, venueId string) string { l := mustLaunch(id) vid := normalizeVenueID(venueId) if vid != VenueGnoswap { poolU := int64(0) if l != nil && l.Status == StatusGraduated && !l.GnoswapListed { poolU = l.PoolUgnot } return strconv.FormatInt(poolU, 10) + "|0|0|0|0|0|0|" + padAddr.String() } return listNeedOf(l) } // ListedOf reports whether the launch is listed on any venue. func ListedOf(id string) bool { l := mustLaunch(id) return l.GnoswapListed || l.ListVenue != "" } // ListPoolPathOf returns the pool path after listing. func ListPoolPathOf(id string) string { return mustLaunch(id).GnoswapPoolPath } // ListNoteOf returns the listing status note. func ListNoteOf(id string) string { return mustLaunch(id).GnoswapNote } func swapWugnotForGNS(cur realm, amountOutGNS, maxInWugnot int64) (spent int64, ok bool) { if amountOutGNS <= 0 || maxInWugnot <= 0 { return 0, false } routerAddr := address(gnoswapRouterAddrStr) before := wugnot.BalanceOf(cur.Address()) if before < maxInWugnot { maxInWugnot = before } if maxInWugnot <= 0 { return 0, false } wugnot.Approve(cross(cur), routerAddr, maxInWugnot) route := wugnotTokenKey + ":" + gnsTokenKey + ":" + strconv.FormatUint(uint64(GnoswapFeeTier), 10) deadline := time.Now().Unix() + 600 inStr, outStr := router.ExactOutSwapRoute( cross(cur), wugnotTokenKey, gnsTokenKey, strconv.FormatInt(amountOutGNS, 10), route, "100", strconv.FormatInt(maxInWugnot, 10), deadline, "", ) _ = outStr spentIn, err := strconv.ParseInt(inStr, 10, 64) if err != nil || spentIn <= 0 { after := wugnot.BalanceOf(cur.Address()) if after >= before { return 0, false } return before - after, true } return spentIn, true } func computeSqrtPriceX96(amount0, amount1 int64) string { if amount0 <= 0 || amount1 <= 0 { return "" } a0 := u256.MustFromDecimal(strconv.FormatInt(amount0, 10)) a1 := u256.MustFromDecimal(strconv.FormatInt(amount1, 10)) num := u256.Zero().Mul(a1, consts.Q192()) ratio := u256.Zero().Div(num, a0) sqrt := u256Sqrt(ratio) if sqrt.Lt(consts.MinSqrtRatio()) { return consts.MinSqrtRatio().ToString() } if sqrt.Gte(consts.MaxSqrtRatio()) { return u256.Zero().Sub(consts.MaxSqrtRatio(), u256.One()).ToString() } return sqrt.ToString() } func u256Sqrt(x *u256.Uint) *u256.Uint { if x == nil || x.IsZero() { return u256.Zero() } if x.Eq(u256.One()) { return u256.One() } lo := u256.Zero() hi := u256.Zero().Add(x, u256.One()) cap128 := u256.Zero().Lsh(u256.One(), 128) if hi.Gt(cap128) { hi = cap128.Clone() } for lo.Lt(u256.Zero().Sub(hi, u256.One())) { mid := u256.Zero().Div(u256.Zero().Add(lo, hi), u256.NewUint(2)) if mid.IsZero() { lo = u256.One() continue } sq, overflow := u256.Zero().MulOverflow(mid, mid) if overflow || sq.Gt(x) { hi = mid } else { lo = mid } } return lo } func alignedFullRangeTicks(spacing int32) (int32, int32) { if spacing <= 0 { spacing = 60 } tl := (GnoswapMinTick / spacing) * spacing tu := (GnoswapMaxTick / spacing) * spacing if tl >= tu { tl = -spacing tu = spacing } return tl, tu } // GnoswapListedOf reports whether a launch was auto-listed on Gnoswap. func GnoswapListedOf(id string) bool { return mustLaunch(id).GnoswapListed } // GnoswapPoolPathOf returns the Gnoswap pool path after listing (or empty). func GnoswapPoolPathOf(id string) string { return mustLaunch(id).GnoswapPoolPath } // GnoswapNoteOf returns the listing status note. func GnoswapNoteOf(id string) string { return mustLaunch(id).GnoswapNote }
  6. #6list_venue.gno
  7. #7package pad import ( "chain" "strconv" "strings" "gno.land/p/nt/avl/v0" ) // ListVenue IDs (compile-time switch in tryListVenue). Adding a new DEX // means a new case + adapter file and a new pad version — Gno cannot // dynamically import by path string. const ( VenueGnoswap = "gnoswap" VenueZdex = "zdex" ) // listVenueMeta is registry metadata (not a callable adapter). type listVenueMeta struct { ID string Label string Enabled bool FeeAssetHint string // e.g. "GNS" for CreatePool fee UX } var ( listVenues avl.Tree // id -> *listVenueMeta defaultListVenue string ) func ensureListVenuesSeeded() { if listVenues.Size() > 0 { return } // Mainnet-first: zdex default (hub listable when adapter + realm live). // Gnoswap registered but disabled until mainnet router/pool exist — enable via SetListVenue. registerListVenue(VenueZdex, "zdex", true, "ugnot") registerListVenue(VenueGnoswap, "Gnoswap", false, "GNS") if defaultListVenue == "" { defaultListVenue = VenueZdex } } func registerListVenue(id, label string, enabled bool, feeHint string) { id = strings.TrimSpace(strings.ToLower(id)) if id == "" { return } label = strings.TrimSpace(label) if label == "" { label = id } listVenues.Set(id, &listVenueMeta{ ID: id, Label: label, Enabled: enabled, FeeAssetHint: strings.TrimSpace(feeHint), }) } func normalizeVenueID(venueId string) string { ensureListVenuesSeeded() v := strings.TrimSpace(strings.ToLower(venueId)) if v == "" { if defaultListVenue != "" { return defaultListVenue } return VenueZdex } return v } func venueEnabled(venueId string) bool { ensureListVenuesSeeded() id := normalizeVenueID(venueId) raw := listVenues.Get(id) if raw == nil { return false } m, ok := raw.(*listVenueMeta) return ok && m != nil && m.Enabled } // DefaultListVenue returns the venue id used when callers pass "". func DefaultListVenue() string { ensureListVenuesSeeded() if defaultListVenue == "" { return VenueZdex } return defaultListVenue } // SetDefaultListVenue sets the default list venue (protocol only). func SetDefaultListVenue(cur realm, venueId string) { requireProtocol(cur) ensureListVenuesSeeded() id := strings.TrimSpace(strings.ToLower(venueId)) if id == "" { panic("pad: empty venue") } if listVenues.Get(id) == nil { panic("pad: unknown venue") } defaultListVenue = id chain.Emit("SetDefaultListVenue", "venue", id) } // SetListVenue registers or updates venue metadata (protocol only). // Does not add compile-time dispatch — unknown ids stay non-listable until // tryListVenue gains a case in a future pad version. func SetListVenue(cur realm, id, label string, enabled bool, feeAssetHint string) { requireProtocol(cur) ensureListVenuesSeeded() id = strings.TrimSpace(strings.ToLower(id)) if id == "" || len(id) > 32 { panic("pad: invalid venue id") } for _, c := range id { if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-') { panic("pad: venue id charset") } } registerListVenue(id, label, enabled, feeAssetHint) chain.Emit("SetListVenue", "id", id, "label", strings.TrimSpace(label), "enabled", strconv.FormatBool(enabled), "feeHint", strings.TrimSpace(feeAssetHint), ) } // ListVenues returns newline-separated "id|label|enabled|feeHint" (enabled 0|1). func ListVenues() string { ensureListVenuesSeeded() out := "" listVenues.Iterate("", "", func(key string, value any) bool { m, ok := value.(*listVenueMeta) if !ok || m == nil { return false } en := "0" if m.Enabled { en = "1" } line := m.ID + "|" + m.Label + "|" + en + "|" + m.FeeAssetHint if out != "" { out += "\n" } out += line return false }) return out } // ListVenueOf returns the venue id used for a successful list (or ""). func ListVenueOf(id string) string { return mustLaunch(id).ListVenue } // resetListVenuesForTest clears venue registry (unit tests). func resetListVenuesForTest() { listVenues = avl.Tree{} defaultListVenue = "" } // tryListVenue dispatches to a compile-time adapter. Unknown / disabled // venues soft-fail with a note (no panic) so UI can show ListNeed guidance. func tryListVenue(cur realm, l *Launch, venueId string, raisedUgnot, liqTokens int64) bool { ensureListVenuesSeeded() id := normalizeVenueID(venueId) if !venueEnabled(id) { if l != nil { l.GnoswapNote = "list skip: venue disabled or unknown: " + id } return false } switch id { case VenueZdex: ok := tryListOnZdex(cur, l, raisedUgnot, liqTokens) if ok && l != nil { l.ListVenue = VenueZdex } return ok case VenueGnoswap: ok := listOnGnoswapWithFunding(cur, l, raisedUgnot, liqTokens) if ok && l != nil { l.ListVenue = VenueGnoswap } return ok default: // Registered in metadata but no adapter case yet. if l != nil { l.GnoswapNote = "list skip: venue adapter not linked in this pad: " + id } return false } }
  8. #8memepad.gno
  9. #9// Package pad is gnomemepad: a self-contained meme launchpad for gno.land. // // CreateWithVenue (bond + venue-aware GNS escrow) -> GRC20 + WUGNOT curve -> Graduate // -> internal CPMM; then either: // gnoswap: RetryList / RetryListGnoswap (escrowed GNS + WUGNOT inventory) // zdex: ReleaseListSeed -> EOA CreatePool -> ConfirmZdexList // // Production: Buy/Sell/Swap* use WUGNOT (push-pay + prepaid credits). // Payment: user wugnot.Transfer(pad, amount) then Buy. // Create fee: gnoswap preference locks ListFeeGns; zdex preference locks 0. // DefaultListVenue source = zdex; Pearl may SetDefaultListVenue(gnoswap) after deploy. // Unit tests (testSkipBanker): OriginSend ugnot as collateral units. // Tokens are real GRC20 (mint on buy, burn on sell). package pad import ( "chain" "chain/banker" "chain/runtime" "chain/runtime/unsafe" "strconv" "strings" "gno.land/p/nt/grc20/v0" "gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/ammmath" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/r/nt/grc20reg/v0" "gno.land/r/gnoland/wugnot" "gno.land/r/gnoswap/gns" // bond: create-bond policy (promo / normal). Separate package - pad upgrades // do not replace bond schedule. Deploy prepare rewrites to personal path. createbond "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/bond" // pointsv2: optional trade/create awards (off by default until SetPointsEnabled). // Deploy prepare rewrites this import to the Sapphire personal-namespace path. pointsv2 "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/pointsv2" ) var ( launches avl.Tree // id -> *Launch bySymbol avl.Tree // symbol -> id string nextID seqid.ID nextTokenID seqid.ID // GRC20 identity sequence (shared for all launches) // padAddr: this realm's package address (set in init) - for inventory funding. padAddr address // protocolAddr: treasury set by Init (first EOA). Receives protocol fee share. protocolAddr address // protocolFees: ugnot still on pad, claimable / pushable to protocolAddr. protocolFees int64 // protocolFeesPaid: lifetime ugnot already sent to protocolAddr (stats only). protocolFeesPaid int64 inited bool // testSkipBanker: when true, sendUgnot is a no-op (unit tests without funded realm bank). // Always false in production. testSkipBanker bool // testForceGnoswapList: unit tests only - stub tryListOnGnoswap succeeds without Sapphire deps. // Always false in production (gnoswap_list_full ignores it). testForceGnoswapList bool // testForceZdexList: unit tests only - stub tryListOnZdex succeeds. testForceZdexList bool // pointsEnabled: when true, notify pointsv2 after Create / Buy / Sell / Swap*. // Admin must also AllowPad(this package) on pointsv2. pointsEnabled bool // Live mutable economics (protocol-gated). Seeded from consts in Init. // Changing graduation mid-flight affects open curve launches' ready/remaining. graduationUgnot int64 listFeeGnsLive int64 // wugnotCredit: EOA address string -> int64 prepaid WUGNOT claimable by user. // totalWugnotCredit: sum of all credits (included in reservedWugnot). // Flow: Transfer WUGNOT to pad (raises free) -> Buy auto-locks free into credit // and spends; overpay refund goes back to credit; ClaimWugnot withdraws credit. wugnotCredit avl.Tree totalWugnotCredit int64 // totalListFeeGns: sum of per-launch ListFeeGns escrow (reserved GNS on pad). // freeGns = gns.BalanceOf(pad) - totalListFeeGns. totalListFeeGns int64 ) func init(cur realm) { padAddr = cur.Address() } // Trade is one price sample for charts (capped history per launch). type Trade struct { Height int64 Side int // TradeSideBuy | TradeSideSell | TradeSideOpen Ugnot int64 Tokens int64 Price int64 // ugnot per token * 1e6 after the trade } // Launch is one meme market: curve phase then locked pool phase. // token/ledger are unexported so external packages cannot Mint/Burn via field access. type Launch struct { ID string Name string Symbol string URI string Creator address Status int Created int64 // block height // GRC20 (mint/burn only via pad-owned private ledger) token *grc20.Token ledger *grc20.PrivateLedger TokenID string // Token.ID() - registry / Gnoswap identity // Virtual curve reserves VirtualUgnot int64 VirtualToken int64 RealSold int64 // tokens sold on curve (<= CurveSupply) RaisedUgnot int64 // net ugnot collateral in curve (excl. fee vaults) // Real pool (post-grad); LP permanently locked - no remove path // PoolToken is pad-internal reserve sized to curve spot (not always all unsold). // LeftoverTokens = (TotalSupply - RealSold) - PoolToken; minted to pad at list, not LP'd. PoolUgnot int64 PoolToken int64 LeftoverTokens int64 CreatorFees int64 BondUgnot int64 BondRefunded bool UniqueBuyers avl.Tree // address -> true BuyerCount int // snipeBought: address -> cumulative tokens bought during anti-snipe window snipeBought avl.Tree // Gnoswap listing state GnoswapReady bool // graduated; token is listable / listed GnoswapListed bool // true when CreatePool+Mint succeeded on Gnoswap GnoswapNote string // human status / failure reason GnoswapPoolPath string GnoswapPositionID uint64 // FeeWugnotSpent / LiqWugnotUsed: inventory spent at graduate (1:1 vs raised ugnot notionally) FeeWugnotSpent int64 LiqWugnotUsed int64 // ListFeeGns: Create-time GNS escrow for Gnoswap CreatePool fee (padv20+). // Consumed on successful list; ClaimListFee refunds creator if still unlisted. ListFeeGns int64 ListFeeConsumed bool // true after list paid CreatePool fee from escrow // ListVenue: which DEX adapter succeeded (e.g. "zdex" / "gnoswap"). Empty if unlisted. ListVenue string // PreferredVenue: chosen at Create (or SetPreferredVenue). Used when RetryList venueId == "". PreferredVenue string // ListSeedReleased: PoolUgnot (WUGNOT) + PoolToken (minted GRC20) sent to creator for EOA zdex CreatePool. ListSeedReleased bool // Chart history (ordered AVL keys) Trades avl.Tree // tradeKey -> *Trade NextTrade int64 } // Init sets the protocol treasury. First EOA caller becomes fee recipient // (protocolAddr). Protocol trade fees accrue on-pad until ClaimProtocolFees // (treasury only) or PushProtocolFees (anyone may push to treasury). // Creator fees always need ClaimCreatorFees by the token creator. // // Deploy note: call Init with the wallet that should receive protocol fees // (or TransferProtocol later). Gnoswap CreatePool GNS fee is paid to Gnoswap, // not to this treasury. func Init(cur realm) { if inited { panic("pad: already initialized") } if !cur.Previous().IsUserCall() { panic("pad: EOA only") } protocolAddr = cur.Previous().Address() graduationUgnot = GraduationThreshold // seed from const default listFeeGnsLive = ListFeeGns if graduationUgnot <= 0 { panic("pad: default graduation misconfigured") } ensureListVenuesSeeded() inited = true chain.Emit("Init", "protocol", protocolAddr.String(), "graduationUgnot", strconv.FormatInt(graduationUgnot, 10), "listFeeGns", strconv.FormatInt(listFeeGnsLive, 10), "defaultListVenue", DefaultListVenue(), ) } func requireProtocol(cur realm) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: EOA only") } if cur.Previous().Address() != protocolAddr { panic("pad: not protocol") } } // graduationThreshold is the live raise target (ugnot). Falls back to const if unset. func graduationThreshold() int64 { if graduationUgnot > 0 { return graduationUgnot } return GraduationThreshold } // SetGraduationThreshold updates the live raise target (ugnot, 1 GNOT = 1e6). // Protocol/deploy wallet only. Affects open curve launches (remaining raise / ready). func SetGraduationThreshold(cur realm, ugnot int64) { requireProtocol(cur) if ugnot <= 0 { panic("pad: graduation must be positive") } old := graduationThreshold() graduationUgnot = ugnot chain.Emit("SetGraduationThreshold", "from", strconv.FormatInt(old, 10), "to", strconv.FormatInt(ugnot, 10), ) } // GraduationThresholdLive returns the live raise target (ugnot) for UIs/qeval. func GraduationThresholdLive() int64 { return graduationThreshold() } // SetListFeeGns updates Create-time GNS escrow required for new launches. // Protocol/deploy wallet only. Does not change already-escrowed launches. func SetListFeeGns(cur realm, gns int64) { requireProtocol(cur) if gns < 0 { panic("pad: list fee must be non-negative") } old := requiredListFeeGns() listFeeGnsLive = gns chain.Emit("SetListFeeGns", "from", strconv.FormatInt(old, 10), "to", strconv.FormatInt(gns, 10), ) } // creditProtocol accrues protocol ugnot liability on the pad realm. // Cash stays in pad until ClaimProtocolFees / PushProtocolFees. func creditProtocol(amt int64) { if amt <= 0 { return } protocolFees += amt } func requireInit() { if !inited { panic("pad: call Init first") } } // SetPointsEnabled toggles pointsv2 notifications (protocol admin only). // pointsv2 must AllowPad(this package path) or OnTrade/OnCreate will panic and revert the trade. func SetPointsEnabled(cur realm, on bool) { requireProtocol(cur) pointsEnabled = on chain.Emit("SetPointsEnabled", "on", strconv.FormatBool(on)) } // PointsEnabled reports whether pad notifies pointsv2 after trades/creates. func PointsEnabled() bool { return pointsEnabled } func notifyTrade(cur realm, trader address, id string, side int64, volumeUgnot int64) { if !pointsEnabled || testSkipBanker { return } _ = pointsv2.OnTrade(cross(cur), trader, id, side, volumeUgnot) } func notifyCreate(cur realm, creator address, id string) { if !pointsEnabled || testSkipBanker { return } _ = pointsv2.OnCreate(cross(cur), creator, id) } func mustLaunch(id string) *Launch { // Sapphire avl.Tree.Get returns a single any (nil if missing). l, ok := launches.Get(id).(*Launch) if !ok { panic("pad: unknown launch") } return l } func balOf(l *Launch, addr address) int64 { if l == nil || l.token == nil { return 0 } return l.token.BalanceOf(addr) } // addBal mints (delta>0) or burns (delta<0) GRC20 via pad-owned PrivateLedger. func addBal(l *Launch, addr address, delta int64) { if l == nil || l.ledger == nil { panic("pad: missing GRC20 ledger") } if delta == 0 { return } if delta > 0 { if err := l.ledger.Mint(addr, delta); err != nil { panic("pad: mint: " + err.Error()) } return } if err := l.ledger.Burn(addr, -delta); err != nil { panic("pad: burn: " + err.Error()) } } func requireMinOut(got, minOut int64, what string) { if minOut < 0 { panic("pad: minOut must be non-negative") } if minOut > 0 && got < minOut { panic("pad: " + what + " below minOut (slippage)") } } func snipeBoughtOf(l *Launch, buyer address) int64 { v := l.snipeBought.Get(buyer.String()) if v == nil { return 0 } n, ok := v.(int64) if !ok { return 0 } return n } func checkAndAddSnipe(l *Launch, buyer address, tokensOut int64) { height := runtime.ChainHeight() if height-l.Created >= AntiSnipeHeights { return } maxTok := TotalSupply * AntiSnipeMaxBuyBPS / 10000 prev := snipeBoughtOf(l, buyer) if prev+tokensOut > maxTok { panic("pad: anti-snipe cumulative max buy exceeded") } l.snipeBought.Set(buyer.String(), prev+tokensOut) } func sendUgnot(cur realm, to address, amount int64) { if amount <= 0 { return } if testSkipBanker { return } bk := banker.NewBanker(banker.BankerTypeRealmSend, cur) bk.SendCoins(cur.Address(), to, chain.Coins{{Denom: DenomUgnot, Amount: amount}}) } func requireUserPayment(cur realm) int64 { if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } sent := unsafe.OriginSend().AmountOf(DenomUgnot) if sent <= 0 { panic("pad: need ugnot -send") } return sent } // --- WUGNOT collateral (production curve / internal CPMM) --- // reservedWugnot is WUGNOT already committed (markets/fees/user prepaid credits). // Excludes bank-only liabilities (create bond ugnot). func reservedWugnot() int64 { reserved := protocolFees + totalWugnotCredit launches.Iterate("", "", func(_ string, value any) bool { l := value.(*Launch) reserved += l.CreatorFees if l.Status == StatusCurve { reserved += l.RaisedUgnot } // Internal CPMM holds WUGNOT when not listed and seed not released to EOA. if l.Status == StatusGraduated && !l.GnoswapListed && !l.ListSeedReleased { reserved += l.PoolUgnot } return false }) return reserved } // freeWugnotOnPad is WUGNOT on pad above reserved commitments. // After user wugnot.Transfer(pad, amt), free rises; Buy locks free into caller credit. func freeWugnotOnPad() int64 { have := wugnot.BalanceOf(padAddr) free := have - reservedWugnot() if free < 0 { return 0 } return free } // FreeWugnot is free|have|reserved|totalCredit for UI preflight. func FreeWugnot() string { have := wugnot.BalanceOf(padAddr) res := reservedWugnot() free := have - res if free < 0 { free = 0 } return strconv.FormatInt(free, 10) + "|" + strconv.FormatInt(have, 10) + "|" + strconv.FormatInt(res, 10) + "|" + strconv.FormatInt(totalWugnotCredit, 10) } func prepaidOf(addr address) int64 { if !addr.IsValid() { return 0 } v := wugnotCredit.Get(addr.String()) if v == nil { return 0 } n, ok := v.(int64) if !ok { return 0 } return n } func addPrepaid(addr address, amount int64) { if amount <= 0 || !addr.IsValid() { return } next := prepaidOf(addr) + amount wugnotCredit.Set(addr.String(), next) totalWugnotCredit += amount } func subPrepaid(addr address, amount int64) { if amount <= 0 { return } have := prepaidOf(addr) if have < amount { panic("pad: prepaid WUGNOT underfunded") } next := have - amount if next == 0 { wugnotCredit.Remove(addr.String()) } else { wugnotCredit.Set(addr.String(), next) } totalWugnotCredit -= amount } // PrepaidBalance returns caller's claimable WUGNOT credit on this pad. func PrepaidBalance(cur realm) int64 { if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } return prepaidOf(cur.Previous().Address()) } // PrepaidOf returns claimable credit for an address (read helper). func PrepaidOf(addr string) int64 { return prepaidOf(address(addr)) } // ClaimWugnot withdraws all of the caller's prepaid WUGNOT credit to their wallet. // Safe after Buy overpay or after graduate/list — excess stays claimable until claimed. func ClaimWugnot(cur realm) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } user := cur.Previous().Address() amt := prepaidOf(user) if amt <= 0 { return 0 } subPrepaid(user, amt) payWugnotOut(cur, user, amt) chain.Emit("ClaimWugnot", "user", user.String(), "amount", strconv.FormatInt(amt, 10), ) return amt } // takeWugnotIn collects amount WUGNOT as payment for Buy/SwapBuy. // // 1) Auto-lock free float into caller's prepaid (user must Transfer to pad first). // 2) Spend amount from prepaid (overpay stays as credit via creditRefund). // No TransferFrom. // // Tests (testSkipBanker): OriginSend ugnot as synthetic collateral units. func takeWugnotIn(cur realm, amount int64) int64 { if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if testSkipBanker { // Local tests: OriginSend ugnot stands in for WUGNOT units. return requireUserPayment(cur) } if amount <= 0 { panic("pad: amountWugnot must be positive") } user := cur.Previous().Address() have := prepaidOf(user) if have < amount { need := amount - have free := freeWugnotOnPad() if free < need { havePad := wugnot.BalanceOf(padAddr) res := reservedWugnot() panic("pad: Transfer " + strconv.FormatInt(need, 10) + " more WUGNOT to pad then Buy (free=" + strconv.FormatInt(free, 10) + " prepaid=" + strconv.FormatInt(have, 10) + " have=" + strconv.FormatInt(havePad, 10) + " reserved=" + strconv.FormatInt(res, 10) + " needTotal=" + strconv.FormatInt(amount, 10) + ")") } // Lock free into this caller's prepaid (raises reserved, lowers free). addPrepaid(user, need) } subPrepaid(user, amount) return amount } // creditRefund keeps unused Buy WUGNOT as claimable prepaid (not instant wallet refund). // User calls ClaimWugnot after buy / after list when convenient. func creditRefund(buyer address, amount int64) { if testSkipBanker || amount <= 0 { return } addPrepaid(buyer, amount) } // payWugnotOut sends WUGNOT from pad to user (production). // Tests: pay ugnot via banker when testSkipBanker. func payWugnotOut(cur realm, to address, amount int64) { if amount <= 0 { return } if testSkipBanker { sendUgnot(cur, to, amount) return } have := wugnot.BalanceOf(cur.Address()) if have < amount { amount = have } if amount > 0 { wugnot.Transfer(cross(cur), to, amount) } } func noteBuyer(l *Launch, buyer address) { k := buyer.String() if l.UniqueBuyers.Has(k) { return } l.UniqueBuyers.Set(k, true) l.BuyerCount++ } func tradeKey(n int64) string { s := strconv.FormatInt(n, 10) for len(s) < 12 { s = "0" + s } return s } // spotPriceScaled returns ugnot/token * 1e6 from current curve or pool reserves. func spotPriceScaled(l *Launch) int64 { if l.Status == StatusGraduated { if l.PoolToken <= 0 { return 0 } return l.PoolUgnot * 1000000 / l.PoolToken } if l.VirtualToken <= 0 { return 0 } return l.VirtualUgnot * 1000000 / l.VirtualToken } func recordTrade(l *Launch, side int, ugnot, tokens int64) { l.NextTrade++ t := &Trade{ Height: runtime.ChainHeight(), Side: side, Ugnot: ugnot, Tokens: tokens, Price: spotPriceScaled(l), } l.Trades.Set(tradeKey(l.NextTrade), t) // Ring buffer: drop oldest while over cap. for l.Trades.Size() > MaxTradeHistory { oldest := "" l.Trades.Iterate("", "", func(k string, _ any) bool { oldest = k return true // stop }) if oldest == "" { break } l.Trades.Remove(oldest) } } func maybeRefundBond(cur realm, l *Launch) { if l.BondRefunded || l.BondUgnot <= 0 { return } if l.BuyerCount < BondRefundBuyers { return } // Quality gate: pure sybil micro-buys cannot refund bond. if l.RaisedUgnot < BondRefundMinRaised { return } if runtime.ChainHeight()-l.Created > BondRefundMaxHeights { return } amt := l.BondUgnot l.BondUgnot = 0 l.BondRefunded = true sendUgnot(cur, l.Creator, amt) chain.Emit("BondRefund", "id", l.ID, "amount", strconv.FormatInt(amt, 10)) } // requiredCreateBond returns ugnot the creator must send. // Production: createbond.CurrentBondUgnot() (promo or normal). // Unit tests (testSkipBanker): local CreateBondUgnot constant. func requiredCreateBond() int64 { if testSkipBanker { return CreateBondUgnot } return createbond.CurrentBondUgnot() } // CreateBondRequired is a public alias for UIs / qeval (same as requiredCreateBond). func CreateBondRequired() int64 { return requiredCreateBond() } // requiredListFeeGns returns GNS base units the creator must pre-fund (free on pad). // Live value from SetListFeeGns; falls back to ListFeeGns const. func requiredListFeeGns() int64 { if listFeeGnsLive > 0 { return listFeeGnsLive } if ListFeeGns > 0 { return ListFeeGns } return 100_000_000 } // ListFeeRequired is a public alias for UIs / qeval. func ListFeeRequired() int64 { return requiredListFeeGns() } // freeGnsOnPad is GNS on pad above per-launch list-fee escrow. func freeGnsOnPad() int64 { if testSkipBanker { // Tests skip GNS inventory checks. return requiredListFeeGns() } have := gns.BalanceOf(padAddr) free := have - totalListFeeGns if free < 0 { return 0 } return free } // FreeGns returns free|have|reserved for UI preflight (GNS list fee). func FreeGns() string { have := int64(0) if !testSkipBanker { have = gns.BalanceOf(padAddr) } free := freeGnsOnPad() return strconv.FormatInt(free, 10) + "|" + strconv.FormatInt(have, 10) + "|" + strconv.FormatInt(totalListFeeGns, 10) } // lockListFeeEscrow earmarks free GNS for this launch's CreatePool fee. func lockListFeeEscrow(l *Launch, amt int64) { if l == nil || amt <= 0 { return } if freeGnsOnPad() < amt { have := int64(0) if !testSkipBanker { have = gns.BalanceOf(padAddr) } panic("pad: Transfer " + strconv.FormatInt(amt, 10) + " GNS to pad then Create (free=" + strconv.FormatInt(freeGnsOnPad(), 10) + " have=" + strconv.FormatInt(have, 10) + " reserved=" + strconv.FormatInt(totalListFeeGns, 10) + ")") } totalListFeeGns += amt l.ListFeeGns = amt l.ListFeeConsumed = false } // listFeeForVenue: gnoswap → ListFeeRequired; zdex / other → 0. func listFeeForVenue(venueId string) int64 { if normalizeVenueID(venueId) == VenueGnoswap { return requiredListFeeGns() } return 0 } // ListFeeRequiredFor is venue-aware Create fee for UI preflight. func ListFeeRequiredFor(venueId string) int64 { ensureListVenuesSeeded() return listFeeForVenue(venueId) } // unlockListFeeEscrow clears launch GNS escrow; refund sends GNS back to creator. func unlockListFeeEscrow(cur realm, l *Launch, refund bool) { if l == nil || l.ListFeeGns <= 0 || l.ListFeeConsumed { return } amt := l.ListFeeGns l.ListFeeGns = 0 totalListFeeGns -= amt if totalListFeeGns < 0 { totalListFeeGns = 0 } if refund && !testSkipBanker && amt > 0 { have := gns.BalanceOf(padAddr) if have < amt { amt = have } if amt > 0 { gns.Transfer(cross(cur), l.Creator, amt) } } chain.Emit("ListFeeUnlocked", "id", l.ID, "amount", strconv.FormatInt(amt, 10), "refund", strconv.FormatBool(refund), ) } // syncListFeeForVenue adjusts escrow after PreferredVenue change (pre-list only). func syncListFeeForVenue(cur realm, l *Launch, vid string) { if l == nil || l.ListFeeConsumed || l.GnoswapListed || l.ListVenue != "" { return } want := listFeeForVenue(vid) if l.ListFeeGns == want { return } if l.ListFeeGns > 0 { unlockListFeeEscrow(cur, l, true) } lockListFeeEscrow(l, want) } // consumeListFeeEscrow clears launch escrow after successful Gnoswap list // (CreatePool spent feeNeed GNS from pad balance). func consumeListFeeEscrow(l *Launch) { if l == nil || l.ListFeeGns <= 0 { return } amt := l.ListFeeGns totalListFeeGns -= amt if totalListFeeGns < 0 { totalListFeeGns = 0 } l.ListFeeGns = 0 l.ListFeeConsumed = true chain.Emit("ListFeeConsumed", "id", l.ID, "amount", strconv.FormatInt(amt, 10)) } // ClaimListFee refunds Create-time GNS escrow to the token creator if still unlisted. // Use when graduate/list will not complete or creator abandons listing. func ClaimListFee(cur realm, id string) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } l := mustLaunch(id) caller := cur.Previous().Address() if caller != l.Creator { panic("pad: only creator may claim list fee") } if l.GnoswapListed { panic("pad: already listed - list fee spent") } if l.ListFeeConsumed { panic("pad: list fee already consumed") } amt := l.ListFeeGns if amt <= 0 { return 0 } l.ListFeeGns = 0 totalListFeeGns -= amt if totalListFeeGns < 0 { totalListFeeGns = 0 } if !testSkipBanker && amt > 0 { have := gns.BalanceOf(padAddr) if have < amt { amt = have } if amt > 0 { gns.Transfer(cross(cur), l.Creator, amt) } } chain.Emit("ClaimListFee", "id", id, "creator", l.Creator.String(), "amount", strconv.FormatInt(amt, 10), ) return amt } // Create deploys a fair-launch meme. Bond amount from bond realm (or fallback const). // Also locks ListFeeGns free GNS (creator must Transfer GNS to pad first). // No pre-mint; all tradeable float starts on the bonding curve. func Create(cur realm, name, symbol, uri string) string { requireInit() sent := requireUserPayment(cur) bondNeed := requiredCreateBond() if bondNeed <= 0 { panic("pad: create bond misconfigured") } if sent < bondNeed { panic("pad: create bond underpaid") } if name == "" || symbol == "" { panic("pad: name and symbol required") } if len(symbol) > 12 { panic("pad: symbol too long") } if bySymbol.Has(symbol) { panic("pad: symbol taken") } extra := sent - bondNeed if extra > 0 { creditProtocol(extra) } creator := cur.Previous().Address() id := nextID.Next().String() // Real GRC20 bound to this pad realm (mint/burn only via pad ledger). // Decimals=0: whole-token units (matches existing trade amounts). token, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), cur) // Adena (and Gnoswap registries) resolve tokens ONLY via grc20reg under key // packagePath.SYMBOL - Token.ID() itself is packagePath.SYMBOL.seq and is // rejected as "Invalid path" if pasted into Adena without registration. // Skip in unit tests (testSkipBanker); production always registers. regKey := "" if !testSkipBanker { regKey = grc20reg.Register(cross(cur), token, symbol) } ensureListVenuesSeeded() pref := DefaultListVenue() return finishCreate(cur, id, name, symbol, uri, creator, token, ledger, regKey, bondNeed, pref) } // CreateWithVenue is Create + PreferredVenue in one MsgCall (fee escrow follows venue). // venueId: "zdex" → no GNS list escrow; "gnoswap" → ListFeeRequired GNS. func CreateWithVenue(cur realm, name, symbol, uri, venueId string) string { requireInit() sent := requireUserPayment(cur) bondNeed := requiredCreateBond() if bondNeed <= 0 { panic("pad: create bond misconfigured") } if sent < bondNeed { panic("pad: create bond underpaid") } name = strings.TrimSpace(name) symbol = strings.TrimSpace(symbol) uri = strings.TrimSpace(uri) if name == "" || symbol == "" { panic("pad: name and symbol required") } if len(symbol) > 12 { panic("pad: symbol too long") } if bySymbol.Has(symbol) { panic("pad: symbol taken") } extra := sent - bondNeed if extra > 0 { creditProtocol(extra) } creator := cur.Previous().Address() id := nextID.Next().String() token, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), cur) regKey := "" if !testSkipBanker { regKey = grc20reg.Register(cross(cur), token, symbol) } ensureListVenuesSeeded() vid := normalizeVenueID(venueId) if !venueEnabled(vid) { panic("pad: venue disabled or unknown") } return finishCreate(cur, id, name, symbol, uri, creator, token, ledger, regKey, bondNeed, vid) } func finishCreate( cur realm, id, name, symbol, uri string, creator address, token *grc20.Token, ledger *grc20.PrivateLedger, regKey string, bondNeed int64, pref string, ) string { l := &Launch{ ID: id, Name: name, Symbol: symbol, URI: uri, Creator: creator, Status: StatusCurve, Created: runtime.ChainHeight(), token: token, ledger: ledger, TokenID: token.ID(), VirtualUgnot: VirtualUgnot0, VirtualToken: VirtualToken0, UniqueBuyers: avl.Tree{}, snipeBought: avl.Tree{}, BondUgnot: bondNeed, Trades: avl.Tree{}, PreferredVenue: pref, } // Venue-aware list fee: zdex → 0 GNS; gnoswap → ListFeeRequired. lockListFeeEscrow(l, listFeeForVenue(pref)) recordTrade(l, TradeSideOpen, 0, 0) launches.Set(id, l) bySymbol.Set(symbol, id) chain.Emit("Created", "id", id, "symbol", symbol, "creator", creator.String(), "token", l.TokenID, "reg", regKey, "listFeeGns", strconv.FormatInt(l.ListFeeGns, 10), "preferredVenue", pref, ) notifyCreate(cur, creator, id) return id } // AdenaPathOf returns the grc20reg / Adena token key: packagePath.SYMBOL // (Token.ID is packagePath.SYMBOL.seq - Adena rejects that form). func AdenaPathOf(id string) string { l := mustLaunch(id) return adenaKeyFromTokenID(l.TokenID, l.Symbol) } // adenaKeyFromTokenID strips the trailing .seq from Token.ID when present. func adenaKeyFromTokenID(tokenID, symbol string) string { if tokenID == "" { return "" } // Token.ID = packagePath.symbol.seq -> registry key = packagePath.symbol suffix := "." + symbol + "." if i := strings.LastIndex(tokenID, suffix); i >= 0 { // packagePath + "." + symbol return tokenID[:i] + "." + symbol } // Already packagePath.symbol or unknown layout if strings.HasSuffix(tokenID, "."+symbol) { return tokenID } return tokenID } // maxGrossForNetIn finds largest gross ugnot <= sentMax whose fee-split netIn <= maxNet. func maxGrossForNetIn(maxNet, sentMax int64) int64 { if maxNet <= 0 || sentMax <= 0 { return 0 } lo, hi := int64(0), sentMax for lo < hi { mid := (lo + hi + 1) / 2 f := ammmath.ApplyFee(mid, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) net := f.Net + f.Remainder if net <= maxNet { lo = mid } else { hi = mid - 1 } } return lo } // readyToGraduate is true when raise met the threshold, or the entire curve // float is sold (sold-out escape: threshold may be unreachable with bad virtuals). func readyToGraduate(l *Launch) bool { if l == nil || l.Status != StatusCurve { return false } if l.RaisedUgnot <= 0 { return false } if ammmath.CanGraduate(l.RaisedUgnot, graduationThreshold()) { return true } // Curve exhausted before threshold: still graduate with whatever was raised // so the market is never permanently stuck on Buy/Graduate. return l.RealSold >= CurveSupply } // Buy spends WUGNOT on the bonding curve; credits tokens. // amountWugnot: max to spend (may overpay). UI: Deposit + Transfer(pad, amt) + Buy. // Overpay stays as claimable prepaid — ClaimWugnot anytime (incl. after list). // minTokensOut: slippage floor (0 = disabled). Auto-graduates at threshold or sold-out. // // Production: collateral is real WUGNOT on pad -> Graduate can auto-list Gnoswap. // Tests (testSkipBanker): amountWugnot ignored; OriginSend ugnot is used as units. // // Last-fill (no overshoot): // 1. Cap net so RaisedUgnot never exceeds graduationThreshold() (refund excess WUGNOT). // 2. Cap by remaining curve tokens (CurveSupply - RealSold). // // If the curve is already sold out (or raise already filled), Buy refunds the full // take and graduates when ready - no panic so users are not stuck mid-tx. func Buy(cur realm, id string, amountWugnot, minTokensOut int64) int64 { requireInit() sent := takeWugnotIn(cur, amountWugnot) l := mustLaunch(id) if l.Status != StatusCurve { panic("pad: not on curve (use SwapBuy)") } buyer := cur.Previous().Address() remainingTok := CurveSupply - l.RealSold needRaise := graduationThreshold() - l.RaisedUgnot // Already complete: refund payment and graduate (sold-out or raise-filled). if remainingTok <= 0 || needRaise <= 0 { if !readyToGraduate(l) { // Edge: zero raise with empty float should not happen in production. if remainingTok <= 0 { panic("pad: curve sold out with no raise") } panic("pad: raise filled - call Graduate") } if sent > 0 { // Keep as claimable credit (ClaimWugnot) — safe after graduate/list too. creditRefund(buyer, sent) chain.Emit("BuyRefund", "id", id, "buyer", buyer.String(), "refund", strconv.FormatInt(sent, 10), "toCredit", "1", ) } graduate(cur, l) return 0 } usedGross := sent fee := ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) // Net enters curve; remainder boosts virtual ugnot (stays as collateral). netIn := fee.Net + fee.Remainder // Max net allowed: min(user net, remaining raise, remaining tokens). maxNet := netIn if maxNet > needRaise { maxNet = needRaise } maxNetTok := ammmath.MaxNetInForTokenOut(l.VirtualUgnot, l.VirtualToken, remainingTok) if maxNetTok > 0 && maxNet > maxNetTok { maxNet = maxNetTok } if maxNet <= 0 { panic("pad: no fill capacity remaining") } // Clamp gross + recompute fee when caps bind (last-fill refund path). if maxNet < netIn { usedGross = maxGrossForNetIn(maxNet, sent) if usedGross <= 0 { panic("pad: buy too small for remaining fill") } fee = ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) netIn = fee.Net + fee.Remainder if netIn > maxNet { netIn = maxNet } } tokensOut, newVU, newVT := ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn) // Integer edge: step down net until tokens <= remaining curve supply. for tokensOut > remainingTok && netIn > 1 { netIn-- tokensOut, newVU, newVT = ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn) } if tokensOut > remainingTok || tokensOut <= 0 { panic("pad: cannot fill remaining curve supply") } // If net was reduced further, shrink usedGross so refund is correct. if netIn < maxNet || usedGross < sent { // Re-derive gross that yields this netIn (<= sent). g2 := maxGrossForNetIn(netIn, sent) if g2 > 0 && g2 < usedGross { usedGross = g2 fee = ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) // Keep curve netIn as simulated (may be slightly below fee.Net+Rem). } } // Hard safety: never overshoot graduation raise after this buy. if l.RaisedUgnot+netIn > graduationThreshold() { netIn = graduationThreshold() - l.RaisedUgnot if netIn <= 0 { panic("pad: raise filled - call Graduate") } tokensOut, newVU, newVT = ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn) for tokensOut > remainingTok && netIn > 1 { netIn-- tokensOut, newVU, newVT = ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn) } if tokensOut <= 0 { panic("pad: cannot fill remaining raise") } usedGross = maxGrossForNetIn(netIn, sent) if usedGross <= 0 { panic("pad: buy too small for remaining raise") } fee = ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) } refund := sent - usedGross if refund > 0 { // Overpay stays claimable — user ClaimWugnot anytime (incl. after list). creditRefund(buyer, refund) } requireMinOut(tokensOut, minTokensOut, "tokens out") checkAndAddSnipe(l, buyer, tokensOut) // Mutate only after all checks pass. // Fees stay as WUGNOT on pad (liabilities); netIn is raised collateral for LP. l.CreatorFees += fee.Creator creditProtocol(fee.Protocol) l.VirtualUgnot = newVU l.VirtualToken = newVT l.RealSold += tokensOut l.RaisedUgnot += netIn // Invariant: raise never exceeds threshold after Buy. if l.RaisedUgnot > graduationThreshold() { panic("pad: raise overshoot invariant") } addBal(l, buyer, tokensOut) noteBuyer(l, buyer) maybeRefundBond(cur, l) recordTrade(l, TradeSideBuy, usedGross, tokensOut) chain.Emit("Buy", "id", id, "buyer", buyer.String(), "ugnot", strconv.FormatInt(usedGross, 10), "wugnot", strconv.FormatInt(usedGross, 10), "tokens", strconv.FormatInt(tokensOut, 10), ) if refund > 0 { chain.Emit("BuyRefund", "id", id, "buyer", buyer.String(), "refund", strconv.FormatInt(refund, 10), ) } notifyTrade(cur, buyer, id, 0, usedGross) if readyToGraduate(l) { graduate(cur, l) } return tokensOut } // RemainingRaiseUgnot is net ugnot still needed to hit graduationThreshold() (0 if met/over). func RemainingRaiseUgnot(id string) int64 { l := mustLaunch(id) if l.Status != StatusCurve { return 0 } if l.RaisedUgnot >= graduationThreshold() { return 0 } return graduationThreshold() - l.RaisedUgnot } // Sell burns curve tokens and pays WUGNOT (fee on output). // minWugnotOut: slippage floor (0 = disabled). User may wugnot.Withdraw to ugnot. func Sell(cur realm, id string, tokensIn, minWugnotOut int64) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if tokensIn <= 0 { panic("pad: tokensIn must be positive") } l := mustLaunch(id) if l.Status != StatusCurve { panic("pad: not on curve (use SwapSell)") } seller := cur.Previous().Address() if balOf(l, seller) < tokensIn { panic("pad: insufficient token balance") } gross, newVU, newVT := ammmath.SellTokens(l.VirtualUgnot, l.VirtualToken, tokensIn) fee := ammmath.ApplyFeeOnOutput(gross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) requireMinOut(fee.Net, minWugnotOut, "wugnot out") // Full gross left virtual reserves; retain fee in virtual ugnot (cash stays in realm). l.VirtualUgnot = newVU + fee.Fee l.VirtualToken = newVT l.RealSold -= tokensIn if l.RealSold < 0 { l.RealSold = 0 } // User receives net; creator+protocol become fee liabilities (leave Raised). payOut := fee.Net + fee.Creator + fee.Protocol if l.RaisedUgnot >= payOut { l.RaisedUgnot -= payOut } else { l.RaisedUgnot = 0 } l.CreatorFees += fee.Creator creditProtocol(fee.Protocol) addBal(l, seller, -tokensIn) payWugnotOut(cur, seller, fee.Net) recordTrade(l, TradeSideSell, fee.Net, tokensIn) chain.Emit("Sell", "id", id, "seller", seller.String(), "tokens", strconv.FormatInt(tokensIn, 10), "ugnot", strconv.FormatInt(fee.Net, 10), "wugnot", strconv.FormatInt(fee.Net, 10), ) notifyTrade(cur, seller, id, 1, fee.Net) return fee.Net } // Graduate permissionlessly moves a ready curve into a permanently locked CPMM. // Ready when RaisedUgnot >= graduationThreshold(), or when RealSold >= CurveSupply // with RaisedUgnot > 0 (sold-out before threshold - escape hatch for unreachable raise). func Graduate(cur realm, id string) { requireInit() l := mustLaunch(id) if l.Status != StatusCurve { panic("pad: already graduated") } if !readyToGraduate(l) { panic("pad: not ready to graduate (need raise threshold or curve sold out)") } graduate(cur, l) } func graduate(cur realm, l *Launch) { if l.Status != StatusCurve { return } // Liquidity capital = all raised GNOT. Token side is sized to the current // bonding-curve spot so internal CPMM / Gnoswap open ~ last curve trade // (seamless graduate). Dumping ALL unsold tokens made DEX spot << curve exit. poolU := l.RaisedUgnot if poolU <= 0 { panic("pad: empty pool ugnot") } remaining := TotalSupply - l.RealSold if remaining <= 0 { panic("pad: no remaining tokens for liquidity") } // tokensForLP = raised * VirtualToken / VirtualUgnot (same units as reserves) poolT := remaining if l.VirtualUgnot > 0 && l.VirtualToken > 0 { needed := poolU * l.VirtualToken / l.VirtualUgnot if needed > 0 && needed < remaining { poolT = needed } } l.PoolUgnot = poolU l.PoolToken = poolT l.LeftoverTokens = remaining - poolT l.RaisedUgnot = 0 l.VirtualUgnot = 0 l.VirtualToken = 0 l.Status = StatusGraduated // Forfeit unrefunded bond to protocol at graduation if still locked. if !l.BondRefunded && l.BondUgnot > 0 { creditProtocol(l.BondUgnot) l.BondUgnot = 0 l.BondRefunded = true } // Mark graduation on chart at pool spot (matches curve exit when sized above). recordTrade(l, TradeSideOpen, poolU, poolT) // NEVER auto-list inside Buy/Graduate. // Gnoswap CreatePool/Mint does WUGNOT Approve+TransferFrom; realm spender // frame often panics "insufficient allowance" and REVERTS the entire Buy // (including the curve fill that triggered graduation). List is a separate // EOA call: pre-fund pad WUGNOT/GNS then RetryListGnoswap. // Unit tests may still force list via testForceGnoswapList. l.GnoswapReady = true listed := false if testForceGnoswapList { listed = listOnGnoswapWithFunding(cur, l, poolU, poolT) } if !listed { // Internal CPMM: PoolToken is pad-accounting reserve (not minted GRC20). // Circulating = user balances; pool side is virtual reserve PoolToken. if l.GnoswapNote == "" { if l.PreferredVenue == VenueZdex { l.GnoswapNote = "internal CPMM; ReleaseListSeed → EOA zdex CreatePool → ConfirmZdexList" } else { l.GnoswapNote = "internal CPMM; Transfer WUGNOT+GNS to pad then RetryListGnoswap" } } chain.Emit("Graduated", "id", l.ID, "poolUgnot", strconv.FormatInt(poolU, 10), "poolToken", strconv.FormatInt(poolT, 10), "token", l.TokenID, "gnoswap_listed", "0", ) return } // Listed on Gnoswap (test-only path): capital is in the CL position (NFT owned by pad). // Internal SwapBuy/Sell disabled (PoolUgnot/PoolToken kept as listing record). chain.Emit("Graduated", "id", l.ID, "poolUgnot", strconv.FormatInt(poolU, 10), "poolToken", strconv.FormatInt(poolT, 10), "token", l.TokenID, "gnoswap_listed", "1", "poolPath", l.GnoswapPoolPath, "positionId", strconv.FormatUint(l.GnoswapPositionID, 10), ) } // SetPreferredVenue sets the DEX target used when RetryList(id, "") is called. // Creator or protocol only; locked once listed. Syncs Create-time GNS escrow // (gnoswap locks ListFeeRequired; zdex unlocks/refunds escrow). func SetPreferredVenue(cur realm, id, venueId string) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } l := mustLaunch(id) caller := cur.Previous().Address() if caller != l.Creator && caller != protocolAddr { panic("pad: creator or protocol only") } if l.GnoswapListed || l.ListVenue != "" { panic("pad: already listed") } ensureListVenuesSeeded() vid := normalizeVenueID(venueId) if !venueEnabled(vid) { panic("pad: venue disabled or unknown") } l.PreferredVenue = vid syncListFeeForVenue(cur, l, vid) chain.Emit("SetPreferredVenue", "id", id, "venue", vid, "listFeeGns", strconv.FormatInt(l.ListFeeGns, 10), ) } // PreferredVenueOf returns the Create-time / SetPreferredVenue target. func PreferredVenueOf(id string) string { return mustLaunch(id).PreferredVenue } // ListSeedReleasedOf reports whether ReleaseListSeed already ran. func ListSeedReleasedOf(id string) bool { return mustLaunch(id).ListSeedReleased } // ReleaseListSeed sends graduated internal-CPMM seed to the creator for EOA zdex CreatePool: // - PoolUgnot as WUGNOT Transfer to creator (EOA may wugnot.Withdraw → ugnot OriginSend) // - PoolToken minted as GRC20 to creator (was pad-accounting only) // PoolUgnot/PoolToken kept as listing-size record; reservedWugnot skips via ListSeedReleased. // Disables internal SwapBuy/Sell. Creator or protocol may call; assets always go to creator. func ReleaseListSeed(cur realm, id string) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } l := mustLaunch(id) caller := cur.Previous().Address() if caller != l.Creator && caller != protocolAddr { panic("pad: creator or protocol only") } if l.Status != StatusGraduated { panic("pad: not graduated") } if l.GnoswapListed || l.ListVenue != "" { panic("pad: already listed") } if l.ListSeedReleased { panic("pad: list seed already released") } poolU := l.PoolUgnot poolT := l.PoolToken if poolU <= 0 || poolT <= 0 { panic("pad: empty internal pool") } // Fail-closed: must fully fund creator before killing internal CPMM. if !testSkipBanker { have := wugnot.BalanceOf(padAddr) if have < poolU { panic("pad: underfunded WUGNOT for list seed release") } } // Mark released before Transfer so reservedWugnot drops in same tx. l.ListSeedReleased = true l.PoolUgnot = 0 l.PoolToken = 0 l.LiqWugnotUsed = poolU l.GnoswapNote = "list seed released to creator for zdex CreatePool; then ConfirmZdexList" if !testSkipBanker { wugnot.Transfer(cross(cur), l.Creator, poolU) } else { payWugnotOut(cur, l.Creator, poolU) } addBal(l, l.Creator, poolT) chain.Emit("ReleaseListSeed", "id", id, "to", l.Creator.String(), "wugnot", strconv.FormatInt(poolU, 10), "tokens", strconv.FormatInt(poolT, 10), ) } // ConfirmZdexList marks a launch listed on zdex after EOA CreatePool succeeded. // poolId e.g. "ugnot|SYMBOL" (must be non-empty; no on-chain pool verify). // Creator or protocol. Prefer ReleaseListSeed first when pad still holds LP seed. func ConfirmZdexList(cur realm, id, poolId string) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } l := mustLaunch(id) caller := cur.Previous().Address() if caller != l.Creator && caller != protocolAddr { panic("pad: creator or protocol only") } if l.Status != StatusGraduated { panic("pad: not graduated") } if l.GnoswapListed || l.ListVenue != "" { panic("pad: already listed") } // Avoid marking listed while pad still holds unreleased LP capital. if !l.ListSeedReleased && (l.PoolUgnot > 0 || l.PoolToken > 0) { panic("pad: ReleaseListSeed first (or empty pool)") } poolId = strings.TrimSpace(poolId) if poolId == "" { panic("pad: empty poolId") } l.ListVenue = VenueZdex l.GnoswapListed = true // UI/compat: "listed on DEX" l.GnoswapPoolPath = poolId l.GnoswapPositionID = 0 if l.LiqWugnotUsed == 0 { l.LiqWugnotUsed = l.PoolUgnot } l.GnoswapNote = "confirmed zdex list; pool=" + poolId chain.Emit("ConfirmZdexList", "id", id, "poolPath", poolId, "symbol", l.Symbol, "venue", VenueZdex, ) chain.Emit("ListedRetry", "id", id, "venue", VenueZdex, "poolPath", poolId, "positionId", "0", "poolUgnot", strconv.FormatInt(l.PoolUgnot, 10), "poolToken", strconv.FormatInt(l.PoolToken, 10), ) } // RetryList lists a graduated launch on the chosen venue. // Permissionless EOA MsgCall. Uses PoolUgnot / PoolToken (curve-spot sized). // venueId: empty → PreferredVenue → DefaultListVenue(); unknown/disabled → soft-fail note. func RetryList(cur realm, id, venueId string) bool { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } l := mustLaunch(id) if l.Status != StatusGraduated { panic("pad: not graduated") } if l.GnoswapListed || l.ListVenue != "" { panic("pad: already listed") } if l.ListSeedReleased { panic("pad: list seed released - use ConfirmZdexList after EOA CreatePool") } poolU := l.PoolUgnot poolT := l.PoolToken if poolU <= 0 || poolT <= 0 { panic("pad: empty internal pool") } raw := strings.TrimSpace(venueId) if raw == "" && l.PreferredVenue != "" { raw = l.PreferredVenue } vid := normalizeVenueID(raw) ok := tryListVenue(cur, l, vid, poolU, poolT) if ok { chain.Emit("ListedRetry", "id", l.ID, "venue", l.ListVenue, "poolPath", l.GnoswapPoolPath, "positionId", strconv.FormatUint(l.GnoswapPositionID, 10), "poolUgnot", strconv.FormatInt(poolU, 10), "poolToken", strconv.FormatInt(poolT, 10), ) // Compat event name for indexers that listen for GnoswapListedRetry. if l.ListVenue == VenueGnoswap { chain.Emit("GnoswapListedRetry", "id", l.ID, "poolPath", l.GnoswapPoolPath, "positionId", strconv.FormatUint(l.GnoswapPositionID, 10), "poolUgnot", strconv.FormatInt(poolU, 10), "poolToken", strconv.FormatInt(poolT, 10), ) } } else if l.GnoswapNote == "" { l.GnoswapNote = "retry list failed - check ListNeed / ListNeedFor; venue=" + vid } return ok } // RetryListGnoswap is the Gnoswap-specific wrapper (compat for existing UI). func RetryListGnoswap(cur realm, id string) bool { return RetryList(cur, id, VenueGnoswap) } // TokenIDOf returns the GRC20 Token.ID() for a launch. func TokenIDOf(id string) string { return mustLaunch(id).TokenID } // GRC20Bank returns the underlying *grc20.Token for interop (metadata / external DEX). // Does not expose PrivateLedger - mint/burn stay pad-only. func GRC20Bank(id string) *grc20.Token { l := mustLaunch(id) if l.token == nil { panic("pad: no token") } return l.token } // SwapBuy buys tokens from the graduated internal pool with WUGNOT. // amountWugnot: max to spend (Approve pad). minTokensOut: slippage (0 = off). // Disabled when listed on Gnoswap (trade via router). func SwapBuy(cur realm, id string, amountWugnot, minTokensOut int64) int64 { requireInit() sent := takeWugnotIn(cur, amountWugnot) l := mustLaunch(id) if l.Status != StatusGraduated { panic("pad: not graduated (use Buy)") } if l.GnoswapListed || l.ListVenue != "" { panic("pad: listed on DEX - trade via router, not pad SwapBuy") } if l.ListSeedReleased { panic("pad: list seed released - finish zdex CreatePool + ConfirmZdexList") } buyer := cur.Previous().Address() fee := ammmath.ApplyFee(sent, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) tokensOut, newPU, newPT := ammmath.PoolSwapUgnotForToken( l.PoolUgnot, l.PoolToken, fee.Net, fee.Remainder, ) requireMinOut(tokensOut, minTokensOut, "tokens out") // Refund unused gross if pool math used less (rare); fees from sent. // Pool swap uses fee.Net into pool; full sent stays as fee+pool contribution. l.CreatorFees += fee.Creator creditProtocol(fee.Protocol) l.PoolUgnot = newPU l.PoolToken = newPT addBal(l, buyer, tokensOut) noteBuyer(l, buyer) recordTrade(l, TradeSideBuy, sent, tokensOut) chain.Emit("SwapBuy", "id", id, "buyer", buyer.String(), "ugnot", strconv.FormatInt(sent, 10), "wugnot", strconv.FormatInt(sent, 10), "tokens", strconv.FormatInt(tokensOut, 10), ) notifyTrade(cur, buyer, id, 0, sent) return tokensOut } // SwapSell sells tokens into the graduated pool for WUGNOT. // minWugnotOut: slippage floor (0 = disabled). func SwapSell(cur realm, id string, tokensIn, minWugnotOut int64) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if tokensIn <= 0 { panic("pad: tokensIn must be positive") } l := mustLaunch(id) if l.Status != StatusGraduated { panic("pad: not graduated (use Sell)") } if l.GnoswapListed || l.ListVenue != "" { panic("pad: listed on DEX - trade via router, not pad SwapSell") } if l.ListSeedReleased { panic("pad: list seed released - finish zdex CreatePool + ConfirmZdexList") } seller := cur.Previous().Address() if balOf(l, seller) < tokensIn { panic("pad: insufficient token balance") } gross, newPU, newPT := ammmath.PoolSwapTokenForUgnot(l.PoolUgnot, l.PoolToken, tokensIn) fee := ammmath.ApplyFeeOnOutput(gross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS) requireMinOut(fee.Net, minWugnotOut, "wugnot out") // Retain fee in pool ugnot (cash stays); user gets net WUGNOT. l.PoolUgnot = newPU + fee.Fee l.PoolToken = newPT l.CreatorFees += fee.Creator creditProtocol(fee.Protocol) addBal(l, seller, -tokensIn) payWugnotOut(cur, seller, fee.Net) recordTrade(l, TradeSideSell, fee.Net, tokensIn) chain.Emit("SwapSell", "id", id, "seller", seller.String(), "tokens", strconv.FormatInt(tokensIn, 10), "ugnot", strconv.FormatInt(fee.Net, 10), "wugnot", strconv.FormatInt(fee.Net, 10), ) notifyTrade(cur, seller, id, 1, fee.Net) return fee.Net } // Transfer moves GRC20 tokens between addresses (user-initiated). func Transfer(cur realm, id string, to address, amount int64) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if amount <= 0 { panic("pad: amount must be positive") } if !to.IsValid() { panic("pad: invalid to") } l := mustLaunch(id) from := cur.Previous().Address() if from == to { panic("pad: self transfer") } if l.ledger == nil { panic("pad: no GRC20 ledger") } if err := l.ledger.Transfer(from, to, amount); err != nil { panic("pad: transfer: " + err.Error()) } chain.Emit("Transfer", "id", id, "from", from.String(), "to", to.String(), "amount", strconv.FormatInt(amount, 10)) } // Approve sets GRC20 allowance so DEX/contracts can TransferFrom. func Approve(cur realm, id string, spender address, amount int64) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if !spender.IsValid() { panic("pad: invalid spender") } l := mustLaunch(id) if l.ledger == nil { panic("pad: no GRC20 ledger") } owner := cur.Previous().Address() if err := l.ledger.Approve(owner, spender, amount); err != nil { panic("pad: approve: " + err.Error()) } chain.Emit("Approval", "id", id, "owner", owner.String(), "spender", spender.String(), "amount", strconv.FormatInt(amount, 10)) } // TransferFrom spends allowance: spender = MsgCall EOA caller. // Enables DEX / routers that hold allowance from Approve. func TransferFrom(cur realm, id string, from, to address, amount int64) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if amount <= 0 { panic("pad: amount must be positive") } if !from.IsValid() || !to.IsValid() { panic("pad: invalid address") } if from == to { panic("pad: self transfer") } l := mustLaunch(id) if l.ledger == nil { panic("pad: no GRC20 ledger") } spender := cur.Previous().Address() if err := l.ledger.TransferFrom(from, spender, to, amount); err != nil { panic("pad: transferFrom: " + err.Error()) } chain.Emit("TransferFrom", "id", id, "from", from.String(), "to", to.String(), "spender", spender.String(), "amount", strconv.FormatInt(amount, 10)) } // ClaimCreatorFees withdraws accrued creator fees for a launch. // Only the token creator may claim. Fees stay on pad until claimed. func ClaimCreatorFees(cur realm, id string) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } l := mustLaunch(id) caller := cur.Previous().Address() if caller != l.Creator { panic("pad: not creator") } amt := l.CreatorFees if amt <= 0 { return 0 } l.CreatorFees = 0 // Production: fees accrue as WUGNOT on pad. Tests pay ugnot via payWugnotOut. payWugnotOut(cur, caller, amt) chain.Emit("ClaimCreator", "id", id, "amount", strconv.FormatInt(amt, 10)) return amt } // payoutProtocolFees sends all pending protocolFees to protocolAddr. // Shared by ClaimProtocolFees and PushProtocolFees. func payoutProtocolFees(cur realm) int64 { amt := protocolFees if amt <= 0 { return 0 } if !protocolAddr.IsValid() { panic("pad: protocol address unset") } protocolFees = 0 protocolFeesPaid += amt payWugnotOut(cur, protocolAddr, amt) chain.Emit("ClaimProtocol", "to", protocolAddr.String(), "amount", strconv.FormatInt(amt, 10), ) return amt } // ClaimProtocolFees withdraws pending protocol fees to protocolAddr. // Only the current protocol treasury key may call (same wallet that Init'd, // unless TransferProtocol was used). func ClaimProtocolFees(cur realm) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if cur.Previous().Address() != protocolAddr { panic("pad: not protocol") } return payoutProtocolFees(cur) } // PushProtocolFees sends pending protocol fees to protocolAddr. // Permissionless: anyone may call so treasury can be paid without the protocol // key signing (still only pays the configured protocolAddr). func PushProtocolFees(cur realm) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } return payoutProtocolFees(cur) } // TransferProtocol rotates the protocol fee recipient (current protocol only). // Pending protocolFees stay on pad until claimed/pushed to the *new* address. func TransferProtocol(cur realm, newAddr address) { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if cur.Previous().Address() != protocolAddr { panic("pad: not protocol") } if !newAddr.IsValid() { panic("pad: invalid new protocol address") } if newAddr == protocolAddr { panic("pad: same protocol address") } old := protocolAddr protocolAddr = newAddr chain.Emit("TransferProtocol", "from", old.String(), "to", newAddr.String()) } // ProtocolAddress returns the current protocol treasury address (bech32). func ProtocolAddress() string { return protocolAddr.String() } // ProtocolFeesPaid returns lifetime ugnot already paid out to the treasury. func ProtocolFeesPaid() int64 { return protocolFeesPaid } // FeeInfo returns protocolAddr|pendingUgnot|paidUgnot for UIs. func FeeInfo() string { return protocolAddr.String() + "|" + strconv.FormatInt(protocolFees, 10) + "|" + strconv.FormatInt(protocolFeesPaid, 10) } // PadAddress returns this pad realm's bech32 package address (fund WUGNOT here). func PadAddress() string { return padAddr.String() } // AdminInfo is a single-line dashboard snapshot for the ops UI: // // protocolAddr|pendingFees|paidFees|reservedUgnot|launchCount|pointsOn|inited|padAddr // // pointsOn/inited are 0|1. func AdminInfo() string { pts := "0" if pointsEnabled { pts = "1" } ini := "0" if inited { ini = "1" } return protocolAddr.String() + "|" + strconv.FormatInt(protocolFees, 10) + "|" + strconv.FormatInt(protocolFeesPaid, 10) + "|" + strconv.FormatInt(reservedUgnot(), 10) + "|" + strconv.Itoa(launches.Size()) + "|" + pts + "|" + ini + "|" + padAddr.String() } // IsProtocol reports whether addr is the current treasury (for UI gating). func IsProtocol(addr string) bool { if !inited || !protocolAddr.IsValid() { return false } return protocolAddr.String() == addr } // reservedUgnot is ugnot the pad must keep to honor user/creator liabilities // and active markets (curve raised, internal CPMM, bonds, pending fees). func reservedUgnot() int64 { reserved := protocolFees launches.Iterate("", "", func(_ string, value any) bool { l := value.(*Launch) reserved += l.CreatorFees if !l.BondRefunded { reserved += l.BondUgnot } if l.Status == StatusCurve { reserved += l.RaisedUgnot } // Internal CPMM (fallback when not listed / seed not released). if l.Status == StatusGraduated && !l.GnoswapListed && !l.ListSeedReleased { reserved += l.PoolUgnot } return false }) return reserved } // ReservedUgnot is ugnot the pad must keep for markets + pending claims. func ReservedUgnot() int64 { return reservedUgnot() } // freeUgnot reports bank ugnot above reserved liabilities (0 if short/test). func freeUgnot(cur realm) int64 { if testSkipBanker { return 0 } bk := banker.NewBanker(banker.BankerTypeReadonly, cur) bal := bk.GetCoins(cur.Address()).AmountOf(DenomUgnot) free := bal - reservedUgnot() if free < 0 { return 0 } return free } // WithdrawProtocolUgnot lets the treasury pull free ugnot from the pad bank // (e.g. raised backlog after Gnoswap list, to re-wrap as WUGNOT inventory). // Capped by free balance; panics if amount > free. func WithdrawProtocolUgnot(cur realm, amount int64) int64 { requireInit() if !cur.Previous().IsUserCall() { panic("pad: must be EOA MsgCall") } if cur.Previous().Address() != protocolAddr { panic("pad: not protocol") } if amount <= 0 { panic("pad: amount must be positive") } free := freeUgnot(cur) if amount > free { panic("pad: amount exceeds free ugnot (reserved for markets/fees)") } sendUgnot(cur, protocolAddr, amount) chain.Emit("WithdrawProtocolUgnot", "to", protocolAddr.String(), "amount", strconv.FormatInt(amount, 10), "freeLeft", strconv.FormatInt(free-amount, 10), ) return amount } // --- read helpers (non-crossing) --- func BalanceOf(id string, owner address) int64 { return balOf(mustLaunch(id), owner) } // ListBuyers returns unique buyer addresses (one per line), capped for query size. // Only addresses that bought at least once on this pad (UniqueBuyers). Not full GRC20 holders // who received tokens via transfer. func ListBuyers(id string) string { l := mustLaunch(id) const maxN = 100 out := "" n := 0 l.UniqueBuyers.Iterate("", "", func(key string, _ any) bool { if n >= maxN { return true } if out != "" { out += "\n" } out += key n++ return false }) return out } func GetStatus(id string) int { return mustLaunch(id).Status } func GetRaised(id string) int64 { return mustLaunch(id).RaisedUgnot } func GetPool(id string) (ugnot, token int64) { l := mustLaunch(id) return l.PoolUgnot, l.PoolToken } func GetCreatorFees(id string) int64 { return mustLaunch(id).CreatorFees } func ProtocolFees() int64 { return protocolFees } func LaunchCount() int { return launches.Size() } func ResolveSymbol(symbol string) string { s, ok := bySymbol.Get(symbol).(string) if !ok { return "" } return s } // ListIDs returns newline-separated launch IDs (sorted by AVL key / creation order). func ListIDs() string { out := "" launches.Iterate("", "", func(key string, _ any) bool { if out != "" { out += "\n" } out += key return false }) return out } // LaunchInfo returns a single-line pipe-delimited summary for UIs/indexers: // // id|name|symbol|status|raised|sold|buyers|creatorFees|poolUgnot|poolToken|uri|creator|virtualUgnot|virtualToken|created|tokenID|gnoswapReady|gnoswapListed|gnoswapPoolPath|gnoswapNote|listVenue // // status: 0=curve 1=graduated; gnoswapReady/listed: 0|1 // gnoswapNote: optional (padv12+); pipes stripped for delimiter safety. // listVenue: optional (padv23+); empty if unlisted. func LaunchInfo(id string) string { l := mustLaunch(id) gs := "0" if l.GnoswapReady { gs = "1" } gl := "0" if l.GnoswapListed { gl = "1" } note := strings.ReplaceAll(l.GnoswapNote, "|", "/") venue := strings.ReplaceAll(l.ListVenue, "|", "/") return l.ID + "|" + l.Name + "|" + l.Symbol + "|" + strconv.Itoa(l.Status) + "|" + strconv.FormatInt(l.RaisedUgnot, 10) + "|" + strconv.FormatInt(l.RealSold, 10) + "|" + strconv.Itoa(l.BuyerCount) + "|" + strconv.FormatInt(l.CreatorFees, 10) + "|" + strconv.FormatInt(l.PoolUgnot, 10) + "|" + strconv.FormatInt(l.PoolToken, 10) + "|" + l.URI + "|" + l.Creator.String() + "|" + strconv.FormatInt(l.VirtualUgnot, 10) + "|" + strconv.FormatInt(l.VirtualToken, 10) + "|" + strconv.FormatInt(l.Created, 10) + "|" + l.TokenID + "|" + gs + "|" + gl + "|" + l.GnoswapPoolPath + "|" + note + "|" + venue } // ParamsInfo returns parameters for UI display. // total|curve|poolSeed|gradThreshold|feeBps|createBond|listFeeGns // createBond is live from bond realm when not in unit-test mode. // listFeeGns: Create-time GNS escrow required (padv20+). func ParamsInfo() string { return strconv.FormatInt(TotalSupply, 10) + "|" + strconv.FormatInt(CurveSupply, 10) + "|" + strconv.FormatInt(PoolSeed, 10) + "|" + strconv.FormatInt(graduationThreshold(), 10) + "|" + strconv.FormatInt(FeeBPS, 10) + "|" + strconv.FormatInt(requiredCreateBond(), 10) + "|" + strconv.FormatInt(requiredListFeeGns(), 10) } // TradeHistory returns newline-separated chart points: // // height|side|ugnot|tokens|priceScaled // // side: 0=buy 1=sell 2=open/graduate. Ordered oldest -> newest. func TradeHistory(id string) string { l := mustLaunch(id) out := "" l.Trades.Iterate("", "", func(_ string, value any) bool { t := value.(*Trade) line := strconv.FormatInt(t.Height, 10) + "|" + strconv.Itoa(t.Side) + "|" + strconv.FormatInt(t.Ugnot, 10) + "|" + strconv.FormatInt(t.Tokens, 10) + "|" + strconv.FormatInt(t.Price, 10) if out != "" { out += "\n" } out += line return false }) return out } // TradeCount returns number of stored chart samples for a launch. func TradeCount(id string) int { return mustLaunch(id).Trades.Size() } // resetForTest clears package state between unit tests. func resetForTest() { launches = avl.Tree{} bySymbol = avl.Tree{} nextID = 0 nextTokenID = 0 var zero address protocolAddr = zero // padAddr is set in package init - do not clear (realm address is fixed). protocolFees = 0 protocolFeesPaid = 0 inited = false pointsEnabled = false graduationUgnot = 0 listFeeGnsLive = 0 testSkipBanker = true // unit tests skip banker; integration/chain tests leave false testForceGnoswapList = false testForceZdexList = false totalListFeeGns = 0 totalWugnotCredit = 0 wugnotCredit = avl.Tree{} resetListVenuesForTest() }
  10. #10params.gno
  11. #11package pad // Protocol parameters. // Defaults are compile-time constants; GraduationThreshold and ListFeeGns also have // live copies set in Init and adjustable by protocol via SetGraduationThreshold / // SetListFeeGns (see memepad.gno). Bond create fee is owned by the bond realm. const ( // Token economics TotalSupply int64 = 1_000_000_000 CurveSupply int64 = 800_000_000 // sold on bonding curve // PoolSeed is legacy. Graduation sizes LP tokens to curve spot // (raised * VirtualToken / VirtualUgnot); unsold remainder = LeftoverTokens. PoolSeed int64 = 200_000_000 // Virtual constant-product seed (Pump-style). // Max net raise when selling full CurveSupply: // R = VirtualUgnot0 * CurveSupply / (VirtualToken0 - CurveSupply) // Pearl default: VU0=3.5e9 + VT0≈1.073e9 => max raise ≈10.2k GNOT so // DefaultGraduationThreshold 10_000 GNOT is reachable before sold-out. VirtualUgnot0 int64 = 102_375_071_625 // mainnet: max raise ≥ 300k GNOT VirtualToken0 int64 = 1_073_000_191 // virtual token side // Default graduation raise (ugnot). Live value starts here in Init; // protocol may call SetGraduationThreshold afterward. GraduationThreshold int64 = 300_000_000_000 // 300_000 GNOT (SetGraduationThreshold later) // Fees: 1.20% total. Of the fee: 40% creator, 40% protocol, 20% LP/k remainder. FeeBPS int64 = 120 CreatorFeeShareBPS int64 = 4000 ProtocolFeeShareBPS int64 = 4000 // Fallback create bond for unit tests / if bond realm unavailable. // Production pad uses createbond.CurrentBondUgnot() (see bond package). // Sapphire normal bond = 2 GNOT; promo ~ $20 in ugnot set on bond.StartPromo. CreateBondUgnot int64 = 150_000_000 // 150 GNOT fallback; live = bond realm BondRefundBuyers int = 5 BondRefundMinRaised int64 = 5_000_000 // >=5 GNOT raised BondRefundMaxHeights int64 = 100_000 // Anti-snipe: first N heights, max cumulative tokens per address (BPS of TotalSupply). AntiSnipeHeights int64 = 20 AntiSnipeMaxBuyBPS int64 = 500 // 5% per address in window // Status StatusCurve = 0 StatusGraduated = 1 // Trade history ring buffer. MaxTradeHistory = 128 // Trade sides stored in history TradeSideBuy = 0 TradeSideSell = 1 TradeSideOpen = 2 // initial listing mark DenomUgnot = "ugnot" // --- Gnoswap auto-list (Sapphire) --- // Curve collateral is WUGNOT (Buy TransferFrom). At graduate, pad already holds // raised WUGNOT for LP - no realm self-wrap needed. // CreatePool fee is fixed in GNS (typically 100e6 = 100 GNS): // Fee = pre-funded GNS on pad OR surplus WUGNOT (fees retained) ExactOut -> GNS GnoswapFeeTier uint32 = 3000 // 0.3% meme tier GnoswapTickSpacing int32 = 60 GnoswapMinTick int32 = -887272 GnoswapMaxTick int32 = 887272 // Max WUGNOT used as ExactOut maxIn for fee buy (ceiling / slippage). // Prefer pre-funding >=100 GNS on pad so LP depth stays = raised. GnoswapMaxFeeWugnot int64 = 1_500_000_000 // 1500 GNOT // Create-time GNS list fee escrow (padv20+). // Creator must Transfer >= ListFeeGns free GNS to pad before Create. // Locked per-launch until Gnoswap list consumes it, or ClaimListFee refunds creator. // Matches typical gnspool.GetPoolCreationFee() (100 GNS, 6 decimals). ListFeeGns int64 = 100_000_000 )
  12. #12render.gno
  13. #13package pad import ( "strconv" "strings" "gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/ammmath" "gno.land/p/nt/ufmt/v0" ) // Render provides on-chain discovery (gnoweb). Read-only by convention. // // Paths: // // "" - home / list // token/{id} - launch detail // help - API help func Render(path string) string { path = strings.Trim(path, "/") if path == "" { return renderHome() } if path == "help" { return renderHelp() } if strings.HasPrefix(path, "token/") { id := strings.TrimPrefix(path, "token/") return renderToken(id) } return "> [!WARNING]\n> Path not found: " + sanitize(path) + "\n" } func sanitize(s string) string { s = strings.ReplaceAll(s, "`", "'") s = strings.ReplaceAll(s, "<", "") s = strings.ReplaceAll(s, ">", "") return s } func renderHome() string { out := "# gnomi\n\n" out += "Meme launchpad - WUGNOT bonding curve -> remaining tokens + raised WUGNOT seed Gnoswap LP (auto-list).\n\n" if !inited { out += "> [!CAUTION]\n> Protocol not initialized. Call Init first.\n\n" return out } out += ufmt.Sprintf("- Launches: **%d**\n", launches.Size()) if protocolAddr.IsValid() { out += ufmt.Sprintf("- Protocol treasury: `%s`\n", protocolAddr.String()) } out += ufmt.Sprintf("- Protocol fees pending (claim/push): **%d** ugnot\n", protocolFees) out += ufmt.Sprintf("- Protocol fees paid (lifetime): **%d** ugnot\n\n", protocolFeesPaid) out += "## Markets\n\n" if launches.Size() == 0 { out += "_No launches yet. Call Create with name, symbol, uri and bond._\n" return out } launches.Iterate("", "", func(key string, value any) bool { l := value.(*Launch) st := "curve" if l.Status == StatusGraduated { st = "graduated" } out += ufmt.Sprintf("- [%s ($%s)](token/%s) - %s - raised %d ugnot - buyers %d\n", sanitize(l.Name), sanitize(l.Symbol), l.ID, st, l.RaisedUgnot, l.BuyerCount) return false }) out += "\n[Help](help)\n" return out } func renderToken(id string) string { id = sanitize(id) l, ok := launches.Get(id).(*Launch) if !ok { return "> [!WARNING]\n> Unknown launch " + id + "\n\n[Home](./)\n" } st := "bonding curve" if l.Status == StatusGraduated { st = "graduated pool (LP locked)" } out := ufmt.Sprintf("# %s ($%s)\n\n", sanitize(l.Name), sanitize(l.Symbol)) out += ufmt.Sprintf("- **ID:** %s\n", l.ID) out += ufmt.Sprintf("- **Status:** %s\n", st) out += ufmt.Sprintf("- **Creator:** %s\n", l.Creator.String()) if l.URI != "" { out += ufmt.Sprintf("- **URI:** %s\n", sanitize(l.URI)) } out += ufmt.Sprintf("- **Unique buyers:** %d\n", l.BuyerCount) out += ufmt.Sprintf("- **Creator fees (claimable):** %d ugnot\n\n", l.CreatorFees) if l.Status == StatusCurve { remaining := CurveSupply - l.RealSold pct := int64(0) if graduationThreshold() > 0 { pct = l.RaisedUgnot * 100 / graduationThreshold() if pct > 100 { pct = 100 } } spot := ammmath.SpotPriceUgnotPerToken(l.VirtualUgnot, l.VirtualToken) out += "## Bonding curve\n\n" out += ufmt.Sprintf("- Raised: **%d** / %d ugnot (%d%%)\n", l.RaisedUgnot, graduationThreshold(), pct) out += ufmt.Sprintf("- Tokens sold: **%d** / %d\n", l.RealSold, CurveSupply) out += ufmt.Sprintf("- Remaining on curve: **%d**\n", remaining) out += ufmt.Sprintf("- Spot (ugnot/token x1e6): **%d**\n", spot) out += "\nProgress toward graduation:\n\n" out += progressBar(pct) + "\n" } else { spot := ammmath.SpotPriceUgnotPerToken(l.PoolUgnot, l.PoolToken) out += "## Liquidity (post-graduate)\n\n" out += ufmt.Sprintf("- Raised capital (record): **%d** ugnot\n", l.PoolUgnot) out += ufmt.Sprintf("- LP tokens (curve-spot sized): **%d**\n", l.PoolToken) if l.LeftoverTokens > 0 { out += ufmt.Sprintf("- Leftover on pad (not in LP): **%d**\n", l.LeftoverTokens) } out += ufmt.Sprintf("- Spot (ugnot/token x1e6): **%d**\n", spot) if l.GnoswapListed { out += ufmt.Sprintf("- **Gnoswap listed:** `%s`\n", sanitize(l.GnoswapPoolPath)) out += ufmt.Sprintf("- Position NFT id: **%d** (pad-owned, locked)\n", l.GnoswapPositionID) out += ufmt.Sprintf("- Fee WUGNOT spent: **%d** | LP WUGNOT: **%d**\n", l.FeeWugnotSpent, l.LiqWugnotUsed) out += "\n> [!NOTE]\n> Trade on Gnoswap router - pad SwapBuy/SwapSell disabled after auto-list.\n" } else { out += ufmt.Sprintf("- Gnoswap: %s\n", sanitize(l.GnoswapNote)) out += "\n> [!NOTE]\n> Internal CPMM. List: wugnot.Deposit -> Approve(pad) -> RetryListGnoswap (LP ugnot reimbursed; prefer GNS for fee).\n" out += ufmt.Sprintf("- ListNeed: `%s`\n", listNeedOf(l)) } } out += "\n## Trade\n\n" out += "Use wallet MsgCall (payable txs):\n\n" if l.Status == StatusCurve { out += ufmt.Sprintf("- Buy(id) with -send ugnot - id=%s\n", l.ID) out += "- Sell(id, tokensIn)\n" out += "- Graduate(id) when threshold met\n" } else if l.GnoswapListed { out += "- Trade on Gnoswap router (pad SwapBuy/Sell disabled)\n" out += ufmt.Sprintf("- Pool: `%s`\n", sanitize(l.GnoswapPoolPath)) } else { out += "- SwapBuy(id) with -send ugnot\n" out += "- SwapSell(id, tokensIn)\n" out += ufmt.Sprintf("- RetryListGnoswap(id) - auto-pull WUGNOT/GNS from caller if Approved - id=%s\n", l.ID) } out += "\n[Home](./) | [Help](help)\n" return out } func progressBar(pct int64) string { if pct < 0 { pct = 0 } if pct > 100 { pct = 100 } filled := int(pct / 5) bar := "[" for i := 0; i < 20; i++ { if i < filled { bar += "#" } else { bar += "." } } bar += "] " + strconv.FormatInt(pct, 10) + "%" return bar } func renderHelp() string { out := "# gnomi API\n\n" out += "## Lifecycle\n\n" out += "1. Init() - once; caller becomes protocol treasury\n" out += "2. Create(name, symbol, uri) + bond from bond realm (promo/normal)\n" out += "3. Buy(id, minTokensOut) / Sell(id, amount, minUgnotOut) - bonding curve\n" out += "4. Auto or Graduate(id) at threshold\n" out += "5. Graduate seeds remaining tokens + raised as liquidity\n" out += " -> auto Gnoswap CreatePool+Mint if pad has WUGNOT inventory\n" out += " -> else locked internal CPMM (SwapBuy/SwapSell)\n" out += "6. Transfer / Approve / TransferFrom - GRC20\n" out += "7. Fees:\n" out += " - Creator: ClaimCreatorFees(id) - only creator, **must claim**\n" out += " - Protocol: ClaimProtocolFees() (treasury) or PushProtocolFees() (anyone -> treasury)\n" out += " - TransferProtocol(newAddr) / WithdrawProtocolUgnot(amount) - treasury ops\n" out += " - Gnoswap CreatePool GNS fee goes to Gnoswap, not pad treasury\n\n" out += "## Params (MVP)\n\n" out += ufmt.Sprintf("- Total supply: %d\n", TotalSupply) out += ufmt.Sprintf("- Curve sale: %d\n", CurveSupply) out += ufmt.Sprintf("- LP tokens at grad: TotalSupply - RealSold (was PoolSeed=%d)\n", PoolSeed) out += ufmt.Sprintf("- Graduation: %d ugnot\n", graduationThreshold()) out += ufmt.Sprintf("- Fee: %d BPS\n", FeeBPS) out += ufmt.Sprintf("- Create bond (live): %d ugnot\n\n", requiredCreateBond()) out += "## Design\n\n" out += "- Remaining tokens + raised GNOT -> liquidity at graduate\n" out += "- Part of WUGNOT inventory swaps to GNS for CreatePool fee\n" out += "- Permanent LP lock (pad holds Gnoswap position NFT)\n" out += "- EOA + OriginSend payment guards\n\n" out += "[Home](./)\n" return out }
  14. #14zdex_list.gno
  15. #15package pad // tryListOnZdex — stub until Zdex hub allows farm/realm CreatePool. // Live Pearl/mainnet: CreatePool is EOA-only + requireListed. // Pad path today: // 1) ReleaseListSeed(id) — WUGNOT + minted PoolToken → creator // 2) EOA: optional wugnot.Withdraw → Approve(zdex RealmAddr) → CreatePool // 3) ConfirmZdexList(id, poolId) — mark ListVenue=zdex // // Pearl hub: gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/zdex/v3 // CreatePool(cur, tokenKey, symbol, amountU, amountT, feeBps) string func tryListOnZdex(cur realm, l *Launch, raisedUgnot, liqTokens int64) bool { _ = cur _ = raisedUgnot _ = liqTokens if testForceZdexList && l != nil { l.GnoswapListed = true // compat "listed" flag used by UI l.GnoswapPoolPath = "ugnot|" + l.Symbol l.GnoswapPositionID = 0 l.LiqWugnotUsed = raisedUgnot l.GnoswapNote = "listed (test force zdex)" l.ListVenue = VenueZdex return true } if l != nil { if l.ListSeedReleased { l.GnoswapNote = "zdex: seed released — EOA CreatePool then ConfirmZdexList(id, ugnot|SYMBOL); PreferredVenue=" + l.PreferredVenue } else { l.GnoswapNote = "zdex: ReleaseListSeed → EOA CreatePool (WUGNOT Withdraw + Approve RealmAddr) → ConfirmZdexList; PreferredVenue=" + l.PreferredVenue } } return false }
  16. #16/gno.MemPackageType
  17. #17 MPUserAll

Result log

msg:0,success:true,log:,events:[]

← Back to block 80,582