Search Apps Documentation Source Content File Folder Download Copy Actions Download

swap.gno

23.79 Kb · 781 lines
  1package v1
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"time"
  7
  8	ufmt "gno.land/p/nt/ufmt/v0"
  9	"gno.land/r/gnoswap/access"
 10	"gno.land/r/gnoswap/halt"
 11
 12	"gno.land/p/gnoswap/gnsmath"
 13	i256 "gno.land/p/gnoswap/int256"
 14	u256 "gno.land/p/gnoswap/uint256"
 15
 16	pl "gno.land/r/gnoswap/pool"
 17)
 18
 19// Hook functions allow external contracts to be notified of swap events.
 20var (
 21	// MUST BE IMMUTABLE.
 22	// DO NOT USE THIS VALUE IN ANY ARITHMETIC OPERATIONS' INITIALIZATION
 23	zero           = u256.Zero()
 24	zeroI256       = i256.Zero() /* readonly */
 25	fixedPointQ128 = u256.MustFromDecimal(Q128)
 26
 27	maxInt256 = u256.MustFromDecimal(MAX_INT256)
 28	maxInt64  = i256.Zero().SetInt64(INT64_MAX)
 29	minInt64  = i256.Zero().SetInt64(INT64_MIN)
 30)
 31
 32// SetTickCrossHook sets the hook function called when a tick is crossed during swaps.
 33//
 34// Allows staker to monitor liquidity changes at price levels.
 35// Used for reward calculation when positions enter/exit range.
 36//
 37// Only callable by staker contract.
 38func (i *poolV1) SetTickCrossHook(_ int, rlm realm, hook func(cur realm, poolPath string, tickId int32, zeroForOne bool, timestamp int64)) {
 39	if !rlm.IsCurrent() {
 40		panic(errSpoofedRealm)
 41	}
 42
 43	i.assertPoolUnlocked()
 44	halt.AssertIsNotHaltedPool()
 45
 46	caller := rlm.Previous().Address()
 47	access.AssertIsStaker(caller)
 48
 49	i.lockPool(0, rlm)
 50	defer i.unlockPool(0, rlm)
 51
 52	err := i.store.SetTickCrossHook(0, rlm, hook)
 53	if err != nil {
 54		panic(err)
 55	}
 56}
 57
 58// SetSwapStartHook sets the hook function called at the beginning of a swap.
 59//
 60// Enables pre-swap state tracking for reward distribution.
 61// Captures timestamp for time-weighted calculations.
 62//
 63// Only callable by staker contract.
 64func (i *poolV1) SetSwapStartHook(_ int, rlm realm, hook func(cur realm, poolPath string, timestamp int64)) {
 65	if !rlm.IsCurrent() {
 66		panic(errSpoofedRealm)
 67	}
 68
 69	i.assertPoolUnlocked()
 70	halt.AssertIsNotHaltedPool()
 71
 72	caller := rlm.Previous().Address()
 73	access.AssertIsStaker(caller)
 74
 75	i.lockPool(0, rlm)
 76	defer i.unlockPool(0, rlm)
 77
 78	err := i.store.SetSwapStartHook(0, rlm, hook)
 79	if err != nil {
 80		panic(err)
 81	}
 82}
 83
 84// SetSwapEndHook sets the hook function called at the end of a swap.
 85//
 86// Finalizes reward calculations after swap completion.
 87// Allows error propagation to revert invalid swaps.
 88//
 89// Only callable by staker contract.
 90func (i *poolV1) SetSwapEndHook(_ int, rlm realm, hook func(cur realm, poolPath string) error) {
 91	if !rlm.IsCurrent() {
 92		panic(errSpoofedRealm)
 93	}
 94
 95	i.assertPoolUnlocked()
 96	halt.AssertIsNotHaltedPool()
 97
 98	caller := rlm.Previous().Address()
 99	access.AssertIsStaker(caller)
100
101	i.lockPool(0, rlm)
102	defer i.unlockPool(0, rlm)
103
104	err := i.store.SetSwapEndHook(0, rlm, hook)
105	if err != nil {
106		panic(err)
107	}
108}
109
110// SwapResult encapsulates all state changes from a swap.
111// It ensures atomic state transitions that can be applied at once.
112type SwapResult struct {
113	Amount0              *i256.Int
114	Amount1              *i256.Int
115	NewSqrtPrice         *u256.Uint
116	NewTick              int32
117	NewLiquidity         *u256.Uint
118	NewProtocolFeeToken0 int64
119	NewProtocolFeeToken1 int64
120	FeeGrowthGlobal0X128 *u256.Uint
121	FeeGrowthGlobal1X128 *u256.Uint
122}
123
124// SwapComputation encapsulates the pure computation logic for swaps.
125type SwapComputation struct {
126	AmountSpecified   *i256.Int
127	SqrtPriceLimitX96 *u256.Uint
128	ZeroForOne        bool
129	ExactInput        bool
130	InitialState      SwapState
131	Cache             *SwapCache
132}
133
134// Swap executes a swap with callback pattern for optimistic transfers.
135// This allows flash swaps where tokens are sent before payment is received.
136//
137// The flow is:
138// 1. Pool sends output tokens to recipient
139// 2. Pool calls callback on msg.sender
140// 3. Callback must ensure pool receives input tokens
141// 4. Pool validates its balance increased correctly
142//
143// Parameters:
144//   - token0Path: Path of token0 in the pool
145//   - token1Path: Path of token1 in the pool
146//   - fee: Pool fee tier
147//   - recipient: Address to receive output tokens
148//   - zeroForOne: Direction of swap (true = token0 to token1)
149//   - amountSpecified: Exact input (positive) or exact output (negative)
150//   - sqrtPriceLimitX96: Price limit for the swap
151//   - payer: Address that provides input tokens
152//   - swapCallback: Callback function to handle token transfers
153//
154// Returns amount0 and amount1 deltas as strings.
155func (i *poolV1) Swap(
156	_ int,
157	rlm realm,
158	token0Path string,
159	token1Path string,
160	fee uint32,
161	recipient address,
162	zeroForOne bool,
163	amountSpecified string,
164	sqrtPriceLimitX96 string,
165	payer address,
166	swapCallback func(cur realm, amount0Delta, amount1Delta int64, _ *pl.CallbackMarker) error,
167) (string, string) {
168	if !rlm.IsCurrent() {
169		panic(errSpoofedRealm)
170	}
171
172	i.assertPoolUnlocked()
173	halt.AssertIsNotHaltedPool()
174
175	assertIsNotUserCall(0, rlm)
176	assertIsValidTokenOrder(token0Path, token1Path)
177
178	if amountSpecified == "0" {
179		panic(newErrorWithDetail(
180			errInvalidSwapAmount,
181			"amountSpecified == 0",
182		))
183	}
184
185	pool := i.mustGetPoolBy(token0Path, token1Path, fee)
186
187	slot0Start := pool.Slot0()
188	i.lockPool(0, rlm)
189	defer i.unlockPool(0, rlm)
190
191	// no liquidity -> no swap, return zero amounts
192	if pool.Liquidity().IsZero() {
193		return "0", "0"
194	}
195
196	blockTimestamp := time.Now().Unix()
197
198	// Call swap start hook if set
199	if i.store.HasSwapStartHook() {
200		swapStartHook := i.store.GetSwapStartHook()
201
202		if swapStartHook != nil {
203			swapStartHook(cross(rlm), pool.PoolPath(), blockTimestamp)
204		}
205	}
206
207	defer func() {
208		if i.store.HasSwapEndHook() {
209			swapEndHook := i.store.GetSwapEndHook()
210
211			if swapEndHook != nil {
212				err := swapEndHook(cross(rlm), pool.PoolPath())
213				if err != nil {
214					panic(err)
215				}
216			}
217		}
218	}()
219
220	sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96)
221	validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit)
222
223	amounts := i256.MustFromDecimal(amountSpecified)
224	feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne)
225	feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
226	cache := newSwapCache(feeProtocol, pool.Liquidity().Clone(), blockTimestamp)
227	state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart.Clone(), slot0Start)
228
229	comp := SwapComputation{
230		AmountSpecified:   amounts,
231		SqrtPriceLimitX96: sqrtPriceLimit,
232		ZeroForOne:        zeroForOne,
233		ExactInput:        amounts.Gt(zeroI256),
234		InitialState:      state,
235		Cache:             cache,
236	}
237
238	var onTickCross tickCrossHookFn
239	if i.store.HasTickCrossHook() {
240		hook := i.store.GetTickCrossHook()
241		if hook != nil {
242			onTickCross = func(poolPath string, tickId int32, zeroForOne bool, timestamp int64) {
243				hook(cross(rlm), poolPath, tickId, zeroForOne, timestamp)
244			}
245		}
246	}
247
248	result, err := i.computeSwap(pool, comp, onTickCross)
249	if err != nil {
250		panic(err)
251	}
252
253	// Update oracle BEFORE applying swap result (using pre-swap state)
254	if result.NewTick != pool.Slot0Tick() {
255		err := writeObservationByPool(pool, cache.blockTimestamp, pool.Slot0Tick(), pool.Liquidity())
256		if err != nil {
257			panic(err)
258		}
259	}
260
261	applySwapResult(pool, result)
262
263	// transfer swap result to recipient then receive input tokens from swap callback
264	if zeroForOne {
265		// receive token0 from swap callback
266		// send token1 to recipient (output)
267		if result.Amount1.IsNeg() {
268			i.safeTransfer(0, rlm, pool, recipient, token1Path, result.Amount1.Abs(), false)
269		}
270		i.safeSwapCallback(0, rlm, pool, token0Path, result.Amount0, result.Amount1, zeroForOne, swapCallback)
271	} else {
272		// receive token1 from swap callback
273		// send token0 to recipient (output)
274		if result.Amount0.IsNeg() {
275			i.safeTransfer(0, rlm, pool, recipient, token0Path, result.Amount0.Abs(), true)
276		}
277		i.safeSwapCallback(0, rlm, pool, token1Path, result.Amount1, result.Amount0, zeroForOne, swapCallback)
278	}
279
280	lastObservation, err := lastObservation(pool.ObservationState())
281	if err != nil {
282		panic(err)
283	}
284
285	token0Amount := result.Amount0.ToString()
286	token1Amount := result.Amount1.ToString()
287
288	previousRealm := rlm.Previous()
289
290	chain.Emit(
291		"Swap",
292		"prevAddr", previousRealm.Address().String(),
293		"prevRealm", previousRealm.PkgPath(),
294		"poolPath", pool.PoolPath(),
295		"zeroForOne", formatBool(zeroForOne),
296		"requestAmount", amountSpecified,
297		"sqrtPriceLimitX96", sqrtPriceLimitX96,
298		"payer", payer.String(),
299		"recipient", recipient.String(),
300		"token0Amount", token0Amount,
301		"token1Amount", token1Amount,
302		"protocolFee0", formatInt(pool.ProtocolFeesToken0()),
303		"protocolFee1", formatInt(pool.ProtocolFeesToken1()),
304		"sqrtPriceX96", pool.Slot0SqrtPriceX96().ToString(),
305		"exactIn", strconv.FormatBool(comp.ExactInput),
306		"currentTick", strconv.FormatInt(int64(pool.Slot0Tick()), 10),
307		"liquidity", pool.Liquidity().ToString(),
308		"feeGrowthGlobal0X128", pool.FeeGrowthGlobal0X128().ToString(),
309		"feeGrowthGlobal1X128", pool.FeeGrowthGlobal1X128().ToString(),
310		"balanceToken0", formatInt(pool.BalanceToken0()),
311		"balanceToken1", formatInt(pool.BalanceToken1()),
312		"tickCumulative", formatInt(lastObservation.TickCumulative()),
313		"liquidityCumulative", lastObservation.LiquidityCumulative(),
314		"secondsPerLiquidityCumulativeX128", lastObservation.SecondsPerLiquidityCumulativeX128(),
315		"observationTimestamp", formatInt(lastObservation.BlockTimestamp()),
316	)
317
318	return token0Amount, token1Amount
319}
320
321// DrySwap simulates a swap without modifying pool state.
322// Returns amount0, amount1 and a success boolean.
323// Returns false if pool has no liquidity or computation fails.
324func (i *poolV1) DrySwap(
325	token0Path string,
326	token1Path string,
327	fee uint32,
328	zeroForOne bool,
329	amountSpecified string,
330	sqrtPriceLimitX96 string,
331) (string, string, bool) {
332	if amountSpecified == "0" {
333		return "0", "0", false
334	}
335
336	pool := i.mustGetPoolBy(token0Path, token1Path, fee)
337	poolSnapshot := pool.Clone()
338
339	// no liquidity -> simulation fails
340	if poolSnapshot.Liquidity().IsZero() {
341		return "0", "0", false
342	}
343
344	slot0Start := poolSnapshot.Slot0()
345	sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96)
346	validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit)
347
348	amounts := i256.MustFromDecimal(amountSpecified)
349	feeGrowthGlobalX128 := getFeeGrowthGlobal(poolSnapshot, zeroForOne)
350	feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
351	cache := newSwapCache(feeProtocol, poolSnapshot.Liquidity().Clone(), time.Now().Unix())
352	state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart, slot0Start)
353
354	comp := SwapComputation{
355		AmountSpecified:   amounts,
356		SqrtPriceLimitX96: sqrtPriceLimit,
357		ZeroForOne:        zeroForOne,
358		ExactInput:        amounts.Gt(zeroI256),
359		InitialState:      state,
360		Cache:             cache,
361	}
362
363	result, err := i.computeSwap(poolSnapshot, comp, nil)
364	if err != nil {
365		return "0", "0", false
366	}
367
368	if zeroForOne {
369		if poolSnapshot.BalanceToken1() < safeConvertToInt64(result.Amount1.Abs()) {
370			return "0", "0", false
371		}
372	} else {
373		if poolSnapshot.BalanceToken0() < safeConvertToInt64(result.Amount0.Abs()) {
374			return "0", "0", false
375		}
376	}
377
378	return result.Amount0.ToString(), result.Amount1.ToString(), true
379}
380
381// tickCrossHookFn is invoked from inside the swap loop whenever an
382// initialized tick is crossed. DrySwap passes nil to skip side effects.
383type tickCrossHookFn func(poolPath string, tickId int32, zeroForOne bool, timestamp int64)
384
385// computeSwap performs the core swap computation without modifying pool state.
386// The computation continues until either:
387// - The entire amount is consumed (amountSpecifiedRemaining = 0)
388// - The price limit is reached (sqrtPriceX96 = sqrtPriceLimitX96)
389//
390// Important: This function is critical for AMM price discovery. It iterates through
391// tick ranges, calculating swap amounts and fees for each liquidity segment.
392// Returns an error if the computation fails at any step.
393//
394// The optional `onTickCross` callback is invoked when an initialized tick is
395// crossed; Swap supplies a hook that performs a cross-realm call into the
396// configured tick-cross hook, while DrySwap passes nil.
397func (i *poolV1) computeSwap(pool *pl.Pool, comp SwapComputation, onTickCross tickCrossHookFn) (*SwapResult, error) {
398	state := comp.InitialState
399	var err error
400
401	// Compute swap steps until completion
402	for shouldContinueSwap(state, comp.SqrtPriceLimitX96) {
403		state, err = i.computeSwapStep(state, pool, comp.ZeroForOne, comp.SqrtPriceLimitX96, comp.ExactInput, comp.Cache, onTickCross)
404		if err != nil {
405			return nil, err
406		}
407	}
408
409	// Calculate final amounts
410	amount0 := state.amountCalculated
411	amount1 := i256.Zero().Sub(comp.AmountSpecified, state.amountSpecifiedRemaining)
412	if comp.ZeroForOne == comp.ExactInput {
413		amount0, amount1 = amount1, amount0
414	}
415
416	// Prepare result
417	result := &SwapResult{
418		Amount0:              amount0,
419		Amount1:              amount1,
420		NewSqrtPrice:         state.sqrtPriceX96,
421		NewTick:              state.tick,
422		NewLiquidity:         state.liquidity,
423		NewProtocolFeeToken0: pool.ProtocolFeesToken0(),
424		NewProtocolFeeToken1: pool.ProtocolFeesToken1(),
425		FeeGrowthGlobal0X128: pool.FeeGrowthGlobal0X128(),
426		FeeGrowthGlobal1X128: pool.FeeGrowthGlobal1X128(),
427	}
428
429	// Update protocol fees if necessary
430	if comp.ZeroForOne {
431		if state.protocolFee.Gt(zero) {
432			result.NewProtocolFeeToken0 = safeAddInt64(result.NewProtocolFeeToken0, safeConvertToInt64(state.protocolFee))
433		}
434		result.FeeGrowthGlobal0X128 = state.feeGrowthGlobalX128.Clone()
435	} else {
436		if state.protocolFee.Gt(zero) {
437			result.NewProtocolFeeToken1 = safeAddInt64(result.NewProtocolFeeToken1, safeConvertToInt64(state.protocolFee))
438		}
439		result.FeeGrowthGlobal1X128 = state.feeGrowthGlobalX128.Clone()
440	}
441
442	return result, nil
443}
444
445// applySwapResult updates pool state with computed results.
446// All state changes are applied at once to maintain consistency
447func applySwapResult(pool *pl.Pool, result *SwapResult) {
448	slot0 := pool.Slot0()
449	slot0.SetSqrtPriceX96(result.NewSqrtPrice)
450	slot0.SetTick(result.NewTick)
451	pool.SetSlot0(slot0)
452
453	pool.SetLiquidity(result.NewLiquidity)
454	pool.SetProtocolFeesToken0(result.NewProtocolFeeToken0)
455	pool.SetProtocolFeesToken1(result.NewProtocolFeeToken1)
456	pool.SetFeeGrowthGlobal0X128(result.FeeGrowthGlobal0X128)
457	pool.SetFeeGrowthGlobal1X128(result.FeeGrowthGlobal1X128)
458}
459
460// validatePriceLimits ensures the provided price limit is valid for the swap direction
461// The function enforces that:
462// For zeroForOne (selling token0):
463//   - Price limit must be below current price
464//   - Price limit must be above MIN_SQRT_RATIO
465//
466// For !zeroForOne (selling token1):
467//   - Price limit must be above current price
468//   - Price limit must be below MAX_SQRT_RATIO
469func validatePriceLimits(slot0 pl.Slot0, zeroForOne bool, sqrtPriceLimitX96 *u256.Uint) {
470	if zeroForOne {
471		cond1 := sqrtPriceLimitX96.Lt(slot0.SqrtPriceX96())
472		cond2 := sqrtPriceLimitX96.Gt(minSqrtRatio)
473		if !(cond1 && cond2) {
474			panic(newErrorWithDetail(
475				errPriceOutOfRange,
476				ufmt.Sprintf("sqrtPriceLimitX96(%s) < slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) > MIN_SQRT_RATIO(%s)",
477					sqrtPriceLimitX96.ToString(),
478					slot0.SqrtPriceX96().ToString(),
479					sqrtPriceLimitX96.ToString(),
480					MIN_SQRT_RATIO),
481			))
482		}
483	} else {
484		cond1 := sqrtPriceLimitX96.Gt(slot0.SqrtPriceX96())
485		cond2 := sqrtPriceLimitX96.Lt(maxSqrtRatio)
486		if !(cond1 && cond2) {
487			panic(newErrorWithDetail(
488				errPriceOutOfRange,
489				ufmt.Sprintf("sqrtPriceLimitX96(%s) > slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) < MAX_SQRT_RATIO(%s)",
490					sqrtPriceLimitX96.ToString(),
491					slot0.SqrtPriceX96().ToString(),
492					sqrtPriceLimitX96.ToString(),
493					MAX_SQRT_RATIO),
494			))
495		}
496	}
497}
498
499// getFeeProtocol returns the appropriate fee protocol based on zero for one.
500// When zeroForOne is true, we want the lower 4 bits (% 16).
501// Otherwise, we want the upper 4 bits (/ 16).
502func getFeeProtocol(slot0 pl.Slot0, zeroForOne bool) uint8 {
503	shift := uint8(0)
504	if !zeroForOne {
505		shift = 4
506	}
507	return (slot0.FeeProtocol() >> shift) & uint8(0xF)
508}
509
510// getFeeGrowthGlobal returns the appropriate fee growth global based on zero for one.
511func getFeeGrowthGlobal(pool *pl.Pool, zeroForOne bool) *u256.Uint {
512	if zeroForOne {
513		return pool.FeeGrowthGlobal0X128().Clone()
514	}
515	return pool.FeeGrowthGlobal1X128().Clone()
516}
517
518// shouldContinueSwap checks if swap should continue based on remaining amount and price limit.
519func shouldContinueSwap(state SwapState, sqrtPriceLimitX96 *u256.Uint) bool {
520	return !state.amountSpecifiedRemaining.IsZero() && !state.sqrtPriceX96.Eq(sqrtPriceLimitX96)
521}
522
523// computeSwapStep executes a single step of swap and returns new state
524func (i *poolV1) computeSwapStep(
525	state SwapState,
526	pool *pl.Pool,
527	zeroForOne bool,
528	sqrtPriceLimitX96 *u256.Uint,
529	exactInput bool,
530	cache *SwapCache,
531	onTickCross tickCrossHookFn,
532) (SwapState, error) {
533	step := computeSwapStepInit(state, pool, zeroForOne)
534
535	// determining the price target for this step
536	sqrtRatioTargetX96 := computeTargetSqrtRatio(step, sqrtPriceLimitX96, zeroForOne).Clone()
537
538	// computing the amounts to be swapped at this step
539	var (
540		newState SwapState
541		err      error
542	)
543
544	newState, step = computeAmounts(state, sqrtRatioTargetX96, pool, step)
545	newState, err = updateAmounts(step, newState, exactInput)
546	if err != nil {
547		return state, err
548	}
549
550	// if the protocol fee is on, calculate how much is owed,
551	// decrement fee amount, and increment protocol fee
552	if cache.feeProtocol > 0 {
553		newState, step, err = updateFeeProtocol(step, cache.feeProtocol, newState)
554		if err != nil {
555			return state, err
556		}
557	}
558
559	// update global fee tracker
560	if newState.liquidity.Gt(u256.Zero()) {
561		update := u256.MulDiv(step.feeAmount, fixedPointQ128, newState.liquidity)
562		feeGrowthGlobalX128 := u256.Zero().Add(newState.feeGrowthGlobalX128, update)
563		newState.setFeeGrowthGlobalX128(feeGrowthGlobalX128)
564	}
565
566	// handling tick transitions
567	if newState.sqrtPriceX96.Eq(step.sqrtPriceNextX96) {
568		newState = i.tickTransition(step, zeroForOne, newState, pool, cache, onTickCross)
569	} else if newState.sqrtPriceX96.Neq(step.sqrtPriceStartX96) {
570		newState.setTick(gnsmath.TickMathGetTickAtSqrtRatio(newState.sqrtPriceX96))
571	}
572
573	return newState, nil
574}
575
576// updateFeeProtocol calculates and updates protocol fees for the current step.
577func updateFeeProtocol(step StepComputations, feeProtocol uint8, state SwapState) (SwapState, StepComputations, error) {
578	delta := u256.Zero().Div(step.feeAmount, u256.NewUint(uint64(feeProtocol)))
579
580	newFeeAmount, overflow := u256.Zero().SubOverflow(step.feeAmount, delta)
581	if overflow {
582		return state, step, errUnderflow
583	}
584
585	step.feeAmount = newFeeAmount
586
587	newProtocolFee, overflow := u256.Zero().AddOverflow(state.protocolFee, delta)
588	if overflow {
589		return state, step, errOverflow
590	}
591	state.protocolFee = newProtocolFee
592
593	return state, step, nil
594}
595
596// computeSwapStepInit initializes the computation for a single swap step.
597func computeSwapStepInit(state SwapState, pool *pl.Pool, zeroForOne bool) StepComputations {
598	var step StepComputations
599	step.sqrtPriceStartX96 = state.sqrtPriceX96
600	tickNext, initialized := tickBitmapNextInitializedTickWithInOneWord(
601		pool,
602		state.tick,
603		pool.TickSpacing(),
604		zeroForOne,
605	)
606
607	step.tickNext = tickNext
608	step.initialized = initialized
609
610	// prevent overshoot the min/max tick
611	step.clampTickNext()
612	// get the price for the next tick
613	step.sqrtPriceNextX96 = gnsmath.TickMathGetSqrtRatioAtTick(step.tickNext)
614	return step
615}
616
617// computeTargetSqrtRatio determines the target sqrt price for the current swap step.
618func computeTargetSqrtRatio(step StepComputations, sqrtPriceLimitX96 *u256.Uint, zeroForOne bool) *u256.Uint {
619	if shouldUsePriceLimit(step.sqrtPriceNextX96, sqrtPriceLimitX96, zeroForOne) {
620		return sqrtPriceLimitX96
621	}
622	return step.sqrtPriceNextX96
623}
624
625// shouldUsePriceLimit returns true if the price limit should be used instead of the next tick price
626func shouldUsePriceLimit(sqrtPriceNext, sqrtPriceLimit *u256.Uint, zeroForOne bool) bool {
627	if zeroForOne {
628		return sqrtPriceNext.Lt(sqrtPriceLimit)
629	}
630	return sqrtPriceNext.Gt(sqrtPriceLimit)
631}
632
633// computeAmounts calculates the input and output amounts for the current swap step.
634func computeAmounts(state SwapState, sqrtRatioTargetX96 *u256.Uint, pool *pl.Pool, step StepComputations) (SwapState, StepComputations) {
635	sqrtPriceX96, amountIn, amountOut, feeAmount := gnsmath.SwapMathComputeSwapStep(
636		state.sqrtPriceX96,
637		sqrtRatioTargetX96,
638		state.liquidity,
639		state.amountSpecifiedRemaining,
640		uint64(pool.Fee()),
641	)
642
643	step.amountIn = amountIn
644	step.amountOut = amountOut
645	step.feeAmount = feeAmount
646
647	state.setSqrtPriceX96(sqrtPriceX96)
648
649	return state, step
650}
651
652// updateAmounts calculates new remaining and calculated amounts based on the swap step.
653// For exact input swaps:
654//   - Decrements remaining input amount by (amountIn + feeAmount)
655//   - Decrements calculated amount by amountOut
656//
657// For exact output swaps:
658//   - Increments remaining output amount by amountOut
659//   - Increments calculated amount by (amountIn + feeAmount)
660func updateAmounts(step StepComputations, state SwapState, exactInput bool) (SwapState, error) {
661	amountInWithFeeU256 := u256.Zero().Add(step.amountIn, step.feeAmount)
662	if amountInWithFeeU256.Gt(maxInt256) {
663		return state, errOverflow
664	}
665
666	amountInWithFee := i256.FromUint256(amountInWithFeeU256)
667	if step.amountOut.Gt(maxInt256) {
668		return state, errOverflow
669	}
670
671	var (
672		amountSpecifiedRemaining *i256.Int
673		amountCalculated         *i256.Int
674		overflow                 bool
675	)
676
677	if exactInput {
678		amountSpecifiedRemaining, overflow = i256.Zero().SubOverflow(state.amountSpecifiedRemaining, amountInWithFee)
679		if overflow {
680			return state, errUnderflow
681		}
682		amountCalculated, overflow = i256.Zero().SubOverflow(state.amountCalculated, i256.FromUint256(step.amountOut))
683		if overflow {
684			return state, errUnderflow
685		}
686	} else {
687		amountSpecifiedRemaining, overflow = i256.Zero().AddOverflow(state.amountSpecifiedRemaining, i256.FromUint256(step.amountOut))
688		if overflow {
689			return state, errOverflow
690		}
691		amountCalculated, overflow = i256.Zero().AddOverflow(state.amountCalculated, amountInWithFee)
692		if overflow {
693			return state, errOverflow
694		}
695	}
696
697	// If an overflowed value is stored in state, it may cause problems in the next step
698	if amountCalculated.Gt(maxInt64) || amountSpecifiedRemaining.Gt(maxInt64) {
699		return state, errOverflow
700	}
701
702	// If an underflowed value is stored in state, it may cause problems in the next step
703	if amountCalculated.Lt(minInt64) || amountSpecifiedRemaining.Lt(minInt64) {
704		return state, errUnderflow
705	}
706
707	state.amountSpecifiedRemaining = amountSpecifiedRemaining
708	state.amountCalculated = amountCalculated
709
710	return state, nil
711}
712
713// tickTransition handles the transition between price ticks during a swap
714func (i *poolV1) tickTransition(step StepComputations, zeroForOne bool, state SwapState, pool *pl.Pool, cache *SwapCache, onTickCross tickCrossHookFn) SwapState {
715	// ensure existing state to keep immutability
716	newState := state
717
718	if step.initialized {
719		// Compute oracle values on first initialized tick cross
720		if !cache.computedLatestObservation {
721			observationState := pool.ObservationState()
722			if observationState != nil {
723				tickCumulative, secondsPerLiquidityStr, err := observeSingle(
724					observationState,
725					cache.blockTimestamp,
726					0,
727					state.tick,
728					observationState.Index(),
729					cache.liquidityStart,
730					observationState.Cardinality(),
731				)
732				if err == nil {
733					cache.tickCumulative = tickCumulative
734					cache.secondsPerLiquidityCumulativeX128 = u256.MustFromDecimal(secondsPerLiquidityStr)
735					cache.computedLatestObservation = true
736				}
737			}
738		}
739
740		if cache.secondsPerLiquidityCumulativeX128 == nil {
741			cache.secondsPerLiquidityCumulativeX128 = u256.Zero()
742		}
743
744		fee0, fee1 := u256.Zero(), u256.Zero()
745
746		if zeroForOne {
747			fee0 = state.feeGrowthGlobalX128
748			fee1 = pool.FeeGrowthGlobal1X128()
749		} else {
750			fee0 = pool.FeeGrowthGlobal0X128()
751			fee1 = state.feeGrowthGlobalX128
752		}
753
754		liquidityNet := tickCross(
755			pool,
756			step.tickNext,
757			fee0,
758			fee1,
759			cache.secondsPerLiquidityCumulativeX128,
760			cache.tickCumulative,
761			cache.blockTimestamp,
762		)
763
764		if zeroForOne {
765			liquidityNet = i256.Zero().Neg(liquidityNet)
766		}
767
768		newState.liquidity = gnsmath.LiquidityMathAddDelta(state.liquidity, liquidityNet)
769
770		if onTickCross != nil {
771			onTickCross(pool.PoolPath(), step.tickNext, zeroForOne, cache.blockTimestamp)
772		}
773	}
774
775	newState.tick = step.tickNext
776	if zeroForOne {
777		newState.tick = step.tickNext - 1
778	}
779
780	return newState
781}