Search Apps Documentation Source Content File Folder Download Copy Actions Download

swap.gno

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