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.gno

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