Search Apps Documentation Source Content File Folder Download Copy Actions Download

dexprobe.gno

11.33 Kb · 288 lines
  1// Package dexprobe is a one-shot feasibility probe: it exercises every GnoSwap
  2// primitive an on-chain order engine (DCA / limit / stop-loss, keeper-driven,
  3// non-custodial or vault-funded) would ever need, so the real build is derisked
  4// in a single deploy. Each exported fn answers one unknown; a harness drives the
  5// multi-tx cases. Throwaway test realm — no product code here.
  6package dexprobe
  7
  8import (
  9	"strconv"
 10	"time"
 11
 12	"chain"
 13	"chain/banker"
 14
 15	"gno.land/p/nt/avl/v0"
 16
 17	"gno.land/r/gnoland/wugnot"
 18	"gno.land/r/gnoswap/gns"
 19	"gno.land/r/gnoswap/pool"
 20	"gno.land/r/gnoswap/router"
 21)
 22
 23const (
 24	WUG  = "gno.land/r/gnoland/wugnot"
 25	GNS  = "gno.land/r/gnoswap/gns"
 26	USDC = "gno.land/r/gnoswap/test_token/test_usdc"
 27	DAI  = "gno.land/r/gnoswap/test_token/test_dai"
 28
 29	poolWG    = "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000" // canonical, for pool reads
 30	routeBuy  = poolWG                                                  // WG -> GNS
 31	routeSell = "gno.land/r/gnoswap/gns:gno.land/r/gnoland/wugnot:3000" // GNS -> WG
 32	// 2-hop, no direct WG/DAI pool: WG -> USDC -> DAI
 33	routeMulti = "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/test_usdc:3000*POOL*gno.land/r/gnoswap/test_token/test_usdc:gno.land/r/gnoswap/test_token/test_dai:3000"
 34)
 35
 36var routerAddr = address("g1vc883gshu5z7ytk5cdynhc8c2dh67pdp4cszkp")
 37
 38var (
 39	vault   avl.Tree // owner -> int64 wugnot deposited (pre-funded custody model)
 40	counter int      // for the fail-atomicity probe
 41)
 42
 43// ---- helpers ----
 44func i64(n int64) string { return strconv.FormatInt(n, 10) }
 45func atoi(s string) int64 { n, _ := strconv.ParseInt(s, 10, 64); return n }
 46func dl() int64           { return time.Now().Unix() + 300 }
 47
 48// Fund lets the harness deposit native ugnot into this realm (for tip/native-send probes).
 49func Fund(cur realm) {}
 50
 51// ============================================================ PRICE READS ====
 52
 53func SpotTick() int32       { return pool.GetSlot0Tick(poolWG) }
 54func SpotSqrtPrice() string { return pool.GetSlot0SqrtPriceX96(poolWG) }
 55
 56// TwapProbe answers the #1 unknown: does a manipulation-resistant TWAP work?
 57// Returns "tick=<t> liq=<l> err=<e>". On un-warmed pools err is non-nil.
 58func TwapProbe(secondsAgo uint32) string {
 59	t, l, err := pool.GetTWAP(poolWG, secondsAgo)
 60	e := "nil"
 61	if err != nil {
 62		e = err.Error()
 63	}
 64	return "tick=" + i32(t) + " liq=" + l + " err=" + e
 65}
 66
 67// Cardinality reports the pool's observation-buffer size (1 = un-warmed → no TWAP).
 68func Cardinality() string {
 69	os, err := pool.GetObservationState(poolWG)
 70	if err != nil {
 71		return "err=" + err.Error()
 72	}
 73	return "card=" + u16(os.Cardinality()) + " next=" + u16(os.CardinalityNext())
 74}
 75
 76// GrowCardinality warms the TWAP buffer (Uniswap-v3 style). Crossing fn.
 77func GrowCardinality(cur realm, cardNext uint16) {
 78	pool.IncreaseObservationCardinalityNext(cross(cur), WUG, GNS, 3000, cardNext)
 79	chain.Emit("GrewCardinality", "next", u16(cardNext))
 80}
 81
 82// DryQuote calls GnoSwap's read-only quote FROM inside a realm (the slippage-floor
 83// dependency). Returns the expected GNS out for amountIn wugnot.
 84func DryQuote(amountIn int64) string {
 85	_, out, ok := router.DrySwapRoute(WUG, GNS, i64(amountIn), "EXACT_IN", routeBuy, "100", "1")
 86	return "out=" + out + " ok=" + boolS(ok)
 87}
 88
 89// BestTier scans the fee tiers for the deepest pool of a pair (tier-selection).
 90func BestTier(a, b string) string {
 91	best := uint32(0)
 92	bestLiq := int64(-1)
 93	for _, fee := range []uint32{100, 500, 3000, 10000} {
 94		for _, pp := range []string{a + ":" + b + ":" + u32(fee), b + ":" + a + ":" + u32(fee)} {
 95			if pool.ExistsPoolPath(pp) {
 96				liq := atoi(pool.GetLiquidity(pp))
 97				if liq > bestLiq {
 98					bestLiq, best = liq, fee
 99				}
100			}
101		}
102	}
103	return "bestFee=" + u32(best) + " liq=" + i64(bestLiq)
104}
105
106// ============================================================== SWAPS ========
107// All pull the caller's input via allowance, swap as payer/recipient, forward output.
108
109// BuyGns: WG -> GNS (baseline, allowance-funded). Returns GNS out.
110func BuyGns(cur realm, amountIn, minOut int64) int64 {
111	u, self := cur.Previous().Address(), cur.Address()
112	wugnot.TransferFrom(cross(cur), u, self, amountIn)
113	wugnot.Approve(cross(cur), routerAddr, amountIn)
114	b := gns.BalanceOf(self)
115	router.ExactInSingleSwapRoute(cross(cur), WUG, GNS, i64(amountIn), routeBuy, i64(minOut), "0", dl(), "")
116	out := gns.BalanceOf(self) - b
117	gns.Transfer(cross(cur), u, out)
118	return out
119}
120
121// SellGns: GNS -> WG (proves the SELL side). Returns wugnot out.
122func SellGns(cur realm, amountGns, minOut int64) int64 {
123	u, self := cur.Previous().Address(), cur.Address()
124	gns.TransferFrom(cross(cur), u, self, amountGns)
125	gns.Approve(cross(cur), routerAddr, amountGns)
126	b := wugnot.BalanceOf(self)
127	router.ExactInSingleSwapRoute(cross(cur), GNS, WUG, i64(amountGns), routeSell, i64(minOut), "0", dl(), "")
128	out := wugnot.BalanceOf(self) - b
129	wugnot.Transfer(cross(cur), u, out)
130	return out
131}
132
133// SellGnsToNative: sell GNS, unwrap wugnot -> native ugnot, deliver via banker.
134func SellGnsToNative(cur realm, amountGns, minOut int64) int64 {
135	u, self := cur.Previous().Address(), cur.Address()
136	gns.TransferFrom(cross(cur), u, self, amountGns)
137	gns.Approve(cross(cur), routerAddr, amountGns)
138	b := wugnot.BalanceOf(self)
139	router.ExactInSingleSwapRoute(cross(cur), GNS, WUG, i64(amountGns), routeSell, i64(minOut), "0", dl(), "")
140	out := wugnot.BalanceOf(self) - b
141	wugnot.Withdraw(cross(cur), out) // burn realm's wugnot -> native ugnot to realm
142	bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
143	bnk.SendCoins(self, u, chain.Coins{chain.NewCoin("ugnot", out)})
144	return out
145}
146
147// BuyExactGns: ExactOut — buy exactly amountOut GNS, spending <= amountInMax wugnot,
148// refunding the unused wugnot. Returns actual wugnot spent.
149func BuyExactGns(cur realm, amountOut, amountInMax int64) int64 {
150	u, self := cur.Previous().Address(), cur.Address()
151	wugnot.TransferFrom(cross(cur), u, self, amountInMax)
152	wugnot.Approve(cross(cur), routerAddr, amountInMax)
153	gb := gns.BalanceOf(self)
154	inStr, _ := router.ExactOutSingleSwapRoute(cross(cur), WUG, GNS, i64(amountOut), routeBuy, i64(amountInMax), "0", dl(), "")
155	gns.Transfer(cross(cur), u, gns.BalanceOf(self)-gb) // deliver GNS
156	spent := atoi(inStr)
157	if refund := amountInMax - spent; refund > 0 {
158		wugnot.Transfer(cross(cur), u, refund) // refund unused wugnot
159	}
160	return spent
161}
162
163// MultiHopBuy: WG -> USDC -> DAI (2 hops). Returns DAI out (string, from the router).
164// DAI stays in the realm (no DAI import needed to prove multi-hop works).
165func MultiHopBuy(cur realm, amountIn, minOut int64) string {
166	u, self := cur.Previous().Address(), cur.Address()
167	wugnot.TransferFrom(cross(cur), u, self, amountIn)
168	wugnot.Approve(cross(cur), routerAddr, amountIn)
169	_, out := router.ExactInSwapRoute(cross(cur), WUG, DAI, i64(amountIn), routeMulti, "100", i64(minOut), dl(), "")
170	return out
171}
172
173// FloorBuy: the on-chain slippage-floor design. Quote GnoSwap for THIS size, derive
174// minOut = expected*(1-maxSlippageBps) ON-CHAIN, then swap. Keeper can't set the floor.
175func FloorBuy(cur realm, amountIn, maxSlippageBps int64) int64 {
176	u, self := cur.Previous().Address(), cur.Address()
177	_, expStr, ok := router.DrySwapRoute(WUG, GNS, i64(amountIn), "EXACT_IN", routeBuy, "100", "1")
178	if !ok {
179		panic("dry quote unfillable")
180	}
181	minOut := atoi(expStr) * (10000 - maxSlippageBps) / 10000
182	wugnot.TransferFrom(cross(cur), u, self, amountIn)
183	wugnot.Approve(cross(cur), routerAddr, amountIn)
184	b := gns.BalanceOf(self)
185	router.ExactInSingleSwapRoute(cross(cur), WUG, GNS, i64(amountIn), routeBuy, i64(minOut), "0", dl(), "")
186	out := gns.BalanceOf(self) - b
187	gns.Transfer(cross(cur), u, out)
188	chain.Emit("FloorBuy", "expected", expStr, "minOut", i64(minOut), "got", i64(out))
189	return out
190}
191
192// ========================================================= KEEPER ECON =======
193
194// TipKeeper pays the caller `amount` ugnot from the realm's native balance (the
195// permissionless-keeper payment mechanic). Realm must be pre-Funded.
196func TipKeeper(cur realm, amount int64) {
197	bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
198	bnk.SendCoins(cur.Address(), cur.Previous().Address(), chain.Coins{chain.NewCoin("ugnot", amount)})
199	chain.Emit("TipPaid", "keeper", cur.Previous().Address().String(), "amount", i64(amount))
200}
201
202// FeeSkimBuy buys GNS, then skims feeBps of the output to `treasury` (revenue model).
203func FeeSkimBuy(cur realm, amountIn, feeBps int64, treasury address) int64 {
204	u, self := cur.Previous().Address(), cur.Address()
205	wugnot.TransferFrom(cross(cur), u, self, amountIn)
206	wugnot.Approve(cross(cur), routerAddr, amountIn)
207	b := gns.BalanceOf(self)
208	router.ExactInSingleSwapRoute(cross(cur), WUG, GNS, i64(amountIn), routeBuy, "1", "0", dl(), "")
209	out := gns.BalanceOf(self) - b
210	fee := out * feeBps / 10000
211	if fee > 0 {
212		gns.Transfer(cross(cur), treasury, fee)
213	}
214	gns.Transfer(cross(cur), u, out-fee)
215	return fee
216}
217
218// ============================================================ VAULT ==========
219// Pre-funded custody model: owner deposits wugnot; keeper draws per fill (no allowance).
220
221func VaultDeposit(cur realm, amount int64) {
222	u, self := cur.Previous().Address(), cur.Address()
223	wugnot.TransferFrom(cross(cur), u, self, amount)
224	vault.Set(u.String(), vaultBal(u)+amount)
225	chain.Emit("VaultDeposit", "owner", u.String(), "amount", i64(amount))
226}
227
228func VaultBalance(owner address) int64 { return vaultBal(owner) }
229
230// VaultSwapBuy: keeper-callable, draws from the owner's vault balance (not allowance),
231// swaps, forwards GNS to owner. `owner` is an arg because the keeper (not owner) calls.
232func VaultSwapBuy(cur realm, owner address, amountIn, minOut int64) int64 {
233	self := cur.Address()
234	bal := vaultBal(owner)
235	if bal < amountIn {
236		panic("insufficient vault balance")
237	}
238	vault.Set(owner.String(), bal-amountIn) // STATE-BEFORE-SEND
239	wugnot.Approve(cross(cur), routerAddr, amountIn)
240	b := gns.BalanceOf(self)
241	router.ExactInSingleSwapRoute(cross(cur), WUG, GNS, i64(amountIn), routeBuy, i64(minOut), "0", dl(), "")
242	out := gns.BalanceOf(self) - b
243	gns.Transfer(cross(cur), owner, out)
244	return out
245}
246
247func VaultWithdraw(cur realm, amount int64) {
248	u := cur.Previous().Address()
249	bal := vaultBal(u)
250	if bal < amount {
251		panic("insufficient vault balance")
252	}
253	vault.Set(u.String(), bal-amount) // STATE-BEFORE-SEND
254	wugnot.Transfer(cross(cur), u, amount)
255}
256
257func vaultBal(owner address) int64 {
258	v, ok := vault.Get(owner.String())
259	if !ok {
260		return 0
261	}
262	return v.(int64)
263}
264
265// ======================================================= FAIL ATOMICITY ======
266
267func Counter() int { return counter }
268
269// FailAtomic bumps the counter then forces a swap revert (impossible minOut). The whole
270// tx must revert → Counter() stays unchanged, proving state rolls back on a failed fill.
271func FailAtomic(cur realm, amountIn int64) {
272	counter++
273	u, self := cur.Previous().Address(), cur.Address()
274	wugnot.TransferFrom(cross(cur), u, self, amountIn)
275	wugnot.Approve(cross(cur), routerAddr, amountIn)
276	router.ExactInSingleSwapRoute(cross(cur), WUG, GNS, i64(amountIn), routeBuy, i64(amountIn*1000000), "0", dl(), "")
277}
278
279// ---- fmt helpers ----
280func i32(n int32) string  { return strconv.FormatInt(int64(n), 10) }
281func u16(n uint16) string { return strconv.FormatUint(uint64(n), 10) }
282func u32(n uint32) string { return strconv.FormatUint(uint64(n), 10) }
283func boolS(b bool) string { if b { return "true" }; return "false" }
284
285func Render(_ string) string {
286	return "# dexprobe (feasibility probe)\n\nLive tick: " + i32(SpotTick()) +
287		"\nCardinality: " + Cardinality() + "\nCounter: " + strconv.Itoa(counter) + "\n"
288}