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.78 Kb · 238 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		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		utils.FormatInt(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		u256.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}