gnoswap_list.gno
9.00 Kb · 296 lines
1package padv10
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}