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

swap_inner.gno

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