Transaction
C970A42789E218…563108FC6879
Block 57,568 · index 0 · indexed
Summary
- Hash
- C970A42789E2181B9AABD325279E246C0AAA446F149885685D1B563108FC6879
- Block
- 57,568
- Size
- 59834 bytes
- Gas used
- 77,993,544 / 90,000,000
- Fee
- 200000 ugnot
- Status
- success
Messages
- Attached funds
- 12000000 ugnot
Arguments · 24
- #1zdex
- #2admin.gno
- #3package zdex import "chain" func SetPaused(cur realm, v bool) { mustOwner(cur) paused = v chain.Emit("Pause", "paused", itoa(btoi(v))) } func ProposeAdmin(cur realm, next address) { mustOwner(cur) require(next.IsValid(), "zdex: invalid admin") pendingAdmin = next chain.Emit("ProposeAdmin", "to", next.String()) } func AcceptAdmin(cur realm) { require(pendingAdmin.IsValid(), "zdex: no pending admin") require(cur.Previous().Address() == pendingAdmin, "zdex: not pending admin") admin = pendingAdmin var z address pendingAdmin = z chain.Emit("AcceptAdmin", "to", admin.String()) } func TransferAdmin(cur realm, next address) { ProposeAdmin(cur, next) } func Admin() address { return admin } func RealmAddr() address { return thisAddr() } func PendingAdmin() address { return pendingAdmin } func Paused() bool { return paused } // SetFarm allowlists a vault realm so it can prepaid-ugnot LP/swap. // Only that farm's own code should NotifyFarmPay after RealmSend. func SetFarm(cur realm, addr address, on bool) { mustOwner(cur) require(addr.IsValid(), "zdex: farm addr") k := addr.String() if on { farms.Set(k, true) } else { farms.Remove(k) farmPay.Remove(k) } chain.Emit("SetFarm", "farm", k, "on", itoa(btoi(on))) } func IsFarm(addr address) bool { return farms.Has(addr.String()) } // NotifyFarmPay records ugnot the farm already RealmSent to this DEX // in the same call stack. Unknown realms cannot mint LP from the float. func NotifyFarmPay(cur realm, amount int64) { require(amount > 0, "zdex: amount") prev := cur.Previous() require(!prev.IsUserCall(), "zdex: farm only") k := prev.Address().String() require(farms.Has(k), "zdex: unknown farm") curAmt := int64(0) if v := farmPay.Get(k); v != nil { n, ok := v.(int64) if ok { curAmt = n } } farmPay.Set(k, add64(curAmt, amount)) } func CollectFees(cur realm, poolID string) (ugnotOut, tokenOut int64) { rejectStrayUgnot() p := getPool(poolID) caller := cur.Previous().Address() got := false if caller == p.Creator { ugnotOut = add64(ugnotOut, p.AccruedCreatorU) tokenOut = add64(tokenOut, p.AccruedCreatorT) p.AccruedCreatorU = 0 p.AccruedCreatorT = 0 got = true } if caller == admin { ugnotOut = add64(ugnotOut, p.AccruedProtocolU) tokenOut = add64(tokenOut, p.AccruedProtocolT) p.AccruedProtocolU = 0 p.AccruedProtocolT = 0 got = true } require(got, "zdex: not fee recipient") require(ugnotOut > 0 || tokenOut > 0, "zdex: nothing accrued") if ugnotOut > 0 { if p.QuoteSym != "" { pushToken(0, cur, p.QuoteTok, caller, ugnotOut) } else { sendUgnot(0, cur, caller, ugnotOut) } } if tokenOut > 0 { pushToken(0, cur, poolTokRef(p), caller, tokenOut) } chain.Emit("CollectFees", "pool", poolID, "to", caller.String(), "ugnot", itoa(ugnotOut), "token", itoa(tokenOut)) return ugnotOut, tokenOut } func btoi(v bool) int64 { if v { return 1 } return 0 }
- #4book.gno
- #5package zdex func PlaceBid(cur realm, poolID string, giveU, wantAmt, expireH int64, allOrNone bool) uint64 { panic("zdex: orders disabled") } func PlaceAsk(cur realm, poolID string, giveT, wantU, expireH int64, allOrNone bool) uint64 { panic("zdex: orders disabled") } func FillOrder(cur realm, orderID uint64, giveAmt int64) (paid, received int64) { panic("zdex: orders disabled") } func CancelOrder(cur realm, orderID uint64) { panic("zdex: orders disabled") } func OrderCount() int { return orders.Size() }
- #6gnomod.toml
- #7module = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/zdex/v1" gno = "0.9"
- #8launch.gno
- #9package zdex import "chain" // Launch is disabled. Existing launched pools still trade; ClaimVest still works. func Launch(cur realm, name, symbol string, decimals, totalSupply, creatorKeepBps, virtualUgnot, vestBlocks, lockBlocks, snipeBlocks, snipeMaxBps int64) string { panic("zdex: launch disabled") } // launchLegacy keeps the bonding-curve path for tests and already-minted pools. func launchLegacy(cur realm, name, symbol string, decimals, totalSupply, creatorKeepBps, virtualUgnot, vestBlocks, lockBlocks, snipeBlocks, snipeMaxBps int64) string { assertNotPaused() require(cur.Previous().IsUserCall(), "zdex: EOA only") require(totalSupply > minLiquidity, "zdex: supply too small") require(virtualUgnot > 0, "zdex: virtualUgnot") require(creatorKeepBps >= 0 && creatorKeepBps <= maxCreatorKeepBps, "zdex: creator keep") require(decimals >= 0 && decimals <= 18, "zdex: decimals") require(tryPool(poolIDFor(symbol)) == nil, "zdex: pool exists") if creatorKeepBps > 0 && vestBlocks < minVestBlocks { vestBlocks = minVestBlocks } if lockBlocks < minLockBlocks { lockBlocks = minLockBlocks } if snipeBlocks <= 0 { snipeBlocks = defaultSnipeBlocks } if snipeMaxBps <= 0 { snipeMaxBps = defaultSnipeMaxBps } require(snipeMaxBps <= maxSnipeMaxBps, "zdex: snipe max") creator := cur.Previous().Address() keep := MulDiv(totalSupply, creatorKeepBps, bpsDen) poolAmt := totalSupply - keep require(poolAmt > minLiquidity, "zdex: pool seed too small") key := mintInternal(cur, name, symbol, decimals, totalSupply, cur.Address()) h := height() if keep > 0 { vests.Set(vestKey(symbol, creator), &Vesting{ Symbol: symbol, Owner: creator, Total: keep, StartH: h, EndH: h + vestBlocks, }) } id := poolIDFor(symbol) // Burned (unowned) LP so TotalLP > 0: after unlock, others can add. // Nobody holds a Position for this seed, so it cannot be withdrawn. lp := geoMean(virtualUgnot, poolAmt) require(lp > minLiquidity, "zdex: tiny launch LP") p := &Pool{ ID: id, Token: key, Symbol: symbol, Name: name, ReserveU: 0, ReserveT: poolAmt, VirtualU: virtualUgnot, TotalLP: lp, FeeBps: launchFeeBps, Creator: creator, CreatorBps: launchCreatorFeeBps, ProtocolBps: launchProtocolFeeBps, Decimals: decimals, UnlockHeight: h + lockBlocks, SnipeUntil: h + snipeBlocks, SnipeMaxBps: snipeMaxBps, Launched: true, Internal: true, } pools.Set(id, p) chain.Emit("Launch", "pool", id, "symbol", symbol, "token", key, "supply", itoa(totalSupply), "keep", itoa(keep), "virtual", itoa(virtualUgnot), "lp", itoa(lp), "creator", creator.String(), ) return id } // ClaimVest releases linearly vested creator tokens. func ClaimVest(cur realm, symbol string) int64 { assertNotPaused() owner := cur.Previous().Address() v := vests.Get(vestKey(symbol, owner)) require(v != nil, "zdex: no vesting") vs := v.(*Vesting) rel := vestedAmount(vs, height()) delta := rel - vs.Claimed require(delta > 0, "zdex: nothing to claim") vs.Claimed = rel pushToken(0, cur, symbol, owner, delta) chain.Emit("ClaimVest", "symbol", symbol, "to", owner.String(), "amount", itoa(delta)) return delta } func vestedAmount(vs *Vesting, h int64) int64 { if h <= vs.StartH { return 0 } if vs.EndH <= vs.StartH || h >= vs.EndH { return vs.Total } return MulDiv(vs.Total, h-vs.StartH, vs.EndH-vs.StartH) } func VestingOf(symbol string, owner address) (total, claimed, releasable int64) { v := vests.Get(vestKey(symbol, owner)) if v == nil { return 0, 0, 0 } vs := v.(*Vesting) rel := vestedAmount(vs, height()) return vs.Total, vs.Claimed, rel - vs.Claimed }
- #10listing.gno
- #11package zdex import "chain" const ( listPending = int64(0) listApproved = int64(1) listRejected = int64(2) maxLogoURI = 128 ) type Listing struct { TokenKey string Symbol string Name string Decimals int64 LogoURI string Requester address Deposit int64 Status int64 } func isStableSym(s string) bool { return s == "USDC" || s == "USDT" || s == "DAI" } func tryListing(symbol string) *Listing { v := listings.Get(symbol) if v == nil { return nil } return v.(*Listing) } func requireListed(symbol string) { l := tryListing(symbol) require(l != nil && l.Status == listApproved, "zdex: not listed") } func upsertApprovedListing(symbol, name, tokenKey string, decimals int64, who address) { listings.Set(symbol, &Listing{ TokenKey: tokenKey, Symbol: symbol, Name: name, Decimals: decimals, Requester: who, Status: listApproved, }) } func cleanLogo(s string) string { out := "" n := 0 for i := 0; i < len(s) && n < maxLogoURI; i++ { c := s[i] if c == ';' || c == '\n' || c == '\r' { continue } out += s[i : i+1] n++ } return out } // RequestListing posts a token for admin review. Caller sends exactly 1 GNOT. // Approved tokens may CreatePool and appear in the UI catalog. Reject refunds. func RequestListing(cur realm, tokenKey, symbol, name string, decimals int64, logoURI string) string { assertNotPaused() require(cur.Previous().IsUserCall(), "zdex: EOA only") require(symbol != "" && symbol != "GNOT" && symbol != nativeDenom, "zdex: symbol") require(decimals >= 0 && decimals <= 18, "zdex: decimals") prev := tryListing(symbol) require(prev == nil || prev.Status == listRejected, "zdex: listing exists") takeUgnot(cur, minListUgnot) caller := cur.Previous().Address() if name == "" { name = symbol } listings.Set(symbol, &Listing{ TokenKey: tokenKey, Symbol: symbol, Name: name, Decimals: decimals, LogoURI: cleanLogo(logoURI), Requester: caller, Deposit: minListUgnot, Status: listPending, }) chain.Emit("RequestListing", "symbol", symbol, "from", caller.String()) return symbol } func ApproveListing(cur realm, symbol string) { mustOwner(cur) rejectStrayUgnot() l := tryListing(symbol) require(l != nil && l.Status == listPending, "zdex: not pending") dep := l.Deposit l.Status = listApproved l.Deposit = 0 if dep > 0 { sendUgnot(0, cur, admin, dep) } chain.Emit("ApproveListing", "symbol", symbol) } func RejectListing(cur realm, symbol string) { mustOwner(cur) rejectStrayUgnot() l := tryListing(symbol) require(l != nil && l.Status == listPending, "zdex: not pending") dep := l.Deposit to := l.Requester l.Status = listRejected l.Deposit = 0 if dep > 0 { sendUgnot(0, cur, to, dep) } chain.Emit("RejectListing", "symbol", symbol) } func ListingOf(symbol string) (status, decimals, deposit int64) { l := tryListing(symbol) if l == nil { return 0, 0, 0 } return l.Status, l.Decimals, l.Deposit } func ListingSnapshot(symbol string) string { l := tryListing(symbol) if l == nil { return "" } return listingLine(l) } func ListingList() string { out := "" listings.Iterate("", "", func(_ string, v any) bool { if out != "" { out += "\n" } out += listingLine(v.(*Listing)) return false }) return out } func listingLine(l *Listing) string { st := l.Status dec := l.Decimals dep := l.Deposit line := l.Symbol + ";" + l.Name + ";" + l.TokenKey + ";" line += itoa(dec) + ";" + itoa(st) + ";" + itoa(dep) + ";" + l.Requester.String() return line + ";" + l.LogoURI }
- #12math.gno
- #13package zdex import ( "math/overflow" "gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/zdex/amm/v1" ) // AMM formula lives in gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/zdex/amm/v1 so a later /v2 library can // ship without touching LP, book, or points in this realm. These wrappers // keep the v1 MsgCall/qeval names stable. func MulDiv(a, b, d int64) int64 { return amm.MulDiv(a, b, d) } func Sqrt(n int64) int64 { return amm.Sqrt(n) } func MulDivCeil(a, b, d int64) int64 { return amm.MulDivCeil(a, b, d) } func AmountOut(amountIn, reserveIn, virtualIn, reserveOut, virtualOut, feeBps int64) int64 { return amm.AmountOut(amountIn, reserveIn, virtualIn, reserveOut, virtualOut, feeBps) } func AmountIn(amountOut, reserveIn, virtualIn, reserveOut, virtualOut, feeBps int64) int64 { return amm.AmountIn(amountOut, reserveIn, virtualIn, reserveOut, virtualOut, feeBps) } func geoMean(a, b int64) int64 { prod, ok := overflow.Mul64(a, b) if ok { return Sqrt(prod) } return MulDiv(Sqrt(a), Sqrt(b), 1) } func min64(a, b int64) int64 { if a < b { return a } return b } func add64(a, b int64) int64 { s, ok := overflow.Add64(a, b) if !ok { panic("zdex: add overflow") } return s } func sub64(a, b int64) int64 { if b > a { panic("zdex: sub underflow") } return a - b } type feeSplit struct { gross int64 net int64 lp int64 creator int64 protocol int64 } func splitFee(amountIn, feeBps, creatorBps, protocolBps int64) feeSplit { if feeBps < 0 || feeBps >= 10000 { panic("zdex: feeBps out of range") } if creatorBps < 0 || protocolBps < 0 || creatorBps+protocolBps > 10000 { panic("zdex: fee share out of range") } fee := MulDiv(amountIn, feeBps, 10000) net := amountIn - fee creator := MulDiv(fee, creatorBps, 10000) protocol := MulDiv(fee, protocolBps, 10000) lp := fee - creator - protocol if lp < 0 { panic("zdex: fee split underflow") } return feeSplit{gross: amountIn, net: net, lp: lp, creator: creator, protocol: protocol} }
- #14points.gno
- #15package zdex import "chain" func maybeRollEpoch() { h := height() if epochBlocks <= 0 { epochBlocks = defaultEpochBlocks } if epoch == 0 { epoch = 1 epochEnd = h + epochBlocks return } if h < epochEnd { return } carry := closedPot closedID = epoch closedPts = epochPts closedPot = epochPot epoch++ epochPts = 0 epochPot = carry epochEnd = h + epochBlocks chain.Emit("Epoch", "id", itoa(int64(epoch)), "closedPot", itoa(closedPot), "closedPts", itoa(closedPts)) } func getScore(owner address) *Score { v := scores.Get(owner.String()) if v == nil { s := &Score{} scores.Set(owner.String(), s) return s } return v.(*Score) } func syncScore(s *Score) { maybeRollEpoch() if s.EpochID == epoch { return } if s.EpochID == closedID { s.Closed = s.Epoch } else { s.Closed = 0 } s.Epoch = 0 s.EpochID = epoch } func addPoints(owner address, pts int64) { if pts <= 0 { return } maybeRollEpoch() s := getScore(owner) syncScore(s) s.Epoch = add64(s.Epoch, pts) s.Life = add64(s.Life, pts) epochPts = add64(epochPts, pts) } func harvestLP(p *Pool, owner address) { st := getPos(p.ID, owner) if st == nil || st.LP <= 0 { return } acc := MulDiv(st.LP, p.AccPoints, ptsScale) if acc > st.RewardDebt { addPoints(owner, acc-st.RewardDebt) } st.RewardDebt = acc } func creditSwapPoints(p *Pool, trader address, volU int64, fs feeSplit, ugnotSide bool) { _ = trader _ = volU if ugnotSide { p.AccruedCreatorU = add64(p.AccruedCreatorU, fs.creator) p.AccruedProtocolU = add64(p.AccruedProtocolU, fs.protocol) } else { p.AccruedCreatorT = add64(p.AccruedCreatorT, fs.creator) p.AccruedProtocolT = add64(p.AccruedProtocolT, fs.protocol) } } // HarvestPoints settles LP points for a pool without depositing. No extra stake. func HarvestPoints(cur realm, poolID string) int64 { panic("zdex: epoch disabled") } func HarvestAll(cur realm) int64 { panic("zdex: epoch disabled") } func ClaimFeeShare(cur realm) int64 { panic("zdex: epoch disabled") } func PointsOf(owner address) (life, epochPtsUser, closedPtsUser int64) { v := scores.Get(owner.String()) if v == nil { return 0, 0, 0 } s := v.(*Score) if s.EpochID == epoch { return s.Life, s.Epoch, s.Closed } if s.EpochID == closedID { return s.Life, 0, s.Epoch } return s.Life, 0, 0 } func ClaimableFeeShare(owner address) int64 { if closedID == 0 || closedPot <= 0 || closedPts <= 0 { return 0 } v := scores.Get(owner.String()) if v == nil { return 0 } s := v.(*Score) if s.ClaimedID == closedID { return 0 } closed := s.Closed if s.EpochID == closedID { closed = s.Epoch } else if s.EpochID != epoch { return 0 } return MulDiv(closed, closedPot, closedPts) } func EpochInfo() (id, endH, pot, pts, cID, cPot, cPts int64) { return int64(epoch), epochEnd, epochPot, epochPts, int64(closedID), closedPot, closedPts } func SetEpochBlocks(cur realm, n int64) { mustOwner(cur) require(n >= 10, "zdex: epoch too short") epochBlocks = n h := height() if epoch > 0 && epochEnd > h+n { epochEnd = h + n } } func SetFeeShareBps(cur realm, bps int64) { mustOwner(cur) require(bps >= 0 && bps <= bpsDen, "zdex: fee share") feeShareBps = bps }
- #16pool.gno
- #17package zdex import ( "chain" "gno.land/r/nt/grc20reg/v0" ) // CreatePool seeds a two-sided ugnot/GRC20 pool. The GRC20 must already // exist (external or internal). Caller sends `amountU` ugnot with the tx // and must have approved this realm for `amountT` of the token. func CreatePool(cur realm, tokenKey, symbol string, amountU, amountT, feeBps int64) string { assertNotPaused() require(cur.Previous().IsUserCall(), "zdex: EOA only") require(amountU > 0 && amountT > 0, "zdex: amounts") require(amountU >= minListUgnot, "zdex: min 1 GNOT") if feeBps == 0 { if isStableSym(symbol) { feeBps = feeTierStable } else { feeBps = defaultFeeBps } } require(validSwapFee(feeBps), "zdex: fee tier 1/5/30/100") require(symbol != "", "zdex: symbol") id := poolIDFor(symbol) require(tryPool(id) == nil, "zdex: pool exists") caller := cur.Previous().Address() if caller != admin { requireListed(symbol) } takeUgnot(cur, amountU) ref := tokenPullKey(tokenKey, symbol) pullUserToken(0, cur, ref, caller, amountT) lp := geoMean(amountU, amountT) require(lp > minLiquidity, "zdex: tiny LP") lpUser := lp - minLiquidity name := symbol decimals := int64(0) internal := tryTok(symbol) != nil if internal { rec := getTok(symbol) name = rec.token.GetName() decimals = int64(rec.token.GetDecimals()) } else { t := grc20reg.Get(ref) require(t != nil, "zdex: unknown token") require(t.GetSymbol() == symbol, "zdex: symbol mismatch") name = t.GetName() decimals = int64(t.GetDecimals()) } p := &Pool{ ID: id, Token: ref, Symbol: symbol, Name: name, ReserveU: amountU, ReserveT: amountT, TotalLP: lp, FeeBps: feeBps, Creator: caller, ProtocolBps: poolProtocolFeeBps, Decimals: decimals, Internal: internal, } pools.Set(id, p) setPos(id, caller, lpUser) if caller == admin { upsertApprovedListing(symbol, name, ref, decimals, caller) } chain.Emit("CreatePool", "pool", id, "lp", itoa(lpUser), "creator", caller.String()) return id } // CreateTokenPool lists two GRC20s (no ugnot). amountQ is quote token, amountB is base. func CreateTokenPool(cur realm, quoteSym, baseSym string, amountQ, amountB, feeBps int64) string { assertNotPaused() require(cur.Previous().IsUserCall(), "zdex: EOA only") rejectStrayUgnot() require(amountQ > 0 && amountB > 0, "zdex: amounts") if feeBps == 0 { if isStableSym(quoteSym) && isStableSym(baseSym) { feeBps = feeTierStable } else { feeBps = feeTierLow } } require(validSwapFee(feeBps), "zdex: fee tier 1/5/30/100") require(quoteSym != "" && baseSym != "" && quoteSym != baseSym, "zdex: pair") id := pairID(quoteSym, baseSym) require(tryPool(id) == nil, "zdex: pool exists") caller := cur.Previous().Address() qrec := tryTok(quoteSym) brec := tryTok(baseSym) require(qrec != nil && brec != nil, "zdex: unknown token") pullUserToken(0, cur, quoteSym, caller, amountQ) pullUserToken(0, cur, baseSym, caller, amountB) lp := geoMean(amountQ, amountB) require(lp > minLiquidity, "zdex: tiny LP") lpUser := lp - minLiquidity p := &Pool{ ID: id, Token: baseSym, QuoteSym: quoteSym, QuoteTok: quoteSym, Symbol: baseSym, Name: brec.token.GetName(), ReserveU: amountQ, ReserveT: amountB, TotalLP: lp, FeeBps: feeBps, Creator: caller, ProtocolBps: poolProtocolFeeBps, Decimals: int64(brec.token.GetDecimals()), Internal: true, } pools.Set(id, p) setPos(id, caller, lpUser) chain.Emit("CreatePool", "pool", id, "lp", itoa(lpUser), "creator", caller.String()) return id } // AddLiquidity deposits ugnot (OriginSend) + token in pool ratio. func AddLiquidity(cur realm, poolID string, amountU, maxToken, minLP int64) int64 { assertNotPaused() p := getPool(poolID) require(!p.Launched || height() >= p.UnlockHeight, "zdex: launch LP locked") require(p.ReserveT > 0 && p.TotalLP > 0, "zdex: empty pool") require(amountU > 0, "zdex: amountU") baseU := p.ReserveU if baseU == 0 { baseU = p.VirtualU } require(baseU > 0, "zdex: no ugnot base") if p.QuoteSym != "" { rejectStrayUgnot() pullUserToken(0, cur, p.QuoteTok, cur.Previous().Address(), amountU) } else { takeUgnot(cur, amountU) } tokenIn := MulDiv(amountU, p.ReserveT, baseU) require(tokenIn > 0, "zdex: tokenIn") require(tokenIn <= maxToken, "zdex: token slippage") caller := cur.Previous().Address() pullUserToken(0, cur, poolTokRef(p), caller, tokenIn) lp := MulDiv(amountU, p.TotalLP, baseU) require(lp > 0, "zdex: zero LP") require(lp >= minLP, "zdex: min LP") harvestLP(p, caller) p.ReserveU = add64(p.ReserveU, amountU) p.ReserveT = add64(p.ReserveT, tokenIn) p.TotalLP = add64(p.TotalLP, lp) curLP := int64(0) if existing := getPos(poolID, caller); existing != nil { curLP = existing.LP } setPos(poolID, caller, add64(curLP, lp)) if st := getPos(poolID, caller); st != nil { st.RewardDebt = MulDiv(st.LP, p.AccPoints, ptsScale) } chain.Emit("AddLiquidity", "pool", poolID, "lp", itoa(lp), "provider", caller.String()) return lp } // RemoveLiquidity burns LP and returns ugnot + token. Blocked on launched // pools until UnlockHeight (and even then the minimum-liquidity lock remains). func RemoveLiquidity(cur realm, poolID string, lp, minU, minT int64) (int64, int64) { assertNotPaused() rejectStrayUgnot() p := getPool(poolID) require(height() >= p.UnlockHeight, "zdex: LP locked") require(lp > 0, "zdex: lp") require(p.TotalLP > minLiquidity, "zdex: no LP") caller := cur.Previous().Address() st := getPos(poolID, caller) require(st != nil && st.LP >= lp, "zdex: insufficient LP") harvestLP(p, caller) outU := MulDiv(lp, p.ReserveU, p.TotalLP) outT := MulDiv(lp, p.ReserveT, p.TotalLP) require(outU >= minU && outT >= minT, "zdex: slippage") require(outU < p.ReserveU && outT < p.ReserveT, "zdex: would drain") p.ReserveU = sub64(p.ReserveU, outU) p.ReserveT = sub64(p.ReserveT, outT) p.TotalLP = sub64(p.TotalLP, lp) setPos(poolID, caller, st.LP-lp) if left := getPos(poolID, caller); left != nil { left.RewardDebt = MulDiv(left.LP, p.AccPoints, ptsScale) } if p.QuoteSym != "" { pushToken(0, cur, p.QuoteTok, caller, outU) } else { sendUgnot(0, cur, caller, outU) } pushToken(0, cur, poolTokRef(p), caller, outT) chain.Emit("RemoveLiquidity", "pool", poolID, "lp", itoa(lp), "provider", caller.String()) return outU, outT } func PoolInfo(poolID string) (reserveU, reserveT, virtualU, totalLP, feeBps int64) { p := getPool(poolID) return p.ReserveU, p.ReserveT, p.VirtualU, p.TotalLP, p.FeeBps } func PositionOf(poolID string, owner address) int64 { st := getPos(poolID, owner) if st == nil { return 0 } return st.LP } func PoolCount() int { return pools.Size() }
- #18query.gno
- #19package zdex import ( "gno.land/p/nt/ufmt/v0" "gno.land/r/nt/grc20reg/v0" ) // Height is the current chain height (for UI deadlines). func Height() int64 { return height() } // PoolList returns one pool id per line. func PoolList() string { out := "" pools.Iterate("", "", func(k string, _ any) bool { if out != "" { out += "\n" } out += k return false }) return out } // PoolSnapshot is a single-line UI record: // id;symbol;name;reserveU;reserveT;virtualU;totalLP;feeBps;launched;unlockH;snipeUntil;snipeMaxBps;creator;decimals;accruedPU;accruedPT;accruedCU;accruedCT func PoolSnapshot(poolID string) string { p := getPool(poolID) return ufmt.Sprintf("%s;%s;%s;%d;%d;%d;%d;%d;%d;%d;%d;%d;%s;%d;%d;%d;%d;%d", p.ID, p.Symbol, p.Name, p.ReserveU, p.ReserveT, p.VirtualU, p.TotalLP, p.FeeBps, btoi(p.Launched), p.UnlockHeight, p.SnipeUntil, p.SnipeMaxBps, p.Creator.String(), p.Decimals, p.AccruedProtocolU, p.AccruedProtocolT, p.AccruedCreatorU, p.AccruedCreatorT) + ";" + p.QuoteSym } // OrderList returns one order per line: // id;pool;side;giveAmt;wantAmt;expireH;allOrNone;maker func OrderList() string { out := "" orders.Iterate("", "", func(_ string, v any) bool { o := v.(*Order) side := "ask" if o.GiveUgnot { side = "bid" } line := ufmt.Sprintf("%d;%s;%s;%d;%d;%d;%d;%s", int64(o.ID), o.PoolID, side, o.GiveAmt, o.WantAmt, o.ExpireH, btoi(o.AllOrNone), o.Maker.String()) if out != "" { out += "\n" } out += line return false }) return out } func PointsSnapshot(owner address) string { life, ep, cl := PointsOf(owner) return ufmt.Sprintf("%d;%d;%d;%d", life, ep, cl, ClaimableFeeShare(owner)) } func EpochSnapshot() string { id, endH, pot, pts, cID, cPot, cPts := EpochInfo() return ufmt.Sprintf("%d;%d;%d;%d;%d;%d;%d", id, endH, pot, pts, cID, cPot, cPts) } func tokenLine(symbol, name, key string, decimals int64, pooled, internal bool) string { if name == "" { name = symbol } return ufmt.Sprintf("%s;%s;%s;%d;%d;%d", symbol, name, key, decimals, btoi(pooled), btoi(internal)) } // TokenCatalog lists known tokens (internal + pooled), one per line: // symbol;name;key;decimals;pooled;internal func TokenCatalog() string { out := "" seen := map[string]bool{} add := func(line, symbol string) { if seen[symbol] { return } seen[symbol] = true if out != "" { out += "\n" } out += line } tokens.Iterate("", "", func(k string, v any) bool { rec := v.(*tokenRec) pooled := tryPool(poolIDFor(k)) != nil add(tokenLine(rec.token.GetSymbol(), rec.token.GetName(), rec.token.ID(), int64(rec.token.GetDecimals()), pooled, true), k) return false }) pools.Iterate("", "", func(_ string, v any) bool { p := v.(*Pool) internal := tryTok(p.Symbol) != nil add(tokenLine(p.Symbol, p.Name, p.Token, p.Decimals, true, internal), p.Symbol) return false }) return out } // LookupToken resolves an internal symbol, pool id, or GRC20 registry key. // Same record as TokenCatalog, or empty if unknown. func LookupToken(ref string) string { if ref == "" { return "" } if p := tryPool(ref); p != nil { return tokenLine(p.Symbol, p.Name, p.Token, p.Decimals, true, tryTok(p.Symbol) != nil) } if rec := tryTok(ref); rec != nil { return tokenLine(rec.token.GetSymbol(), rec.token.GetName(), rec.token.ID(), int64(rec.token.GetDecimals()), tryPool(poolIDFor(ref)) != nil, true) } if p := tryPool(poolIDFor(ref)); p != nil { return tokenLine(p.Symbol, p.Name, p.Token, p.Decimals, true, tryTok(p.Symbol) != nil) } t := grc20reg.Get(ref) if t == nil { return "" } sym := t.GetSymbol() return tokenLine(sym, t.GetName(), ref, int64(t.GetDecimals()), tryPool(poolIDFor(sym)) != nil, false) }
- #20render.gno
- #21package zdex import ( "gno.land/p/nt/markdown/sanitize/v0" "gno.land/p/nt/ufmt/v0" ) func Render(path string) string { switch path { case "", "home": return renderHome() case "book": return renderBook() default: if len(path) > 5 && path[:5] == "pool/" { return renderPool(path[5:]) } if len(path) > 6 && path[:6] == "token/" { return renderToken(path[6:]) } return "> [!WARNING]\n> Path not found.\n" } } func renderHome() string { out := "# zdex\n\n" out += "GNOT-native AMM. Two-sided pools, native `ugnot` (no wrap).\n" out += "Swap fee 0.30% default (tiers 0.01% / 0.05% / 0.30% / 1.00%). Protocol ~1/6 of the fee.\n\n" if paused { out += "> [!CAUTION]\n> Protocol is paused.\n\n" } out += ufmt.Sprintf("* Version: **%s**\n* Package: `%s`\n", Version(), thisPkg()) if nextPkg != "" { out += ufmt.Sprintf("* Next: `%s`\n", sanitize.InlineText(nextPkg)) } out += ufmt.Sprintf("* Pools: **%d**\n* Admin: `%s`\n\n", pools.Size(), admin.String()) out += "## Pools\n\n" if pools.Size() == 0 { out += "_None yet. Create a two-sided pool._\n\n" } else { out += "| Pair | ugnot | token | virtual | fee | launch |\n|---|---:|---:|---:|---:|---|\n" pools.Iterate("", "", func(_ string, v any) bool { p := v.(*Pool) out += ufmt.Sprintf("| [%s](pool/%s) | %d | %d | %d | %d bps | %s |\n", sanitize.InlineText(p.Symbol), p.ID, p.ReserveU, p.ReserveT, p.VirtualU, p.FeeBps, yesno(p.Launched)) return false }) out += "\n" } out += "## Swap\n\n" out += "<gno-form exec=\"SwapExactIn\">\n" out += "<gno-input name=\"poolID\" placeholder=\"ugnot|SYMBOL\" required=\"true\" />\n" out += "<gno-input name=\"tokenIn\" placeholder=\"ugnot or SYMBOL\" required=\"true\" />\n" out += "<gno-input name=\"amountIn\" type=\"number\" placeholder=\"amount in\" required=\"true\" />\n" out += "<gno-input name=\"minOut\" type=\"number\" placeholder=\"min out\" required=\"true\" />\n" out += "<gno-input name=\"maxHeight\" type=\"number\" placeholder=\"deadline height (0=none)\" />\n" out += "</gno-form>\n\n" out += "## Route (token → GNOT → token)\n\n" out += "Best path is the GNOT hub. One transaction. Two pool fees.\n\n" out += "<gno-form exec=\"SwapRouteExactIn\">\n" out += "<gno-input name=\"tokenIn\" placeholder=\"SYMBOL in\" required=\"true\" />\n" out += "<gno-input name=\"tokenOut\" placeholder=\"SYMBOL out\" required=\"true\" />\n" out += "<gno-input name=\"amountIn\" type=\"number\" placeholder=\"amount in\" required=\"true\" />\n" out += "<gno-input name=\"minOut\" type=\"number\" placeholder=\"min out\" required=\"true\" />\n" out += "<gno-input name=\"maxHeight\" type=\"number\" placeholder=\"deadline height (0=none)\" />\n" out += "</gno-form>\n\n" out += "## Create a pool\n\n" out += "Two-sided ugnot/GRC20. Min **1 GNOT**. Fee tiers 1 / 5 / 30 / 100 bps. Protocol takes ~1/6 of the swap fee; the rest stays as LP.\n\n" out += "<gno-form exec=\"CreatePool\">\n" out += "<gno-input name=\"tokenKey\" placeholder=\"GRC20 registry key\" required=\"true\" />\n" out += "<gno-input name=\"symbol\" placeholder=\"SYMBOL\" required=\"true\" />\n" out += "<gno-input name=\"amountU\" type=\"number\" placeholder=\"ugnot (>= 1000000)\" required=\"true\" />\n" out += "<gno-input name=\"amountT\" type=\"number\" placeholder=\"token amount\" required=\"true\" />\n" out += "<gno-input name=\"feeBps\" type=\"number\" placeholder=\"30 = 0.30%\" required=\"true\" />\n" out += "</gno-form>\n\n" return out } func renderPool(id string) string { id = sanitize.InlineText(id) p := tryPool(id) if p == nil { return "> [!WARNING]\n> Pool not found.\n" } out := ufmt.Sprintf("# Pool %s\n\n", sanitize.InlineText(p.Symbol)) out += ufmt.Sprintf("* Token: `%s`\n* Real ugnot: **%d**\n* Real token: **%d**\n* Virtual ugnot: **%d**\n", p.Token, p.ReserveU, p.ReserveT, p.VirtualU) out += ufmt.Sprintf("* Fee: %d bps (creator share %d, protocol %d)\n* LP: %d\n* Unlock height: %d (now %d)\n", p.FeeBps, p.CreatorBps, p.ProtocolBps, p.TotalLP, p.UnlockHeight, height()) if p.SnipeUntil > height() { out += ufmt.Sprintf("* Snipe cap: %d bps until height %d\n", p.SnipeMaxBps, p.SnipeUntil) } out += "\n## Swap on this pool\n\n" out += "<gno-form exec=\"SwapExactIn\">\n" out += ufmt.Sprintf("<gno-input name=\"poolID\" value=\"%s\" required=\"true\" />\n", p.ID) out += "<gno-input name=\"tokenIn\" placeholder=\"ugnot or symbol\" required=\"true\" />\n" out += "<gno-input name=\"amountIn\" type=\"number\" required=\"true\" />\n" out += "<gno-input name=\"minOut\" type=\"number\" required=\"true\" />\n" out += "<gno-input name=\"maxHeight\" type=\"number\" />\n" out += "</gno-form>\n" return out } func renderToken(symbol string) string { symbol = sanitize.InlineText(symbol) rec := tryTok(symbol) out := ufmt.Sprintf("# %s\n\n", symbol) if rec != nil { out += rec.token.RenderHome() + "\n" } p := tryPool(poolIDFor(symbol)) if p != nil { out += ufmt.Sprintf("[Open pool](pool/%s)\n", p.ID) } return out } func renderBook() string { return "# Order book\n\nOrders are disabled.\n" } func yesno(v bool) string { if v { return "yes" } return "no" }
- #22route.gno
- #23package zdex func connectPool(a, b string) *Pool { a = normTok(a) b = normTok(b) if a == "" || b == "" || a == b { return nil } if a == nativeDenom { return tryPool(poolIDFor(b)) } if b == nativeDenom { return tryPool(poolIDFor(a)) } return tryPool(pairID(a, b)) } func otherLeg(p *Pool, tok string) string { if p == nil { return "" } if p.QuoteSym != "" { if isQuoteTok(p, tok) { return p.Symbol } if isBaseTok(p, tok) { return p.QuoteSym } return "" } if tok == nativeDenom || tok == "GNOT" { return p.Symbol } if tok == p.Symbol || tok == p.Token { return nativeDenom } return "" } func hopIsBuy(p *Pool, tokenIn string) bool { if p.QuoteSym != "" { return isQuoteTok(p, tokenIn) } return tokenIn == nativeDenom || tokenIn == "GNOT" } func hopSnipeOK(p *Pool, out int64, buy bool) bool { if height() >= p.SnipeUntil || p.SnipeMaxBps <= 0 { return true } base := p.ReserveU if buy { base = p.ReserveT } return out <= MulDiv(base, p.SnipeMaxBps, bpsDen) } func quoteHop(p *Pool, tokenIn string, amountIn int64) int64 { if p == nil || amountIn <= 0 { return 0 } if p.QuoteSym != "" { if isQuoteTok(p, tokenIn) { return AmountOut(amountIn, p.ReserveU, 0, p.ReserveT, 0, p.FeeBps) } if !isBaseTok(p, tokenIn) { return 0 } return AmountOut(amountIn, p.ReserveT, 0, p.ReserveU, 0, p.FeeBps) } if tokenIn == nativeDenom || tokenIn == "GNOT" { return AmountOut(amountIn, p.ReserveU, p.VirtualU, p.ReserveT, 0, p.FeeBps) } if tokenIn != p.Symbol && tokenIn != p.Token { return 0 } return AmountOut(amountIn, p.ReserveT, 0, p.ReserveU, 0, p.FeeBps) } func hasHub(hubs []string, h string) bool { for i := 0; i < len(hubs); i++ { if hubs[i] == h { return true } } return false } func collectHubs(in, out string) []string { hubs := []string{nativeDenom, "USDC", "USDT", "DAI"} pools.Iterate("", "", func(_ string, v any) bool { p := v.(*Pool) h := otherLeg(p, in) if h != "" && h != in && h != out && !hasHub(hubs, h) { hubs = append(hubs, h) } return false }) return hubs } func quoteVia(in, hub, out string, amountIn int64) (mid, got int64) { p1 := connectPool(in, hub) p2 := connectPool(hub, out) if p1 == nil || p2 == nil || p1.ID == p2.ID { return 0, 0 } mid = quoteHop(p1, in, amountIn) if mid <= 0 || !hopSnipeOK(p1, mid, hopIsBuy(p1, in)) { return 0, 0 } got = quoteHop(p2, hub, mid) if got <= 0 || !hopSnipeOK(p2, got, hopIsBuy(p2, hub)) { return 0, 0 } return mid, got } // bestVia picks the 2-hop hub with the highest AmountOut (lowest combined fee+impact). func bestVia(in, out string, amountIn int64) (hub string, mid, got int64) { hubs := collectHubs(in, out) for i := 0; i < len(hubs); i++ { h := hubs[i] if h == in || h == out { continue } m, g := quoteVia(in, h, out, amountIn) if g > got { hub, mid, got = h, m, g } } return hub, mid, got } func applyHopKeep(p *Pool, caller address, tokenIn string, amountIn int64) int64 { if p.QuoteSym == "" { if tokenIn == nativeDenom || tokenIn == "GNOT" { return swapBuyKeepT(p, caller, amountIn) } return swapSellKeepU(p, caller, amountIn) } out := quoteHop(p, tokenIn, amountIn) require(out > 0, "zdex: zero out") assertSnipe(p, out, hopIsBuy(p, tokenIn)) fs := splitFee(amountIn, p.FeeBps, p.CreatorBps, p.ProtocolBps) if isQuoteTok(p, tokenIn) { p.ReserveU = add64(p.ReserveU, fs.net+fs.lp) creditSwapPoints(p, caller, 0, fs, true) p.ReserveT = sub64(p.ReserveT, out) } else { require(isBaseTok(p, tokenIn), "zdex: tokenIn") p.ReserveT = add64(p.ReserveT, fs.net+fs.lp) creditSwapPoints(p, caller, 0, fs, false) p.ReserveU = sub64(p.ReserveU, out) } emitSwap(p, caller, tokenIn, amountIn, out, fs.gross-fs.net) return out }
- #24state.gno
Result log
msg:0,success:true,log:,events:[]