Search Apps Documentation Source Content File Folder Download Copy Actions Download

swap_inner.gno

6.73 Kb · 238 lines
  1package v1
  2
  3import (
  4	"gno.land/p/gnoswap/gnsmath"
  5	ufmt "gno.land/p/nt/ufmt/v0"
  6	"gno.land/r/gnoswap/access"
  7	"gno.land/r/gnoswap/router"
  8
  9	i256 "gno.land/p/gnoswap/int256"
 10	prbac "gno.land/p/gnoswap/rbac"
 11	u256 "gno.land/p/gnoswap/uint256"
 12
 13	pl "gno.land/r/gnoswap/pool"
 14	plv1 "gno.land/r/gnoswap/pool/v1"
 15)
 16
 17const (
 18	MIN_SQRT_RATIO string = "4295128739"                                        // same as TickMathGetSqrtRatioAtTick(MIN_TICK)
 19	MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" // same as TickMathGetSqrtRatioAtTick(MAX_TICK)
 20)
 21
 22// Precomputed sqrt price limits per fee tier and direction.
 23// TickMathGetSqrtRatioAtTick costs ~2M-2.4M gas per call and only depends on fee tier,
 24// so we compute all 8 values (4 fees × 2 directions) once at init and cache them.
 25// Callers use the cached pointer read-only (only .ToString() is called on the result).
 26var (
 27	sqrtPriceLimitForward  = make(map[uint32]*u256.Uint) // zeroForOne=true:  sqrtRatioAtTick(minTick+1) + 1
 28	sqrtPriceLimitBackward = make(map[uint32]*u256.Uint) // zeroForOne=false: sqrtRatioAtTick(maxTick-1) - 1
 29)
 30
 31func init() {
 32	for _, fee := range []uint32{plv1.FeeTier100, plv1.FeeTier500, plv1.FeeTier3000, plv1.FeeTier10000} {
 33		// zeroForOne=true: price must stay above minimum
 34		minTick := getMinTick(fee) + 1
 35		fwd := gnsmath.TickMathGetSqrtRatioAtTick(minTick) // returns a fresh allocation
 36		if fwd.IsZero() {
 37			fwd = u256.MustFromDecimal(MIN_SQRT_RATIO)
 38		}
 39		fwd.Add(fwd, one) // fwd += 1 in-place
 40		sqrtPriceLimitForward[fee] = fwd
 41
 42		// zeroForOne=false: price must stay below maximum
 43		maxTick := getMaxTick(fee) - 1
 44		bwd := gnsmath.TickMathGetSqrtRatioAtTick(maxTick) // returns a fresh allocation
 45		if bwd.IsZero() {
 46			bwd = u256.MustFromDecimal(MAX_SQRT_RATIO)
 47		}
 48		bwd.Sub(bwd, one) // bwd -= 1 in-place
 49		sqrtPriceLimitBackward[fee] = bwd
 50	}
 51}
 52
 53// swapInner executes the core swap logic by interacting with the pool contract.
 54// Returns poolRecv (tokens received by pool) and poolOut (tokens sent by pool).
 55func (r *routerV1) swapInner(
 56	_ int,
 57	rlm realm,
 58	amountSpecified int64,
 59	recipient address,
 60	sqrtPriceLimitX96 *u256.Uint,
 61	data SwapCallbackData,
 62) (int64, int64) {
 63	token0Path, token1Path := data.tokenIn, data.tokenOut
 64	zeroForOne := data.tokenIn < data.tokenOut
 65
 66	if !zeroForOne {
 67		token0Path, token1Path = token1Path, token0Path
 68	}
 69
 70	sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96)
 71
 72	amount0Str, amount1Str := pl.Swap(
 73		cross(rlm),
 74		token0Path,
 75		token1Path,
 76		data.fee,
 77		recipient,
 78		zeroForOne,
 79		formatInt64(amountSpecified),
 80		sqrtPriceLimitX96.ToString(),
 81		data.payer,
 82		func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error {
 83			// assert is pool to prevent unauthorized callback
 84			caller := cur.Previous().Address()
 85			access.AssertIsPool(caller)
 86
 87			return router.SwapCallback(cross(cur), token0Path, token1Path, amount0Delta, amount1Delta, data.payer)
 88		},
 89	)
 90
 91	amount0 := i256.MustFromDecimal(amount0Str)
 92	amount1 := i256.MustFromDecimal(amount1Str)
 93
 94	poolOut, poolRecv := i256MinMax(amount0, amount1)
 95	if poolRecv.IsOverflow() || poolOut.IsOverflow() {
 96		panic("overflow in swapInner")
 97	}
 98
 99	return poolRecv.Int64(), poolOut.Int64()
