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

gnoswap_list.gno

9.00 Kb · 296 lines
  1package padv9
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7	"time"
  8
  9	"gno.land/p/gnoswap/consts"
 10	u256 "gno.land/p/gnoswap/uint256"
 11	"gno.land/r/gnoland/wugnot"
 12	"gno.land/r/gnoswap/gns"
 13	gnspool "gno.land/r/gnoswap/pool"
 14	"gno.land/r/gnoswap/position"
 15	"gno.land/r/gnoswap/router"
 16)
 17
 18// Sapphire Gnoswap registry keys / role addresses (sapphire-1).
 19const (
 20	wugnotTokenKey       = "gno.land/r/gnoland/wugnot.wugnot"
 21	gnsTokenKey          = "gno.land/r/gnoswap/gns.GNS"
 22	gnoswapPoolAddrStr   = "g1dexaf6aqkkyr9yfy9d5up69lsn7ra80af34g5v"
 23	gnoswapRouterAddrStr = "g1vc883gshu5z7ytk5cdynhc8c2dh67pdp4cszkp"
 24)
 25
 26// tryListOnGnoswap seeds a Gnoswap CL pool with remaining meme tokens + raised-sized
 27// WUGNOT as LP. CreatePool fee (fixed GNS amount) is paid from:
 28//  1) pre-funded GNS on pad (preferred — immune to GNOT/GNS price moves), else
 29//  2) ExactOut WUGNOT→GNS using SURPLUS inventory only (above raised), so LP depth
 30//     stays equal to raised even when GNS is expensive.
 31//
 32// Returns true on success. Soft failure sets l.GnoswapNote and returns false
 33// (caller keeps internal CPMM).
 34//
 35// Note: wugnot.Deposit is EOA-only — protocol must pre-fund pad with WUGNOT
 36// (and ideally GNS). Raised ugnot stays on pad as wrap-back backlog.
 37func tryListOnGnoswap(cur realm, l *Launch, raisedUgnot, remainingTokens int64) bool {
 38	if raisedUgnot <= 0 || remainingTokens <= 0 {
 39		l.GnoswapNote = "list skip: empty capital"
 40		return false
 41	}
 42	padAddr := cur.Address()
 43	wBal := wugnot.BalanceOf(padAddr)
 44	if wBal < raisedUgnot {
 45		l.GnoswapNote = "list skip: need WUGNOT inventory >= raised for LP; have " +
 46			strconv.FormatInt(wBal, 10) + " need " + strconv.FormatInt(raisedUgnot, 10)
 47		return false
 48	}
 49
 50	feeNeed := gnspool.GetPoolCreationFee()
 51	if feeNeed <= 0 {
 52		feeNeed = 100_000_000 // 100 GNS default (6 decimals)
 53	}
 54
 55	// --- GNS for CreatePool fee (price-sensitive only if we must swap) ---
 56	gnsBal := gns.BalanceOf(padAddr)
 57	feeWugnot := int64(0)
 58	if gnsBal < feeNeed {
 59		// Fee paid from inventory ABOVE raised so LP size stays = raised.
 60		// maxIn = min(GnoswapMaxFeeWugnot, surplus) — NOT raised/2 (that blocked list at ~10 GNOT/GNS).
 61		surplus := wBal - raisedUgnot
 62		if surplus < 1 {
 63			l.GnoswapNote = "list skip: need GNS on pad or WUGNOT surplus above raised for fee swap; " +
 64				"CreatePool fee is fixed GNS — cost in GNOT moves with market (~1k GNOT/100 GNS at ~10:1)"
 65			return false
 66		}
 67		maxFeeW := GnoswapMaxFeeWugnot
 68		if maxFeeW > surplus {
 69			maxFeeW = surplus
 70		}
 71		if maxFeeW < 1 {
 72			l.GnoswapNote = "list skip: fee WUGNOT budget empty"
 73			return false
 74		}
 75		spent, ok := swapWugnotForGNS(cur, feeNeed, maxFeeW)
 76		if !ok {
 77			l.GnoswapNote = "list skip: WUGNOT→GNS ExactOut failed (price too high for budget " +
 78				strconv.FormatInt(maxFeeW, 10) + " ugnot, or thin liquidity). " +
 79				"Pre-fund " + strconv.FormatInt(feeNeed, 10) + " GNS base units or more WUGNOT surplus."
 80			return false
 81		}
 82		feeWugnot = spent
 83		gnsBal = gns.BalanceOf(padAddr)
 84		if gnsBal < feeNeed {
 85			l.GnoswapNote = "list skip: GNS still short after swap"
 86			return false
 87		}
 88		// Protect LP: after fee spend we must still hold >= raised WUGNOT.
 89		if wugnot.BalanceOf(padAddr) < raisedUgnot {
 90			l.GnoswapNote = "list skip: fee swap ate into LP inventory (should not happen with surplus cap)"
 91			return false
 92		}
 93	}
 94
 95	// LP always seeds full raised-sized WUGNOT (fee never reduces LP when surplus/GNS used).
 96	liqWugnot := raisedUgnot
 97	if wugnot.BalanceOf(padAddr) < liqWugnot {
 98		l.GnoswapNote = "list skip: WUGNOT below raised after fee path"
 99		return false
100	}
101
102	// Mint remaining GRC20 to pad for position.Mint TransferFrom.
103	addBal(l, padAddr, remainingTokens)
104
105	tokenKey := adenaKeyFromTokenID(l.TokenID, l.Symbol)
106	if tokenKey == "" {
107		l.GnoswapNote = "list skip: empty token registry key"
108		return false
109	}
110
111	// Order tokens lexicographically (Gnoswap requirement).
112	t0, t1 := wugnotTokenKey, tokenKey
113	amt0, amt1 := liqWugnot, remainingTokens
114	if strings.Compare(t0, t1) > 0 {
115		t0, t1 = t1, t0
116		amt0, amt1 = amt1, amt0
117	}
118
119	sqrtPrice := computeSqrtPriceX96(amt0, amt1)
120	if sqrtPrice == "" || sqrtPrice == "0" {
121		l.GnoswapNote = "list skip: bad sqrtPriceX96"
122		return false
123	}
124
125	// Approve pool for GNS creation fee + both LP legs.
126	poolAddr := address(gnoswapPoolAddrStr)
127	gns.Approve(cross(cur), poolAddr, feeNeed)
128	wugnot.Approve(cross(cur), poolAddr, liqWugnot)
129	if err := l.ledger.Approve(padAddr, poolAddr, remainingTokens); err != nil {
130		l.GnoswapNote = "list skip: token approve failed: " + err.Error()
131		return false
132	}
133
134	// CreatePool (factory reorders tokens + inverts price if needed).
135	gnspool.CreatePool(cross(cur), wugnotTokenKey, tokenKey, GnoswapFeeTier, sqrtPrice)
136
137	tickLower, tickUpper := alignedFullRangeTicks(GnoswapTickSpacing)
138	deadline := time.Now().Unix() + 600
139
140	posID, liqStr, a0, a1 := position.Mint(
141		cross(cur),
142		t0,
143		t1,
144		GnoswapFeeTier,
145		tickLower,
146		tickUpper,
147		strconv.FormatInt(amt0, 10),
148		strconv.FormatInt(amt1, 10),
149		"0",
150		"0",
151		deadline,
152		padAddr, // permanent lock: pad owns position NFT
153		"",
154	)
155
156	poolPath := t0 + ":" + t1 + ":" + strconv.FormatUint(uint64(GnoswapFeeTier), 10)
157	l.GnoswapListed = true
158	l.GnoswapPoolPath = poolPath
159	l.GnoswapPositionID = posID
160	l.FeeWugnotSpent = feeWugnot
161	l.LiqWugnotUsed = liqWugnot
162	l.GnoswapNote = "listed pool=" + poolPath + " pos=" + strconv.FormatUint(posID, 10) +
163		" liq=" + liqStr + " a0=" + a0 + " a1=" + a1 +
164		" feeWugnot=" + strconv.FormatInt(feeWugnot, 10)
165
166	chain.Emit("GnoswapListed",
167		"id", l.ID,
168		"poolPath", poolPath,
169		"positionId", strconv.FormatUint(posID, 10),
170		"feeWugnot", strconv.FormatInt(feeWugnot, 10),
171		"liqWugnot", strconv.FormatInt(liqWugnot, 10),
172		"tokens", strconv.FormatInt(remainingTokens, 10),
173	)
174	return true
175}
176
177// swapWugnotForGNS ExactOut-swaps WUGNOT→GNS. maxInWugnot is a hard ceiling
178// (slippage / price protection): if market needs more GNOT for amountOutGNS, swap reverts.
179func swapWugnotForGNS(cur realm, amountOutGNS, maxInWugnot int64) (spent int64, ok bool) {
180	if amountOutGNS <= 0 || maxInWugnot <= 0 {
181		return 0, false
182	}
183	routerAddr := address(gnoswapRouterAddrStr)
184	before := wugnot.BalanceOf(cur.Address())
185	if before < maxInWugnot {
186		maxInWugnot = before
187	}
188	if maxInWugnot <= 0 {
189		return 0, false
190	}
191	wugnot.Approve(cross(cur), routerAddr, maxInWugnot)
192
193	route := wugnotTokenKey + ":" + gnsTokenKey + ":" + strconv.FormatUint(uint64(GnoswapFeeTier), 10)
194	deadline := time.Now().Unix() + 600
195	// ExactOut panics on insufficient maxIn — recover as soft fail for graduate fallback.
196	// Gno has no recover in production the same way; ExactOut reverts the whole tx if over maxIn.
197	// Callers must set maxIn high enough. We still try; panic would revert graduate entirely.
198	// Prefer pre-funded GNS to avoid this path under volatile prices.
199	inStr, outStr := router.ExactOutSwapRoute(
200		cross(cur),
201		wugnotTokenKey,
202		gnsTokenKey,
203		strconv.FormatInt(amountOutGNS, 10),
204		route,
205		"100",
206		strconv.FormatInt(maxInWugnot, 10),
207		deadline,
208		"",
209	)
210	_ = outStr
211	spentIn, err := strconv.ParseInt(inStr, 10, 64)
212	if err != nil || spentIn <= 0 {
213		after := wugnot.BalanceOf(cur.Address())
214		if after >= before {
215			return 0, false
216		}
217		return before - after, true
218	}
219	return spentIn, true
220}
221
222// computeSqrtPriceX96 returns sqrt(amount1/amount0) * 2^96 as decimal string.
223func computeSqrtPriceX96(amount0, amount1 int64) string {
224	if amount0 <= 0 || amount1 <= 0 {
225		return ""
226	}
227	a0 := u256.MustFromDecimal(strconv.FormatInt(amount0, 10))
228	a1 := u256.MustFromDecimal(strconv.FormatInt(amount1, 10))
229	num := u256.Zero().Mul(a1, consts.Q192())
230	ratio := u256.Zero().Div(num, a0)
231	sqrt := u256Sqrt(ratio)
232	if sqrt.Lt(consts.MinSqrtRatio()) {
233		return consts.MinSqrtRatio().ToString()
234	}
235	if sqrt.Gte(consts.MaxSqrtRatio()) {
236		return u256.Zero().Sub(consts.MaxSqrtRatio(), u256.One()).ToString()
237	}
238	return sqrt.ToString()
239}
240
241func u256Sqrt(x *u256.Uint) *u256.Uint {
242	if x == nil || x.IsZero() {
243		return u256.Zero()
244	}
245	if x.Eq(u256.One()) {
246		return u256.One()
247	}
248	lo := u256.Zero()
249	hi := u256.Zero().Add(x, u256.One())
250	cap128 := u256.Zero().Lsh(u256.One(), 128)
251	if hi.Gt(cap128) {
252		hi = cap128.Clone()
253	}
254	for lo.Lt(u256.Zero().Sub(hi, u256.One())) {
255		mid := u256.Zero().Div(u256.Zero().Add(lo, hi), u256.NewUint(2))
256		if mid.IsZero() {
257			lo = u256.One()
258			continue
259		}
260		sq, overflow := u256.Zero().MulOverflow(mid, mid)
261		if overflow || sq.Gt(x) {
262			hi = mid
263		} else {
264			lo = mid
265		}
266	}
267	return lo
268}
269
270func alignedFullRangeTicks(spacing int32) (int32, int32) {
271	if spacing <= 0 {
272		spacing = 60
273	}
274	tl := (GnoswapMinTick / spacing) * spacing
275	tu := (GnoswapMaxTick / spacing) * spacing
276	if tl >= tu {
277		tl = -spacing
278		tu = spacing
279	}
280	return tl, tu
281}
282
283// GnoswapListedOf reports whether a launch was auto-listed on Gnoswap.
284func GnoswapListedOf(id string) bool {
285	return mustLaunch(id).GnoswapListed
286}
287
288// GnoswapPoolPathOf returns the Gnoswap pool path after listing (or empty).
289func GnoswapPoolPathOf(id string) string {
290	return mustLaunch(id).GnoswapPoolPath
291}
292
293// GnoswapNoteOf returns the listing status note.
294func GnoswapNoteOf(id string) string {
295	return mustLaunch(id).GnoswapNote
296}