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