100}
101
102// swapDryInner performs a dry-run of a swap operation without executing it.
103func (r *routerV1) swapDryInner(
104	amountSpecified int64,
105	sqrtPriceLimitX96 *u256.Uint,
106	data SwapCallbackData,
107) (int64, int64) {
108	zeroForOne := data.tokenIn < data.tokenOut
109	sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96)
110
111	// check possible
112	amount0Str, amount1Str, ok := pl.DrySwap(
113		data.tokenIn,
114		data.tokenOut,
115		data.fee,
116		zeroForOne,
117		formatInt64(amountSpecified),
118		sqrtPriceLimitX96.ToString(),
119	)
120	if !ok {
121		return 0, 0
122	}
123
124	amount0 := i256.MustFromDecimal(amount0Str)
125	amount1 := i256.MustFromDecimal(amount1Str)
126
127	poolOut, poolRecv := i256MinMax(amount0, amount1)
128	if poolRecv.IsOverflow() || poolOut.IsOverflow() {
129		panic("overflow in swapDryInner")
130	}
131
132	return poolRecv.Int64(), poolOut.Int64()
133}
134
135// RealSwapExecutor implements SwapExecutor for actual swaps.
136type RealSwapExecutor struct {
137	rlm    realm
138	router *routerV1
139}
140
141// execute performs the actual swap execution.
142func (e *RealSwapExecutor) execute(p *SingleSwapParams) (int64, int64) {
143	caller := e.rlm.Previous().Address()
144	recipient := access.MustGetAddress(prbac.ROLE_ROUTER.String())
145
146	return e.router.swapInner(
147		0,
148		e.rlm,
149		p.amountSpecified,
150		recipient,             // if single swap => user will receive
151		p.SqrtPriceLimitX96(), // sqrtPriceLimitX96
152		newSwapCallbackData(p, caller),
153	)
154}
155
156// DrySwapExecutor implements SwapExecutor for dry swaps.
157type DrySwapExecutor struct {
158	router *routerV1
159	// payer is the user's address resolved at the entry point (DrySwapRoute),
160	// since dry-run paths do not have a realm value to read PreviousRealm from.
161	payer address
162}
163
164// execute performs the dry swap execution.
165func (e *DrySwapExecutor) execute(p *SingleSwapParams) (int64, int64) {
166	return e.router.swapDryInner(
167		p.amountSpecified,
168		zero,
169		newSwapCallbackData(p, e.payer),
170	)
171}
172
173// calculateSqrtPriceLimitForSwap returns the price limit for a swap operation.
174// If a non-zero limit is provided by the caller, it is returned as-is.
175// Otherwise, returns the precomputed limit for the given fee tier and direction.
176// The returned pointer is shared (read-only — callers must not mutate it).
177func calculateSqrtPriceLimitForSwap(zeroForOne bool, fee uint32, sqrtPriceLimitX96 *u256.Uint) *u256.Uint {
178	if !sqrtPriceLimitX96.IsZero() {
179		return sqrtPriceLimitX96
180	}
181
182	if zeroForOne {
183		return mustGetSqrtPriceLimit(sqrtPriceLimitForward, fee)
184	}
185	return mustGetSqrtPriceLimit(sqrtPriceLimitBackward, fee)
186}
187
188func mustGetSqrtPriceLimit(limits map[uint32]*u256.Uint, fee uint32) *u256.Uint {
189	limit, ok := limits[fee]
190	if !ok {
191		panic(addDetailToError(
192			errInvalidPoolFeeTier,
193			ufmt.Sprintf("unknown fee(%d)", fee),
194		))
195	}
196	return limit
197}
198
199// getMinTick returns the minimum tick value for a given fee tier.
200// The implementation follows Uniswap V3's tick spacing rules where
201// lower fee tiers allow for finer price granularity.
202func getMinTick(fee uint32) int32 {
203	switch fee {
204	case 100:
205		return -887272
206	case 500:
207		return -887270
208	case 3000:
209		return -887220
210	case 10000:
211		return -887200
212	default:
213		panic(addDetailToError(
214			errInvalidPoolFeeTier,
215			ufmt.Sprintf("unknown fee(%d)", fee),
216		))
217	}
218}
219
220// getMaxTick returns the maximum tick value for a given fee tier.
221// The max tick values are the exact negatives of min tick values.
222func getMaxTick(fee uint32) int32 {
223	switch fee {
224	case 100:
225		return 887272
226	case 500:
227		return 887270
228	case 3000:
229		return 887220
230	case 10000:
231		return 887200
232	default:
233		panic(addDetailToError(
234			errInvalidPoolFeeTier,
235			ufmt.Sprintf("unknown fee(%d)", fee),
236		))
237	}
238}